diff --git a/src/core/command-generation/adapters/easycode.ts b/src/core/command-generation/adapters/easycode.ts new file mode 100644 index 000000000..dba3c424d --- /dev/null +++ b/src/core/command-generation/adapters/easycode.ts @@ -0,0 +1,32 @@ +/** + * Easy Code Command Adapter + * + * Formats commands for Easy Code using TOML format. + * Easy Code stores commands as TOML files at .easycode/commands/opsx/.toml + */ + +import path from 'path'; +import type { CommandContent, ToolCommandAdapter } from '../types.js'; +import { escapeTOMLBasicString, escapeTOMLMultilineString } from '../toml.js'; + +/** + * Easy Code adapter for command generation. + * File path: .easycode/commands/opsx/.toml + * + * Format: + * description = "" single-line, backslash/quote-safe + * prompt = """""" multiline, backslash/triple-quote-safe + */ +export const easycodeAdapter: ToolCommandAdapter = { + toolId: 'easycode', + + getFilePath(commandId: string): string { + return path.join('.easycode', 'commands', 'opsx', `${commandId}.toml`); + }, + + formatFile(content: CommandContent): string { + const safeDesc = escapeTOMLBasicString(content.description); + const safeBody = escapeTOMLMultilineString(content.body); + return `description = "${safeDesc}"\n\nprompt = """\n${safeBody}\n"""\n`; + }, +}; diff --git a/src/core/command-generation/adapters/index.ts b/src/core/command-generation/adapters/index.ts index 89d0fe520..b613756af 100644 --- a/src/core/command-generation/adapters/index.ts +++ b/src/core/command-generation/adapters/index.ts @@ -16,6 +16,7 @@ export { continueAdapter } from './continue.js'; export { costrictAdapter } from './costrict.js'; export { crushAdapter } from './crush.js'; export { cursorAdapter } from './cursor.js'; +export { easycodeAdapter } from './easycode.js'; export { factoryAdapter } from './factory.js'; export { geminiAdapter } from './gemini.js'; export { githubCopilotAdapter } from './github-copilot.js'; diff --git a/src/core/command-generation/registry.ts b/src/core/command-generation/registry.ts index c2773ac41..0e07931a9 100644 --- a/src/core/command-generation/registry.ts +++ b/src/core/command-generation/registry.ts @@ -18,6 +18,7 @@ import { continueAdapter } from './adapters/continue.js'; import { costrictAdapter } from './adapters/costrict.js'; import { crushAdapter } from './adapters/crush.js'; import { cursorAdapter } from './adapters/cursor.js'; +import { easycodeAdapter } from './adapters/easycode.js'; import { factoryAdapter } from './adapters/factory.js'; import { geminiAdapter } from './adapters/gemini.js'; import { githubCopilotAdapter } from './adapters/github-copilot.js'; @@ -55,6 +56,7 @@ export class CommandAdapterRegistry { CommandAdapterRegistry.register(costrictAdapter); CommandAdapterRegistry.register(crushAdapter); CommandAdapterRegistry.register(cursorAdapter); + CommandAdapterRegistry.register(easycodeAdapter); CommandAdapterRegistry.register(factoryAdapter); CommandAdapterRegistry.register(geminiAdapter); CommandAdapterRegistry.register(githubCopilotAdapter); diff --git a/src/core/command-generation/toml.ts b/src/core/command-generation/toml.ts new file mode 100644 index 000000000..64b30a049 --- /dev/null +++ b/src/core/command-generation/toml.ts @@ -0,0 +1,73 @@ +/** + * Shared TOML serialization helpers for command adapters. + * + * Easy Code commands are stored as TOML files. This module provides + * safe serialization for TOML string values so generated files stay + * valid regardless of the content being embedded. + */ + +/** + * Replaces disallowed control characters with TOML \uXXXX escape sequences. + * + * TOML basic strings forbid U+0000–U+0008, U+000B–U+000C, U+000E–U+001F, and + * U+007F. The caller is expected to have already handled \t (U+0009), \n + * (U+000A), and \r (U+000D) via their named escape sequences before calling + * this helper. + */ +function escapeControlChars(value: string): string { + return value.replace( + /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g, + (ch) => `\\u${ch.codePointAt(0)!.toString(16).padStart(4, '0')}` + ); +} + +/** + * Escapes a string for use as a TOML basic string (double-quoted). + * + * TOML basic strings support the following escape sequences: + * \\\\ backslash, \\" double quote, \\n line feed, \\r carriage return, \\t tab. + * All other control characters (U+0000–U+0008, U+000B–U+000C, U+000E–U+001F, + * U+007F) are replaced with \\uXXXX unicode escape sequences. + * + * @param value - The raw string to embed in a TOML basic string. + * @returns The escaped string, suitable for wrapping in double quotes. + */ +export function escapeTOMLBasicString(value: string): string { + return escapeControlChars( + value + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/\t/g, '\\t') + ); +} + +/** + * Prepares a string for use as a TOML basic multiline string (triple double-quoted). + * + * TOML basic multiline strings support the same escape sequences as basic strings. + * We use them instead of literal multiline strings (triple single-quoted) because + * literal strings cannot escape anything — a literal ''' would close the block. + * + * Inside a basic multiline string: + * - backslashes must be escaped first + * - runs of 3+ consecutive double quotes would close the block; we escape the + * first quote in each triple to break the run + * - newlines are preserved verbatim (TOML allows real newlines in multiline strings) + * All other control characters (U+0000–U+0008, U+000B–U+000C, U+000E–U+001F, + * U+007F) are replaced with \\uXXXX unicode escape sequences. + * + * @param value - The raw multiline string to embed. + * @returns The escaped string, suitable for wrapping in triple double quotes. + */ +export function escapeTOMLMultilineString(value: string): string { + return escapeControlChars( + value + .replace(/\\/g, '\\\\') + .replace(/\r/g, '\\r') + // Escape the first quote in any run of 3+ consecutive double quotes + // so they cannot prematurely close the triple-double-quote block. + .replace(/"(?="")/g, '\\"') + ); +} diff --git a/src/core/config.ts b/src/core/config.ts index 55062273d..6132672f3 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -32,6 +32,7 @@ export const AI_TOOLS: AIToolOption[] = [ { name: 'CoStrict', value: 'costrict', available: true, successLabel: 'CoStrict', skillsDir: '.cospec' }, { name: 'Crush', value: 'crush', available: true, successLabel: 'Crush', skillsDir: '.crush' }, { name: 'Cursor', value: 'cursor', available: true, successLabel: 'Cursor', skillsDir: '.cursor' }, + { name: 'Easy Code', value: 'easycode', available: true, successLabel: 'Easy Code', skillsDir: '.easycode' }, { name: 'Factory Droid', value: 'factory', available: true, successLabel: 'Factory Droid', skillsDir: '.factory' }, { name: 'Gemini CLI', value: 'gemini', available: true, successLabel: 'Gemini CLI', skillsDir: '.gemini' }, { name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot', skillsDir: '.github', detectionPaths: ['.github/copilot-instructions.md', '.github/instructions', '.github/workflows/copilot-setup-steps.yml', '.github/prompts', '.github/agents', '.github/skills', '.github/.mcp.json'] },