@pixelbyte-software/pixcode 1.34.0 → 1.35.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/LICENSE +718 -718
  2. package/README.de.md +248 -248
  3. package/README.ja.md +240 -240
  4. package/README.ko.md +240 -240
  5. package/README.md +303 -303
  6. package/README.ru.md +248 -248
  7. package/README.tr.md +250 -250
  8. package/README.zh-CN.md +240 -240
  9. package/dist/api-docs.html +548 -395
  10. package/dist/assets/index-B8w57E1r.css +32 -0
  11. package/dist/assets/index-CBdsvGSR.js +854 -0
  12. package/dist/clear-cache.html +85 -85
  13. package/dist/convert-icons.md +52 -52
  14. package/dist/favicon.svg +8 -8
  15. package/dist/generate-icons.js +48 -48
  16. package/dist/icons/codex-white.svg +3 -3
  17. package/dist/icons/codex.svg +3 -3
  18. package/dist/icons/cursor-white.svg +11 -11
  19. package/dist/icons/icon-128x128.svg +9 -9
  20. package/dist/icons/icon-144x144.svg +9 -9
  21. package/dist/icons/icon-152x152.svg +9 -9
  22. package/dist/icons/icon-192x192.svg +9 -9
  23. package/dist/icons/icon-384x384.svg +9 -9
  24. package/dist/icons/icon-512x512.svg +9 -9
  25. package/dist/icons/icon-72x72.svg +9 -9
  26. package/dist/icons/icon-96x96.svg +9 -9
  27. package/dist/icons/icon-template.svg +9 -9
  28. package/dist/icons/qwen-logo.svg +14 -14
  29. package/dist/index.html +59 -59
  30. package/dist/logo.svg +12 -12
  31. package/dist/manifest.json +60 -60
  32. package/dist/openapi.yaml +1693 -1311
  33. package/dist/sw.js +124 -124
  34. package/dist-server/server/claude-sdk.js +38 -7
  35. package/dist-server/server/claude-sdk.js.map +1 -1
  36. package/dist-server/server/cli.js +107 -112
  37. package/dist-server/server/cli.js.map +1 -1
  38. package/dist-server/server/daemon/manager.js +33 -33
  39. package/dist-server/server/daemon-manager.js +159 -112
  40. package/dist-server/server/daemon-manager.js.map +1 -1
  41. package/dist-server/server/database/json-store.js +8 -5
  42. package/dist-server/server/database/json-store.js.map +1 -1
  43. package/dist-server/server/index.js +31 -10
  44. package/dist-server/server/index.js.map +1 -1
  45. package/dist-server/server/modules/orchestration/a2a/adapter-registry.js +45 -19
  46. package/dist-server/server/modules/orchestration/a2a/adapter-registry.js.map +1 -1
  47. package/dist-server/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.js.map +1 -1
  48. package/dist-server/server/modules/orchestration/a2a/adapters/claude-code.adapter.js +1 -0
  49. package/dist-server/server/modules/orchestration/a2a/adapters/claude-code.adapter.js.map +1 -1
  50. package/dist-server/server/modules/orchestration/a2a/adapters/codex.adapter.js +202 -0
  51. package/dist-server/server/modules/orchestration/a2a/adapters/codex.adapter.js.map +1 -0
  52. package/dist-server/server/modules/orchestration/a2a/adapters/cursor.adapter.js +205 -0
  53. package/dist-server/server/modules/orchestration/a2a/adapters/cursor.adapter.js.map +1 -0
  54. package/dist-server/server/modules/orchestration/a2a/adapters/gemini.adapter.js +205 -0
  55. package/dist-server/server/modules/orchestration/a2a/adapters/gemini.adapter.js.map +1 -0
  56. package/dist-server/server/modules/orchestration/a2a/adapters/opencode.adapter.js +205 -0
  57. package/dist-server/server/modules/orchestration/a2a/adapters/opencode.adapter.js.map +1 -0
  58. package/dist-server/server/modules/orchestration/a2a/adapters/qwen.adapter.js +205 -0
  59. package/dist-server/server/modules/orchestration/a2a/adapters/qwen.adapter.js.map +1 -0
  60. package/dist-server/server/modules/orchestration/a2a/routes.js +298 -34
  61. package/dist-server/server/modules/orchestration/a2a/routes.js.map +1 -1
  62. package/dist-server/server/modules/orchestration/a2a/task-store.js +144 -0
  63. package/dist-server/server/modules/orchestration/a2a/task-store.js.map +1 -0
  64. package/dist-server/server/modules/orchestration/a2a/validator.js +16 -0
  65. package/dist-server/server/modules/orchestration/a2a/validator.js.map +1 -1
  66. package/dist-server/server/modules/orchestration/index.js +14 -0
  67. package/dist-server/server/modules/orchestration/index.js.map +1 -1
  68. package/dist-server/server/modules/orchestration/preview/port-watcher.js +90 -0
  69. package/dist-server/server/modules/orchestration/preview/port-watcher.js.map +1 -0
  70. package/dist-server/server/modules/orchestration/preview/preview-proxy.js +58 -0
  71. package/dist-server/server/modules/orchestration/preview/preview-proxy.js.map +1 -0
  72. package/dist-server/server/modules/orchestration/preview/types.js +2 -0
  73. package/dist-server/server/modules/orchestration/preview/types.js.map +1 -0
  74. package/dist-server/server/modules/orchestration/tasks/orchestration-task-store.js +37 -0
  75. package/dist-server/server/modules/orchestration/tasks/orchestration-task-store.js.map +1 -0
  76. package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js +68 -0
  77. package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js.map +1 -0
  78. package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js +128 -0
  79. package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js.map +1 -0
  80. package/dist-server/server/modules/orchestration/tasks/orchestration-task.types.js +2 -0
  81. package/dist-server/server/modules/orchestration/tasks/orchestration-task.types.js.map +1 -0
  82. package/dist-server/server/modules/orchestration/workflows/built-in-workflows.js +126 -0
  83. package/dist-server/server/modules/orchestration/workflows/built-in-workflows.js.map +1 -0
  84. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +1047 -0
  85. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -0
  86. package/dist-server/server/modules/orchestration/workflows/workflow-store.js +76 -0
  87. package/dist-server/server/modules/orchestration/workflows/workflow-store.js.map +1 -0
  88. package/dist-server/server/modules/orchestration/workflows/workflow.routes.js +151 -0
  89. package/dist-server/server/modules/orchestration/workflows/workflow.routes.js.map +1 -0
  90. package/dist-server/server/modules/orchestration/workflows/workflow.types.js +2 -0
  91. package/dist-server/server/modules/orchestration/workflows/workflow.types.js.map +1 -0
  92. package/dist-server/server/modules/orchestration/workflows/workspace-target.js +98 -0
  93. package/dist-server/server/modules/orchestration/workflows/workspace-target.js.map +1 -0
  94. package/dist-server/server/modules/orchestration/workspace/docker-workspace.js +122 -0
  95. package/dist-server/server/modules/orchestration/workspace/docker-workspace.js.map +1 -0
  96. package/dist-server/server/modules/orchestration/workspace/path-safety.js +48 -0
  97. package/dist-server/server/modules/orchestration/workspace/path-safety.js.map +1 -0
  98. package/dist-server/server/modules/orchestration/workspace/types.js +11 -0
  99. package/dist-server/server/modules/orchestration/workspace/types.js.map +1 -0
  100. package/dist-server/server/modules/orchestration/workspace/workspace-manager.js +80 -0
  101. package/dist-server/server/modules/orchestration/workspace/workspace-manager.js.map +1 -0
  102. package/dist-server/server/modules/orchestration/workspace/worktree-workspace.js +96 -0
  103. package/dist-server/server/modules/orchestration/workspace/worktree-workspace.js.map +1 -0
  104. package/dist-server/server/modules/providers/index.js +3 -0
  105. package/dist-server/server/modules/providers/index.js.map +1 -0
  106. package/dist-server/server/openai-codex.js +35 -4
  107. package/dist-server/server/openai-codex.js.map +1 -1
  108. package/dist-server/server/routes/commands.js +25 -25
  109. package/dist-server/server/routes/git.js +17 -17
  110. package/dist-server/server/routes/taskmaster.js +525 -508
  111. package/dist-server/server/routes/taskmaster.js.map +1 -1
  112. package/package.json +180 -178
  113. package/scripts/fix-node-pty.js +67 -67
  114. package/scripts/smoke/a2a-roundtrip.mjs +86 -17
  115. package/scripts/smoke/orchestration-api.mjs +172 -0
  116. package/scripts/smoke/orchestration-live-run.mjs +176 -0
  117. package/server/claude-sdk.js +898 -857
  118. package/server/cli.js +935 -940
  119. package/server/constants/config.js +4 -4
  120. package/server/cursor-cli.js +342 -342
  121. package/server/daemon/manager.js +564 -564
  122. package/server/daemon-manager.js +959 -920
  123. package/server/database/db.js +794 -794
  124. package/server/database/json-store.js +197 -194
  125. package/server/gemini-cli.js +535 -535
  126. package/server/gemini-response-handler.js +79 -79
  127. package/server/index.js +3135 -3104
  128. package/server/load-env.js +34 -34
  129. package/server/middleware/auth.js +173 -173
  130. package/server/modules/orchestration/a2a/adapter-registry.ts +72 -22
  131. package/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts +9 -3
  132. package/server/modules/orchestration/a2a/adapters/claude-code.adapter.ts +1 -0
  133. package/server/modules/orchestration/a2a/adapters/codex.adapter.ts +244 -0
  134. package/server/modules/orchestration/a2a/adapters/cursor.adapter.ts +249 -0
  135. package/server/modules/orchestration/a2a/adapters/gemini.adapter.ts +248 -0
  136. package/server/modules/orchestration/a2a/adapters/opencode.adapter.ts +248 -0
  137. package/server/modules/orchestration/a2a/adapters/qwen.adapter.ts +248 -0
  138. package/server/modules/orchestration/a2a/routes.ts +349 -36
  139. package/server/modules/orchestration/a2a/task-store.ts +178 -0
  140. package/server/modules/orchestration/a2a/types.ts +14 -0
  141. package/server/modules/orchestration/a2a/validator.ts +25 -2
  142. package/server/modules/orchestration/index.ts +40 -0
  143. package/server/modules/orchestration/preview/port-watcher.ts +112 -0
  144. package/server/modules/orchestration/preview/preview-proxy.ts +60 -0
  145. package/server/modules/orchestration/preview/types.ts +19 -0
  146. package/server/modules/orchestration/tasks/orchestration-task-store.ts +45 -0
  147. package/server/modules/orchestration/tasks/orchestration-task.routes.ts +73 -0
  148. package/server/modules/orchestration/tasks/orchestration-task.service.ts +145 -0
  149. package/server/modules/orchestration/tasks/orchestration-task.types.ts +29 -0
  150. package/server/modules/orchestration/workflows/built-in-workflows.ts +127 -0
  151. package/server/modules/orchestration/workflows/workflow-runner.ts +1206 -0
  152. package/server/modules/orchestration/workflows/workflow-store.ts +97 -0
  153. package/server/modules/orchestration/workflows/workflow.routes.ts +169 -0
  154. package/server/modules/orchestration/workflows/workflow.types.ts +70 -0
  155. package/server/modules/orchestration/workflows/workspace-target.ts +120 -0
  156. package/server/modules/orchestration/workspace/docker-workspace.ts +135 -0
  157. package/server/modules/orchestration/workspace/path-safety.ts +55 -0
  158. package/server/modules/orchestration/workspace/types.ts +52 -0
  159. package/server/modules/orchestration/workspace/workspace-manager.ts +97 -0
  160. package/server/modules/orchestration/workspace/worktree-workspace.ts +125 -0
  161. package/server/modules/providers/index.ts +2 -0
  162. package/server/modules/providers/list/claude/claude-auth.provider.ts +145 -145
  163. package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -135
  164. package/server/modules/providers/list/claude/claude-sessions.provider.ts +306 -306
  165. package/server/modules/providers/list/claude/claude.provider.ts +15 -15
  166. package/server/modules/providers/list/codex/codex-auth.provider.ts +115 -115
  167. package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -135
  168. package/server/modules/providers/list/codex/codex-sessions.provider.ts +319 -319
  169. package/server/modules/providers/list/codex/codex.provider.ts +15 -15
  170. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +143 -143
  171. package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -108
  172. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +421 -421
  173. package/server/modules/providers/list/cursor/cursor.provider.ts +15 -15
  174. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +163 -163
  175. package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +110 -110
  176. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +227 -227
  177. package/server/modules/providers/list/gemini/gemini.provider.ts +15 -15
  178. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +130 -130
  179. package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +126 -126
  180. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +232 -232
  181. package/server/modules/providers/list/opencode/opencode.provider.ts +29 -29
  182. package/server/modules/providers/list/qwen/qwen-auth.provider.ts +145 -145
  183. package/server/modules/providers/list/qwen/qwen-mcp.provider.ts +114 -114
  184. package/server/modules/providers/list/qwen/qwen-sessions.provider.ts +265 -265
  185. package/server/modules/providers/list/qwen/qwen.provider.ts +21 -21
  186. package/server/modules/providers/provider.registry.ts +40 -40
  187. package/server/modules/providers/provider.routes.ts +819 -819
  188. package/server/modules/providers/services/mcp.service.ts +86 -86
  189. package/server/modules/providers/services/provider-auth.service.ts +26 -26
  190. package/server/modules/providers/services/sessions.service.ts +45 -45
  191. package/server/modules/providers/shared/base/abstract.provider.ts +20 -20
  192. package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -151
  193. package/server/modules/providers/shared/provider-configs.ts +142 -142
  194. package/server/modules/providers/tests/mcp.test.ts +293 -293
  195. package/server/openai-codex.js +462 -426
  196. package/server/opencode-cli.js +459 -459
  197. package/server/opencode-response-handler.js +107 -107
  198. package/server/projects.js +3105 -3105
  199. package/server/qwen-code-cli.js +395 -395
  200. package/server/qwen-response-handler.js +73 -73
  201. package/server/routes/agent.js +1365 -1365
  202. package/server/routes/auth.js +138 -138
  203. package/server/routes/codex.js +19 -19
  204. package/server/routes/commands.js +554 -554
  205. package/server/routes/cursor.js +52 -52
  206. package/server/routes/gemini.js +24 -24
  207. package/server/routes/git.js +1488 -1488
  208. package/server/routes/mcp-utils.js +31 -31
  209. package/server/routes/messages.js +61 -61
  210. package/server/routes/network.js +120 -120
  211. package/server/routes/plugins.js +318 -318
  212. package/server/routes/projects.js +915 -915
  213. package/server/routes/qwen.js +27 -27
  214. package/server/routes/settings.js +286 -286
  215. package/server/routes/taskmaster.js +1496 -1471
  216. package/server/routes/telegram.js +125 -125
  217. package/server/routes/user.js +123 -123
  218. package/server/services/external-access.js +171 -171
  219. package/server/services/install-jobs.js +571 -571
  220. package/server/services/notification-orchestrator.js +242 -242
  221. package/server/services/provider-credentials.js +189 -189
  222. package/server/services/provider-models.js +381 -381
  223. package/server/services/telegram/bot.js +279 -279
  224. package/server/services/telegram/telegram-http-client.js +130 -130
  225. package/server/services/telegram/translations.js +170 -170
  226. package/server/services/vapid-keys.js +36 -36
  227. package/server/sessionManager.js +225 -225
  228. package/server/shared/interfaces.ts +54 -54
  229. package/server/shared/types.ts +172 -172
  230. package/server/shared/utils.ts +193 -193
  231. package/server/tsconfig.json +36 -36
  232. package/server/utils/colors.js +21 -21
  233. package/server/utils/commandParser.js +303 -303
  234. package/server/utils/frontmatter.js +18 -18
  235. package/server/utils/gitConfig.js +34 -34
  236. package/server/utils/mcp-detector.js +147 -147
  237. package/server/utils/plugin-loader.js +457 -457
  238. package/server/utils/plugin-process-manager.js +184 -184
  239. package/server/utils/port-access.js +209 -209
  240. package/server/utils/runtime-paths.js +37 -37
  241. package/server/utils/taskmaster-websocket.js +128 -128
  242. package/server/utils/url-detection.js +71 -71
  243. package/server/vite-daemon.js +78 -78
  244. package/shared/modelConstants.js +162 -162
  245. package/shared/networkHosts.js +22 -22
  246. package/dist/assets/index-B1ghfb4w.css +0 -32
  247. package/dist/assets/index-BvClqlMf.js +0 -852
@@ -1,554 +1,554 @@
1
- import express from 'express';
2
- import { promises as fs } from 'fs';
3
- import path from 'path';
4
- import os from 'os';
5
- import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants.js';
6
- import { parseFrontmatter } from '../utils/frontmatter.js';
7
- import { findAppRoot, getModuleDir } from '../utils/runtime-paths.js';
8
-
9
- const __dirname = getModuleDir(import.meta.url);
10
- // This route reads the top-level package.json for the status command, so it needs the real
11
- // app root even after compilation moves the route file under dist-server/server/routes.
12
- const APP_ROOT = findAppRoot(__dirname);
13
-
14
- const router = express.Router();
15
-
16
- /**
17
- * Recursively scan directory for command files (.md)
18
- * @param {string} dir - Directory to scan
19
- * @param {string} baseDir - Base directory for relative paths
20
- * @param {string} namespace - Namespace for commands (e.g., 'project', 'user')
21
- * @returns {Promise<Array>} Array of command objects
22
- */
23
- async function scanCommandsDirectory(dir, baseDir, namespace) {
24
- const commands = [];
25
-
26
- try {
27
- // Check if directory exists
28
- await fs.access(dir);
29
-
30
- const entries = await fs.readdir(dir, { withFileTypes: true });
31
-
32
- for (const entry of entries) {
33
- const fullPath = path.join(dir, entry.name);
34
-
35
- if (entry.isDirectory()) {
36
- // Recursively scan subdirectories
37
- const subCommands = await scanCommandsDirectory(fullPath, baseDir, namespace);
38
- commands.push(...subCommands);
39
- } else if (entry.isFile() && entry.name.endsWith('.md')) {
40
- // Parse markdown file for metadata
41
- try {
42
- const content = await fs.readFile(fullPath, 'utf8');
43
- const { data: frontmatter, content: commandContent } = parseFrontmatter(content);
44
-
45
- // Calculate relative path from baseDir for command name
46
- const relativePath = path.relative(baseDir, fullPath);
47
- // Remove .md extension and convert to command name
48
- const commandName = '/' + relativePath.replace(/\.md$/, '').replace(/\\/g, '/');
49
-
50
- // Extract description from frontmatter or first line of content
51
- let description = frontmatter.description || '';
52
- if (!description) {
53
- const firstLine = commandContent.trim().split('\n')[0];
54
- description = firstLine.replace(/^#+\s*/, '').trim();
55
- }
56
-
57
- commands.push({
58
- name: commandName,
59
- path: fullPath,
60
- relativePath,
61
- description,
62
- namespace,
63
- metadata: frontmatter
64
- });
65
- } catch (err) {
66
- console.error(`Error parsing command file ${fullPath}:`, err.message);
67
- }
68
- }
69
- }
70
- } catch (err) {
71
- // Directory doesn't exist or can't be accessed - this is okay
72
- if (err.code !== 'ENOENT' && err.code !== 'EACCES') {
73
- console.error(`Error scanning directory ${dir}:`, err.message);
74
- }
75
- }
76
-
77
- return commands;
78
- }
79
-
80
- /**
81
- * Built-in commands that are always available
82
- */
83
- const builtInCommands = [
84
- {
85
- name: '/help',
86
- description: 'Show help documentation for Claude Code',
87
- namespace: 'builtin',
88
- metadata: { type: 'builtin' }
89
- },
90
- {
91
- name: '/clear',
92
- description: 'Clear the conversation history',
93
- namespace: 'builtin',
94
- metadata: { type: 'builtin' }
95
- },
96
- {
97
- name: '/model',
98
- description: 'Switch or view the current AI model',
99
- namespace: 'builtin',
100
- metadata: { type: 'builtin' }
101
- },
102
- {
103
- name: '/cost',
104
- description: 'Display token usage and cost information',
105
- namespace: 'builtin',
106
- metadata: { type: 'builtin' }
107
- },
108
- {
109
- name: '/memory',
110
- description: 'Open CLAUDE.md memory file for editing',
111
- namespace: 'builtin',
112
- metadata: { type: 'builtin' }
113
- },
114
- {
115
- name: '/config',
116
- description: 'Open settings and configuration',
117
- namespace: 'builtin',
118
- metadata: { type: 'builtin' }
119
- },
120
- {
121
- name: '/status',
122
- description: 'Show system status and version information',
123
- namespace: 'builtin',
124
- metadata: { type: 'builtin' }
125
- },
126
- {
127
- name: '/rewind',
128
- description: 'Rewind the conversation to a previous state',
129
- namespace: 'builtin',
130
- metadata: { type: 'builtin' }
131
- }
132
- ];
133
-
134
- /**
135
- * Built-in command handlers
136
- * Each handler returns { type: 'builtin', action: string, data: any }
137
- */
138
- const builtInHandlers = {
139
- '/help': async (args, context) => {
140
- const helpText = `# Claude Code Commands
141
-
142
- ## Built-in Commands
143
-
144
- ${builtInCommands.map(cmd => `### ${cmd.name}
145
- ${cmd.description}
146
- `).join('\n')}
147
-
148
- ## Custom Commands
149
-
150
- Custom commands can be created in:
151
- - Project: \`.claude/commands/\` (project-specific)
152
- - User: \`~/.claude/commands/\` (available in all projects)
153
-
154
- ### Command Syntax
155
-
156
- - **Arguments**: Use \`$ARGUMENTS\` for all args or \`$1\`, \`$2\`, etc. for positional
157
- - **File Includes**: Use \`@filename\` to include file contents
158
- - **Bash Commands**: Use \`!command\` to execute bash commands
159
-
160
- ### Examples
161
-
162
- \`\`\`markdown
163
- /mycommand arg1 arg2
164
- \`\`\`
165
- `;
166
-
167
- return {
168
- type: 'builtin',
169
- action: 'help',
170
- data: {
171
- content: helpText,
172
- format: 'markdown'
173
- }
174
- };
175
- },
176
-
177
- '/clear': async (args, context) => {
178
- return {
179
- type: 'builtin',
180
- action: 'clear',
181
- data: {
182
- message: 'Conversation history cleared'
183
- }
184
- };
185
- },
186
-
187
- '/model': async (args, context) => {
188
- // Read available models from centralized constants
189
- const availableModels = {
190
- claude: CLAUDE_MODELS.OPTIONS.map(o => o.value),
191
- cursor: CURSOR_MODELS.OPTIONS.map(o => o.value),
192
- codex: CODEX_MODELS.OPTIONS.map(o => o.value)
193
- };
194
-
195
- const currentProvider = context?.provider || 'claude';
196
- const currentModel = context?.model || CLAUDE_MODELS.DEFAULT;
197
-
198
- return {
199
- type: 'builtin',
200
- action: 'model',
201
- data: {
202
- current: {
203
- provider: currentProvider,
204
- model: currentModel
205
- },
206
- available: availableModels,
207
- message: args.length > 0
208
- ? `Switching to model: ${args[0]}`
209
- : `Current model: ${currentModel}`
210
- }
211
- };
212
- },
213
-
214
- '/cost': async (args, context) => {
215
- const tokenUsage = context?.tokenUsage || {};
216
- const provider = context?.provider || 'claude';
217
- const model =
218
- context?.model ||
219
- (provider === 'cursor'
220
- ? CURSOR_MODELS.DEFAULT
221
- : provider === 'codex'
222
- ? CODEX_MODELS.DEFAULT
223
- : CLAUDE_MODELS.DEFAULT);
224
-
225
- const used = Number(tokenUsage.used ?? tokenUsage.totalUsed ?? tokenUsage.total_tokens ?? 0) || 0;
226
- const total =
227
- Number(
228
- tokenUsage.total ??
229
- tokenUsage.contextWindow ??
230
- parseInt(process.env.CONTEXT_WINDOW || '160000', 10),
231
- ) || 160000;
232
- const percentage = total > 0 ? Number(((used / total) * 100).toFixed(1)) : 0;
233
-
234
- const inputTokensRaw =
235
- Number(
236
- tokenUsage.inputTokens ??
237
- tokenUsage.input ??
238
- tokenUsage.cumulativeInputTokens ??
239
- tokenUsage.promptTokens ??
240
- 0,
241
- ) || 0;
242
- const outputTokens =
243
- Number(
244
- tokenUsage.outputTokens ??
245
- tokenUsage.output ??
246
- tokenUsage.cumulativeOutputTokens ??
247
- tokenUsage.completionTokens ??
248
- 0,
249
- ) || 0;
250
- const cacheTokens =
251
- Number(
252
- tokenUsage.cacheReadTokens ??
253
- tokenUsage.cacheCreationTokens ??
254
- tokenUsage.cacheTokens ??
255
- tokenUsage.cachedTokens ??
256
- 0,
257
- ) || 0;
258
-
259
- // If we only have total used tokens, treat them as input for display/estimation.
260
- const inputTokens =
261
- inputTokensRaw > 0 || outputTokens > 0 || cacheTokens > 0 ? inputTokensRaw + cacheTokens : used;
262
-
263
- // Rough default rates by provider (USD / 1M tokens).
264
- const pricingByProvider = {
265
- claude: { input: 3, output: 15 },
266
- cursor: { input: 3, output: 15 },
267
- codex: { input: 1.5, output: 6 },
268
- };
269
- const rates = pricingByProvider[provider] || pricingByProvider.claude;
270
-
271
- const inputCost = (inputTokens / 1_000_000) * rates.input;
272
- const outputCost = (outputTokens / 1_000_000) * rates.output;
273
- const totalCost = inputCost + outputCost;
274
-
275
- return {
276
- type: 'builtin',
277
- action: 'cost',
278
- data: {
279
- tokenUsage: {
280
- used,
281
- total,
282
- percentage,
283
- },
284
- cost: {
285
- input: inputCost.toFixed(4),
286
- output: outputCost.toFixed(4),
287
- total: totalCost.toFixed(4),
288
- },
289
- model,
290
- },
291
- };
292
- },
293
-
294
- '/status': async (args, context) => {
295
- // Read version from package.json
296
- const packageJsonPath = path.join(APP_ROOT, 'package.json');
297
- let version = 'unknown';
298
- let packageName = '@pixelbyte-software/pixcode';
299
-
300
- try {
301
- const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
302
- version = packageJson.version;
303
- packageName = packageJson.name;
304
- } catch (err) {
305
- console.error('Error reading package.json:', err);
306
- }
307
-
308
- const uptime = process.uptime();
309
- const uptimeMinutes = Math.floor(uptime / 60);
310
- const uptimeHours = Math.floor(uptimeMinutes / 60);
311
- const uptimeFormatted = uptimeHours > 0
312
- ? `${uptimeHours}h ${uptimeMinutes % 60}m`
313
- : `${uptimeMinutes}m`;
314
-
315
- return {
316
- type: 'builtin',
317
- action: 'status',
318
- data: {
319
- version,
320
- packageName,
321
- uptime: uptimeFormatted,
322
- uptimeSeconds: Math.floor(uptime),
323
- model: context?.model || 'claude-sonnet-4.5',
324
- provider: context?.provider || 'claude',
325
- nodeVersion: process.version,
326
- platform: process.platform
327
- }
328
- };
329
- },
330
-
331
- '/memory': async (args, context) => {
332
- const projectPath = context?.projectPath;
333
-
334
- if (!projectPath) {
335
- return {
336
- type: 'builtin',
337
- action: 'memory',
338
- data: {
339
- error: 'No project selected',
340
- message: 'Please select a project to access its CLAUDE.md file'
341
- }
342
- };
343
- }
344
-
345
- const claudeMdPath = path.join(projectPath, 'CLAUDE.md');
346
-
347
- // Check if CLAUDE.md exists
348
- let exists = false;
349
- try {
350
- await fs.access(claudeMdPath);
351
- exists = true;
352
- } catch (err) {
353
- // File doesn't exist
354
- }
355
-
356
- return {
357
- type: 'builtin',
358
- action: 'memory',
359
- data: {
360
- path: claudeMdPath,
361
- exists,
362
- message: exists
363
- ? `Opening CLAUDE.md at ${claudeMdPath}`
364
- : `CLAUDE.md not found at ${claudeMdPath}. Create it to store project-specific instructions.`
365
- }
366
- };
367
- },
368
-
369
- '/config': async (args, context) => {
370
- return {
371
- type: 'builtin',
372
- action: 'config',
373
- data: {
374
- message: 'Opening settings...'
375
- }
376
- };
377
- },
378
-
379
- '/rewind': async (args, context) => {
380
- const steps = args[0] ? parseInt(args[0]) : 1;
381
-
382
- if (isNaN(steps) || steps < 1) {
383
- return {
384
- type: 'builtin',
385
- action: 'rewind',
386
- data: {
387
- error: 'Invalid steps parameter',
388
- message: 'Usage: /rewind [number] - Rewind conversation by N steps (default: 1)'
389
- }
390
- };
391
- }
392
-
393
- return {
394
- type: 'builtin',
395
- action: 'rewind',
396
- data: {
397
- steps,
398
- message: `Rewinding conversation by ${steps} step${steps > 1 ? 's' : ''}...`
399
- }
400
- };
401
- }
402
- };
403
-
404
- /**
405
- * POST /api/commands/list
406
- * List all available commands from project and user directories
407
- */
408
- router.post('/list', async (req, res) => {
409
- try {
410
- const { projectPath } = req.body;
411
- const allCommands = [...builtInCommands];
412
-
413
- // Scan project-level commands (.claude/commands/)
414
- if (projectPath) {
415
- const projectCommandsDir = path.join(projectPath, '.claude', 'commands');
416
- const projectCommands = await scanCommandsDirectory(
417
- projectCommandsDir,
418
- projectCommandsDir,
419
- 'project'
420
- );
421
- allCommands.push(...projectCommands);
422
- }
423
-
424
- // Scan user-level commands (~/.claude/commands/)
425
- const homeDir = os.homedir();
426
- const userCommandsDir = path.join(homeDir, '.claude', 'commands');
427
- const userCommands = await scanCommandsDirectory(
428
- userCommandsDir,
429
- userCommandsDir,
430
- 'user'
431
- );
432
- allCommands.push(...userCommands);
433
-
434
- // Separate built-in and custom commands
435
- const customCommands = allCommands.filter(cmd => cmd.namespace !== 'builtin');
436
-
437
- // Sort commands alphabetically by name
438
- customCommands.sort((a, b) => a.name.localeCompare(b.name));
439
-
440
- res.json({
441
- builtIn: builtInCommands,
442
- custom: customCommands,
443
- count: allCommands.length
444
- });
445
- } catch (error) {
446
- console.error('Error listing commands:', error);
447
- res.status(500).json({
448
- error: 'Failed to list commands',
449
- message: error.message
450
- });
451
- }
452
- });
453
-
454
- /**
455
- * POST /api/commands/execute
456
- * Execute a command with argument replacement
457
- * This endpoint prepares the command content but doesn't execute bash commands yet
458
- * (that will be handled in the command parser utility)
459
- */
460
- router.post('/execute', async (req, res) => {
461
- try {
462
- const { commandName, commandPath, args = [], context = {} } = req.body;
463
-
464
- if (!commandName) {
465
- return res.status(400).json({
466
- error: 'Command name is required'
467
- });
468
- }
469
-
470
- // Handle built-in commands
471
- const handler = builtInHandlers[commandName];
472
- if (handler) {
473
- try {
474
- const result = await handler(args, context);
475
- return res.json({
476
- ...result,
477
- command: commandName
478
- });
479
- } catch (error) {
480
- console.error(`Error executing built-in command ${commandName}:`, error);
481
- return res.status(500).json({
482
- error: 'Command execution failed',
483
- message: error.message,
484
- command: commandName
485
- });
486
- }
487
- }
488
-
489
- // Handle custom commands
490
- if (!commandPath) {
491
- return res.status(400).json({
492
- error: 'Command path is required for custom commands'
493
- });
494
- }
495
-
496
- // Load command content
497
- // Security: validate commandPath is within allowed directories
498
- {
499
- const resolvedPath = path.resolve(commandPath);
500
- const userBase = path.resolve(path.join(os.homedir(), '.claude', 'commands'));
501
- const projectBase = context?.projectPath
502
- ? path.resolve(path.join(context.projectPath, '.claude', 'commands'))
503
- : null;
504
- const isUnder = (base) => {
505
- const rel = path.relative(base, resolvedPath);
506
- return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
507
- };
508
- if (!(isUnder(userBase) || (projectBase && isUnder(projectBase)))) {
509
- return res.status(403).json({
510
- error: 'Access denied',
511
- message: 'Command must be in .claude/commands directory'
512
- });
513
- }
514
- }
515
- const content = await fs.readFile(commandPath, 'utf8');
516
- const { data: metadata, content: commandContent } = parseFrontmatter(content);
517
- // Basic argument replacement (will be enhanced in command parser utility)
518
- let processedContent = commandContent;
519
-
520
- // Replace $ARGUMENTS with all arguments joined
521
- const argsString = args.join(' ');
522
- processedContent = processedContent.replace(/\$ARGUMENTS/g, argsString);
523
-
524
- // Replace $1, $2, etc. with positional arguments
525
- args.forEach((arg, index) => {
526
- const placeholder = `$${index + 1}`;
527
- processedContent = processedContent.replace(new RegExp(`\\${placeholder}\\b`, 'g'), arg);
528
- });
529
-
530
- res.json({
531
- type: 'custom',
532
- command: commandName,
533
- content: processedContent,
534
- metadata,
535
- hasFileIncludes: processedContent.includes('@'),
536
- hasBashCommands: processedContent.includes('!')
537
- });
538
- } catch (error) {
539
- if (error.code === 'ENOENT') {
540
- return res.status(404).json({
541
- error: 'Command not found',
542
- message: `Command file not found: ${req.body.commandPath}`
543
- });
544
- }
545
-
546
- console.error('Error executing command:', error);
547
- res.status(500).json({
548
- error: 'Failed to execute command',
549
- message: error.message
550
- });
551
- }
552
- });
553
-
554
- export default router;
1
+ import express from 'express';
2
+ import { promises as fs } from 'fs';
3
+ import path from 'path';
4
+ import os from 'os';
5
+ import { CLAUDE_MODELS, CURSOR_MODELS, CODEX_MODELS } from '../../shared/modelConstants.js';
6
+ import { parseFrontmatter } from '../utils/frontmatter.js';
7
+ import { findAppRoot, getModuleDir } from '../utils/runtime-paths.js';
8
+
9
+ const __dirname = getModuleDir(import.meta.url);
10
+ // This route reads the top-level package.json for the status command, so it needs the real
11
+ // app root even after compilation moves the route file under dist-server/server/routes.
12
+ const APP_ROOT = findAppRoot(__dirname);
13
+
14
+ const router = express.Router();
15
+
16
+ /**
17
+ * Recursively scan directory for command files (.md)
18
+ * @param {string} dir - Directory to scan
19
+ * @param {string} baseDir - Base directory for relative paths
20
+ * @param {string} namespace - Namespace for commands (e.g., 'project', 'user')
21
+ * @returns {Promise<Array>} Array of command objects
22
+ */
23
+ async function scanCommandsDirectory(dir, baseDir, namespace) {
24
+ const commands = [];
25
+
26
+ try {
27
+ // Check if directory exists
28
+ await fs.access(dir);
29
+
30
+ const entries = await fs.readdir(dir, { withFileTypes: true });
31
+
32
+ for (const entry of entries) {
33
+ const fullPath = path.join(dir, entry.name);
34
+
35
+ if (entry.isDirectory()) {
36
+ // Recursively scan subdirectories
37
+ const subCommands = await scanCommandsDirectory(fullPath, baseDir, namespace);
38
+ commands.push(...subCommands);
39
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
40
+ // Parse markdown file for metadata
41
+ try {
42
+ const content = await fs.readFile(fullPath, 'utf8');
43
+ const { data: frontmatter, content: commandContent } = parseFrontmatter(content);
44
+
45
+ // Calculate relative path from baseDir for command name
46
+ const relativePath = path.relative(baseDir, fullPath);
47
+ // Remove .md extension and convert to command name
48
+ const commandName = '/' + relativePath.replace(/\.md$/, '').replace(/\\/g, '/');
49
+
50
+ // Extract description from frontmatter or first line of content
51
+ let description = frontmatter.description || '';
52
+ if (!description) {
53
+ const firstLine = commandContent.trim().split('\n')[0];
54
+ description = firstLine.replace(/^#+\s*/, '').trim();
55
+ }
56
+
57
+ commands.push({
58
+ name: commandName,
59
+ path: fullPath,
60
+ relativePath,
61
+ description,
62
+ namespace,
63
+ metadata: frontmatter
64
+ });
65
+ } catch (err) {
66
+ console.error(`Error parsing command file ${fullPath}:`, err.message);
67
+ }
68
+ }
69
+ }
70
+ } catch (err) {
71
+ // Directory doesn't exist or can't be accessed - this is okay
72
+ if (err.code !== 'ENOENT' && err.code !== 'EACCES') {
73
+ console.error(`Error scanning directory ${dir}:`, err.message);
74
+ }
75
+ }
76
+
77
+ return commands;
78
+ }
79
+
80
+ /**
81
+ * Built-in commands that are always available
82
+ */
83
+ const builtInCommands = [
84
+ {
85
+ name: '/help',
86
+ description: 'Show help documentation for Claude Code',
87
+ namespace: 'builtin',
88
+ metadata: { type: 'builtin' }
89
+ },
90
+ {
91
+ name: '/clear',
92
+ description: 'Clear the conversation history',
93
+ namespace: 'builtin',
94
+ metadata: { type: 'builtin' }
95
+ },
96
+ {
97
+ name: '/model',
98
+ description: 'Switch or view the current AI model',
99
+ namespace: 'builtin',
100
+ metadata: { type: 'builtin' }
101
+ },
102
+ {
103
+ name: '/cost',
104
+ description: 'Display token usage and cost information',
105
+ namespace: 'builtin',
106
+ metadata: { type: 'builtin' }
107
+ },
108
+ {
109
+ name: '/memory',
110
+ description: 'Open CLAUDE.md memory file for editing',
111
+ namespace: 'builtin',
112
+ metadata: { type: 'builtin' }
113
+ },
114
+ {
115
+ name: '/config',
116
+ description: 'Open settings and configuration',
117
+ namespace: 'builtin',
118
+ metadata: { type: 'builtin' }
119
+ },
120
+ {
121
+ name: '/status',
122
+ description: 'Show system status and version information',
123
+ namespace: 'builtin',
124
+ metadata: { type: 'builtin' }
125
+ },
126
+ {
127
+ name: '/rewind',
128
+ description: 'Rewind the conversation to a previous state',
129
+ namespace: 'builtin',
130
+ metadata: { type: 'builtin' }
131
+ }
132
+ ];
133
+
134
+ /**
135
+ * Built-in command handlers
136
+ * Each handler returns { type: 'builtin', action: string, data: any }
137
+ */
138
+ const builtInHandlers = {
139
+ '/help': async (args, context) => {
140
+ const helpText = `# Claude Code Commands
141
+
142
+ ## Built-in Commands
143
+
144
+ ${builtInCommands.map(cmd => `### ${cmd.name}
145
+ ${cmd.description}
146
+ `).join('\n')}
147
+
148
+ ## Custom Commands
149
+
150
+ Custom commands can be created in:
151
+ - Project: \`.claude/commands/\` (project-specific)
152
+ - User: \`~/.claude/commands/\` (available in all projects)
153
+
154
+ ### Command Syntax
155
+
156
+ - **Arguments**: Use \`$ARGUMENTS\` for all args or \`$1\`, \`$2\`, etc. for positional
157
+ - **File Includes**: Use \`@filename\` to include file contents
158
+ - **Bash Commands**: Use \`!command\` to execute bash commands
159
+
160
+ ### Examples
161
+
162
+ \`\`\`markdown
163
+ /mycommand arg1 arg2
164
+ \`\`\`
165
+ `;
166
+
167
+ return {
168
+ type: 'builtin',
169
+ action: 'help',
170
+ data: {
171
+ content: helpText,
172
+ format: 'markdown'
173
+ }
174
+ };
175
+ },
176
+
177
+ '/clear': async (args, context) => {
178
+ return {
179
+ type: 'builtin',
180
+ action: 'clear',
181
+ data: {
182
+ message: 'Conversation history cleared'
183
+ }
184
+ };
185
+ },
186
+
187
+ '/model': async (args, context) => {
188
+ // Read available models from centralized constants
189
+ const availableModels = {
190
+ claude: CLAUDE_MODELS.OPTIONS.map(o => o.value),
191
+ cursor: CURSOR_MODELS.OPTIONS.map(o => o.value),
192
+ codex: CODEX_MODELS.OPTIONS.map(o => o.value)
193
+ };
194
+
195
+ const currentProvider = context?.provider || 'claude';
196
+ const currentModel = context?.model || CLAUDE_MODELS.DEFAULT;
197
+
198
+ return {
199
+ type: 'builtin',
200
+ action: 'model',
201
+ data: {
202
+ current: {
203
+ provider: currentProvider,
204
+ model: currentModel
205
+ },
206
+ available: availableModels,
207
+ message: args.length > 0
208
+ ? `Switching to model: ${args[0]}`
209
+ : `Current model: ${currentModel}`
210
+ }
211
+ };
212
+ },
213
+
214
+ '/cost': async (args, context) => {
215
+ const tokenUsage = context?.tokenUsage || {};
216
+ const provider = context?.provider || 'claude';
217
+ const model =
218
+ context?.model ||
219
+ (provider === 'cursor'
220
+ ? CURSOR_MODELS.DEFAULT
221
+ : provider === 'codex'
222
+ ? CODEX_MODELS.DEFAULT
223
+ : CLAUDE_MODELS.DEFAULT);
224
+
225
+ const used = Number(tokenUsage.used ?? tokenUsage.totalUsed ?? tokenUsage.total_tokens ?? 0) || 0;
226
+ const total =
227
+ Number(
228
+ tokenUsage.total ??
229
+ tokenUsage.contextWindow ??
230
+ parseInt(process.env.CONTEXT_WINDOW || '160000', 10),
231
+ ) || 160000;
232
+ const percentage = total > 0 ? Number(((used / total) * 100).toFixed(1)) : 0;
233
+
234
+ const inputTokensRaw =
235
+ Number(
236
+ tokenUsage.inputTokens ??
237
+ tokenUsage.input ??
238
+ tokenUsage.cumulativeInputTokens ??
239
+ tokenUsage.promptTokens ??
240
+ 0,
241
+ ) || 0;
242
+ const outputTokens =
243
+ Number(
244
+ tokenUsage.outputTokens ??
245
+ tokenUsage.output ??
246
+ tokenUsage.cumulativeOutputTokens ??
247
+ tokenUsage.completionTokens ??
248
+ 0,
249
+ ) || 0;
250
+ const cacheTokens =
251
+ Number(
252
+ tokenUsage.cacheReadTokens ??
253
+ tokenUsage.cacheCreationTokens ??
254
+ tokenUsage.cacheTokens ??
255
+ tokenUsage.cachedTokens ??
256
+ 0,
257
+ ) || 0;
258
+
259
+ // If we only have total used tokens, treat them as input for display/estimation.
260
+ const inputTokens =
261
+ inputTokensRaw > 0 || outputTokens > 0 || cacheTokens > 0 ? inputTokensRaw + cacheTokens : used;
262
+
263
+ // Rough default rates by provider (USD / 1M tokens).
264
+ const pricingByProvider = {
265
+ claude: { input: 3, output: 15 },
266
+ cursor: { input: 3, output: 15 },
267
+ codex: { input: 1.5, output: 6 },
268
+ };
269
+ const rates = pricingByProvider[provider] || pricingByProvider.claude;
270
+
271
+ const inputCost = (inputTokens / 1_000_000) * rates.input;
272
+ const outputCost = (outputTokens / 1_000_000) * rates.output;
273
+ const totalCost = inputCost + outputCost;
274
+
275
+ return {
276
+ type: 'builtin',
277
+ action: 'cost',
278
+ data: {
279
+ tokenUsage: {
280
+ used,
281
+ total,
282
+ percentage,
283
+ },
284
+ cost: {
285
+ input: inputCost.toFixed(4),
286
+ output: outputCost.toFixed(4),
287
+ total: totalCost.toFixed(4),
288
+ },
289
+ model,
290
+ },
291
+ };
292
+ },
293
+
294
+ '/status': async (args, context) => {
295
+ // Read version from package.json
296
+ const packageJsonPath = path.join(APP_ROOT, 'package.json');
297
+ let version = 'unknown';
298
+ let packageName = '@pixelbyte-software/pixcode';
299
+
300
+ try {
301
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
302
+ version = packageJson.version;
303
+ packageName = packageJson.name;
304
+ } catch (err) {
305
+ console.error('Error reading package.json:', err);
306
+ }
307
+
308
+ const uptime = process.uptime();
309
+ const uptimeMinutes = Math.floor(uptime / 60);
310
+ const uptimeHours = Math.floor(uptimeMinutes / 60);
311
+ const uptimeFormatted = uptimeHours > 0
312
+ ? `${uptimeHours}h ${uptimeMinutes % 60}m`
313
+ : `${uptimeMinutes}m`;
314
+
315
+ return {
316
+ type: 'builtin',
317
+ action: 'status',
318
+ data: {
319
+ version,
320
+ packageName,
321
+ uptime: uptimeFormatted,
322
+ uptimeSeconds: Math.floor(uptime),
323
+ model: context?.model || 'claude-sonnet-4.5',
324
+ provider: context?.provider || 'claude',
325
+ nodeVersion: process.version,
326
+ platform: process.platform
327
+ }
328
+ };
329
+ },
330
+
331
+ '/memory': async (args, context) => {
332
+ const projectPath = context?.projectPath;
333
+
334
+ if (!projectPath) {
335
+ return {
336
+ type: 'builtin',
337
+ action: 'memory',
338
+ data: {
339
+ error: 'No project selected',
340
+ message: 'Please select a project to access its CLAUDE.md file'
341
+ }
342
+ };
343
+ }
344
+
345
+ const claudeMdPath = path.join(projectPath, 'CLAUDE.md');
346
+
347
+ // Check if CLAUDE.md exists
348
+ let exists = false;
349
+ try {
350
+ await fs.access(claudeMdPath);
351
+ exists = true;
352
+ } catch (err) {
353
+ // File doesn't exist
354
+ }
355
+
356
+ return {
357
+ type: 'builtin',
358
+ action: 'memory',
359
+ data: {
360
+ path: claudeMdPath,
361
+ exists,
362
+ message: exists
363
+ ? `Opening CLAUDE.md at ${claudeMdPath}`
364
+ : `CLAUDE.md not found at ${claudeMdPath}. Create it to store project-specific instructions.`
365
+ }
366
+ };
367
+ },
368
+
369
+ '/config': async (args, context) => {
370
+ return {
371
+ type: 'builtin',
372
+ action: 'config',
373
+ data: {
374
+ message: 'Opening settings...'
375
+ }
376
+ };
377
+ },
378
+
379
+ '/rewind': async (args, context) => {
380
+ const steps = args[0] ? parseInt(args[0]) : 1;
381
+
382
+ if (isNaN(steps) || steps < 1) {
383
+ return {
384
+ type: 'builtin',
385
+ action: 'rewind',
386
+ data: {
387
+ error: 'Invalid steps parameter',
388
+ message: 'Usage: /rewind [number] - Rewind conversation by N steps (default: 1)'
389
+ }
390
+ };
391
+ }
392
+
393
+ return {
394
+ type: 'builtin',
395
+ action: 'rewind',
396
+ data: {
397
+ steps,
398
+ message: `Rewinding conversation by ${steps} step${steps > 1 ? 's' : ''}...`
399
+ }
400
+ };
401
+ }
402
+ };
403
+
404
+ /**
405
+ * POST /api/commands/list
406
+ * List all available commands from project and user directories
407
+ */
408
+ router.post('/list', async (req, res) => {
409
+ try {
410
+ const { projectPath } = req.body;
411
+ const allCommands = [...builtInCommands];
412
+
413
+ // Scan project-level commands (.claude/commands/)
414
+ if (projectPath) {
415
+ const projectCommandsDir = path.join(projectPath, '.claude', 'commands');
416
+ const projectCommands = await scanCommandsDirectory(
417
+ projectCommandsDir,
418
+ projectCommandsDir,
419
+ 'project'
420
+ );
421
+ allCommands.push(...projectCommands);
422
+ }
423
+
424
+ // Scan user-level commands (~/.claude/commands/)
425
+ const homeDir = os.homedir();
426
+ const userCommandsDir = path.join(homeDir, '.claude', 'commands');
427
+ const userCommands = await scanCommandsDirectory(
428
+ userCommandsDir,
429
+ userCommandsDir,
430
+ 'user'
431
+ );
432
+ allCommands.push(...userCommands);
433
+
434
+ // Separate built-in and custom commands
435
+ const customCommands = allCommands.filter(cmd => cmd.namespace !== 'builtin');
436
+
437
+ // Sort commands alphabetically by name
438
+ customCommands.sort((a, b) => a.name.localeCompare(b.name));
439
+
440
+ res.json({
441
+ builtIn: builtInCommands,
442
+ custom: customCommands,
443
+ count: allCommands.length
444
+ });
445
+ } catch (error) {
446
+ console.error('Error listing commands:', error);
447
+ res.status(500).json({
448
+ error: 'Failed to list commands',
449
+ message: error.message
450
+ });
451
+ }
452
+ });
453
+
454
+ /**
455
+ * POST /api/commands/execute
456
+ * Execute a command with argument replacement
457
+ * This endpoint prepares the command content but doesn't execute bash commands yet
458
+ * (that will be handled in the command parser utility)
459
+ */
460
+ router.post('/execute', async (req, res) => {
461
+ try {
462
+ const { commandName, commandPath, args = [], context = {} } = req.body;
463
+
464
+ if (!commandName) {
465
+ return res.status(400).json({
466
+ error: 'Command name is required'
467
+ });
468
+ }
469
+
470
+ // Handle built-in commands
471
+ const handler = builtInHandlers[commandName];
472
+ if (handler) {
473
+ try {
474
+ const result = await handler(args, context);
475
+ return res.json({
476
+ ...result,
477
+ command: commandName
478
+ });
479
+ } catch (error) {
480
+ console.error(`Error executing built-in command ${commandName}:`, error);
481
+ return res.status(500).json({
482
+ error: 'Command execution failed',
483
+ message: error.message,
484
+ command: commandName
485
+ });
486
+ }
487
+ }
488
+
489
+ // Handle custom commands
490
+ if (!commandPath) {
491
+ return res.status(400).json({
492
+ error: 'Command path is required for custom commands'
493
+ });
494
+ }
495
+
496
+ // Load command content
497
+ // Security: validate commandPath is within allowed directories
498
+ {
499
+ const resolvedPath = path.resolve(commandPath);
500
+ const userBase = path.resolve(path.join(os.homedir(), '.claude', 'commands'));
501
+ const projectBase = context?.projectPath
502
+ ? path.resolve(path.join(context.projectPath, '.claude', 'commands'))
503
+ : null;
504
+ const isUnder = (base) => {
505
+ const rel = path.relative(base, resolvedPath);
506
+ return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
507
+ };
508
+ if (!(isUnder(userBase) || (projectBase && isUnder(projectBase)))) {
509
+ return res.status(403).json({
510
+ error: 'Access denied',
511
+ message: 'Command must be in .claude/commands directory'
512
+ });
513
+ }
514
+ }
515
+ const content = await fs.readFile(commandPath, 'utf8');
516
+ const { data: metadata, content: commandContent } = parseFrontmatter(content);
517
+ // Basic argument replacement (will be enhanced in command parser utility)
518
+ let processedContent = commandContent;
519
+
520
+ // Replace $ARGUMENTS with all arguments joined
521
+ const argsString = args.join(' ');
522
+ processedContent = processedContent.replace(/\$ARGUMENTS/g, argsString);
523
+
524
+ // Replace $1, $2, etc. with positional arguments
525
+ args.forEach((arg, index) => {
526
+ const placeholder = `$${index + 1}`;
527
+ processedContent = processedContent.replace(new RegExp(`\\${placeholder}\\b`, 'g'), arg);
528
+ });
529
+
530
+ res.json({
531
+ type: 'custom',
532
+ command: commandName,
533
+ content: processedContent,
534
+ metadata,
535
+ hasFileIncludes: processedContent.includes('@'),
536
+ hasBashCommands: processedContent.includes('!')
537
+ });
538
+ } catch (error) {
539
+ if (error.code === 'ENOENT') {
540
+ return res.status(404).json({
541
+ error: 'Command not found',
542
+ message: `Command file not found: ${req.body.commandPath}`
543
+ });
544
+ }
545
+
546
+ console.error('Error executing command:', error);
547
+ res.status(500).json({
548
+ error: 'Failed to execute command',
549
+ message: error.message
550
+ });
551
+ }
552
+ });
553
+
554
+ export default router;