Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/core/command-generation/adapters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ export { opencodeAdapter } from './opencode.js';
export { qoderAdapter } from './qoder.js';
export { qwenAdapter } from './qwen.js';
export { roocodeAdapter } from './roocode.js';
export { traeAdapter } from './trae.js';
export { windsurfAdapter } from './windsurf.js';
47 changes: 47 additions & 0 deletions src/core/command-generation/adapters/trae.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Trae Command Adapter
*
* Formats commands for Trae following its frontmatter specification.
* Trae uses a similar frontmatter format to Cursor with file naming convention.
*/

import path from 'path';
import type { CommandContent, ToolCommandAdapter } from '../types.js';

/**
* Escapes a string value for safe YAML output.
* Quotes the string if it contains special YAML characters.
*/
function escapeYamlValue(value: string): string {
const needsQuoting = /[:\n\r#{}[\],&*!|>'"%@`]|^\s|\s$/.test(value);
if (needsQuoting) {
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
return `"${escaped}"`;
}
return value;
}

/**
* Trae adapter for command generation.
* File path: .trae/commands/opsx-<id>.md
* Frontmatter: name (as /opsx-<id>), id, category, description
*/
export const traeAdapter: ToolCommandAdapter = {
toolId: 'trae',

getFilePath(commandId: string): string {
return path.join('.trae', 'commands', `opsx-${commandId}.md`);
},

formatFile(content: CommandContent): string {
return `---
name: /opsx-${content.id}
id: opsx-${content.id}
category: ${escapeYamlValue(content.category)}
description: ${escapeYamlValue(content.description)}
---

${content.body}
`;
Comment on lines +32 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Sanitize commandId for file paths and escape name/id in frontmatter.

Line 33 builds a path directly from commandId; if it contains path separators or drive prefixes, it can escape .trae/commands. Also name/id are unescaped, so YAML-breaking characters in content.id could corrupt frontmatter. Sanitize the ID and reuse it for both the path and the frontmatter values.

🔧 Suggested fix
+function sanitizeCommandId(commandId: string): string {
+  const sanitized = commandId
+    .replace(/[\\/:]/g, '-')
+    .replace(/[\r\n]/g, ' ')
+    .trim();
+  if (!sanitized) {
+    throw new Error('Command id must not be empty');
+  }
+  return sanitized;
+}
+
 export const traeAdapter: ToolCommandAdapter = {
   toolId: 'trae',
 
   getFilePath(commandId: string): string {
-    return path.join('.trae', 'commands', `opsx-${commandId}.md`);
+    const safeId = sanitizeCommandId(commandId);
+    return path.join('.trae', 'commands', `opsx-${safeId}.md`);
   },
 
   formatFile(content: CommandContent): string {
+    const safeId = sanitizeCommandId(content.id);
+    const name = `/opsx-${safeId}`;
+    const id = `opsx-${safeId}`;
     return `---
-name: /opsx-${content.id}
-id: opsx-${content.id}
+name: ${escapeYamlValue(name)}
+id: ${escapeYamlValue(id)}
 category: ${escapeYamlValue(content.category)}
 description: ${escapeYamlValue(content.description)}
 ---
 
 ${content.body}
 `;
   },
 };
🤖 Prompt for AI Agents
In `@src/core/command-generation/adapters/trae.ts` around lines 32 - 45, Sanitize
the commandId before using it in file paths and frontmatter: in
getFilePath(commandId) normalize and strip any path separators, drive letters or
unsafe characters (e.g., allow only alphanumerics, dashes/underscores) to
produce a safeId and use that for the path (replace
path.join('.trae','commands',`opsx-${safeId}.md`)). In formatFile(content)
compute the same sanitized id from content.id and use it for both the
frontmatter name and id fields, and ensure those fields are escaped or quoted
(in addition to using escapeYamlValue for category/description) so YAML-breaking
characters cannot corrupt the frontmatter.

},
};
2 changes: 2 additions & 0 deletions src/core/command-generation/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { opencodeAdapter } from './adapters/opencode.js';
import { qoderAdapter } from './adapters/qoder.js';
import { qwenAdapter } from './adapters/qwen.js';
import { roocodeAdapter } from './adapters/roocode.js';
import { traeAdapter } from './adapters/trae.js';
import { windsurfAdapter } from './adapters/windsurf.js';

/**
Expand Down Expand Up @@ -56,6 +57,7 @@ export class CommandAdapterRegistry {
CommandAdapterRegistry.register(qoderAdapter);
CommandAdapterRegistry.register(qwenAdapter);
CommandAdapterRegistry.register(roocodeAdapter);
CommandAdapterRegistry.register(traeAdapter);
CommandAdapterRegistry.register(windsurfAdapter);
}

Expand Down