From 15563ecd170f2111e2de3d7ce9489260dfec2c68 Mon Sep 17 00:00:00 2001 From: Trae Date: Mon, 13 Jul 2026 13:48:43 +0800 Subject: [PATCH 1/3] feat: add Easy Code as a supported AI tool - Register 'easycode' in AI_TOOLS (config.ts) with skillsDir '.easycode' - Add EasycodeAdapter (easycode.ts): generates TOML commands at .easycode/commands/opsx/.toml, matching Easy Code's native format - Export easycodeAdapter from adapters/index.ts - Register easycodeAdapter in CommandAdapterRegistry Easy Code (https://easycode.ai) is a terminal-based AI coding assistant. Its commands use TOML with a description field and a prompt multiline literal string, distinct from the Markdown/YAML frontmatter format used by most other tools. Tested locally: `openspec init --tools easycode` generates 5 SKILL.md files and 5 .toml command files in the expected directory structure. --- .../command-generation/adapters/easycode.ts | 27 +++++++++++++++++++ src/core/command-generation/adapters/index.ts | 1 + src/core/command-generation/registry.ts | 2 ++ src/core/config.ts | 1 + 4 files changed, 31 insertions(+) create mode 100644 src/core/command-generation/adapters/easycode.ts diff --git a/src/core/command-generation/adapters/easycode.ts b/src/core/command-generation/adapters/easycode.ts new file mode 100644 index 0000000000..b760f6789d --- /dev/null +++ b/src/core/command-generation/adapters/easycode.ts @@ -0,0 +1,27 @@ +/** + * 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'; + +/** + * Easy Code adapter for command generation. + * File path: .easycode/commands/opsx/.toml + * Format: TOML with description string and prompt multiline literal string + */ +export const easycodeAdapter: ToolCommandAdapter = { + toolId: 'easycode', + + getFilePath(commandId: string): string { + return path.join('.easycode', 'commands', 'opsx', `${commandId}.toml`); + }, + + formatFile(content: CommandContent): string { + const safeDesc = content.description.replace(/"/g, '\\"'); + return `description = "${safeDesc}"\n\nprompt = '''\n${content.body}\n'''\n`; + }, +}; diff --git a/src/core/command-generation/adapters/index.ts b/src/core/command-generation/adapters/index.ts index 89d0fe5201..b613756afb 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 c2773ac410..0e07931a9f 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/config.ts b/src/core/config.ts index 55062273d0..6132672f3e 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'] }, From b943f369dc13ab7ab70c392935d430878d5c05c2 Mon Sep 17 00:00:00 2001 From: Trae Date: Mon, 13 Jul 2026 14:05:23 +0800 Subject: [PATCH 2/3] fix: robust TOML serialization for Easy Code adapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per code review: the original formatFile had unsafe manual escaping that would corrupt output for descriptions containing backslashes or control characters, and prompt bodies containing triple-single-quotes. Changes: - Add src/core/command-generation/toml.ts with two helpers: escapeTOMLBasicString — escapes \, ", \n, \r, \t for TOML basic strings (double-quoted) escapeTOMLMultilineString — escapes \ and \r, and breaks any run of 3+ consecutive " (lookahead match) for TOML basic multiline strings - Switch prompt block from triple-single-quote literal string (''') to triple-double-quote basic multiline string ("""), which allows full escape sequence support and handles arbitrary body content - Update easycode.ts to use both helpers --- .../command-generation/adapters/easycode.ts | 11 ++-- src/core/command-generation/toml.ts | 50 +++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 src/core/command-generation/toml.ts diff --git a/src/core/command-generation/adapters/easycode.ts b/src/core/command-generation/adapters/easycode.ts index b760f6789d..dba3c424d3 100644 --- a/src/core/command-generation/adapters/easycode.ts +++ b/src/core/command-generation/adapters/easycode.ts @@ -7,11 +7,15 @@ 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: TOML with description string and prompt multiline literal string + * + * Format: + * description = "" single-line, backslash/quote-safe + * prompt = """""" multiline, backslash/triple-quote-safe */ export const easycodeAdapter: ToolCommandAdapter = { toolId: 'easycode', @@ -21,7 +25,8 @@ export const easycodeAdapter: ToolCommandAdapter = { }, formatFile(content: CommandContent): string { - const safeDesc = content.description.replace(/"/g, '\\"'); - return `description = "${safeDesc}"\n\nprompt = '''\n${content.body}\n'''\n`; + 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/toml.ts b/src/core/command-generation/toml.ts new file mode 100644 index 0000000000..1585dfbef1 --- /dev/null +++ b/src/core/command-generation/toml.ts @@ -0,0 +1,50 @@ +/** + * 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. + */ + +/** + * 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. + * + * @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 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) + * + * @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 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, '\\"'); +} From 179890b1cdf0c5eaeea5387ab576be293f39cc62 Mon Sep 17 00:00:00 2001 From: Trae Date: Mon, 13 Jul 2026 14:09:14 +0800 Subject: [PATCH 3/3] fix: escape disallowed control characters in TOML helpers Per code review: TOML basic strings forbid U+0000-U+0008, U+000B-U+000C, U+000E-U+001F, and U+007F. Add escapeControlChars() helper that replaces these with \uXXXX sequences, and apply it in both escapeTOMLBasicString and escapeTOMLMultilineString after their named-escape passes. --- src/core/command-generation/toml.ts | 49 +++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/src/core/command-generation/toml.ts b/src/core/command-generation/toml.ts index 1585dfbef1..64b30a0490 100644 --- a/src/core/command-generation/toml.ts +++ b/src/core/command-generation/toml.ts @@ -6,22 +6,41 @@ * 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. + * \\\\ 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 value - .replace(/\\/g, '\\\\') - .replace(/"/g, '\\"') - .replace(/\n/g, '\\n') - .replace(/\r/g, '\\r') - .replace(/\t/g, '\\t'); + return escapeControlChars( + value + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/\t/g, '\\t') + ); } /** @@ -36,15 +55,19 @@ export function escapeTOMLBasicString(value: string): string { * - 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 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, '\\"'); + 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, '\\"') + ); }