@axhub/genie 0.2.9 → 0.2.11

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 (96) hide show
  1. package/dist/api-docs.html +2 -2
  2. package/dist/assets/App-VH1wNUHs.js +259 -0
  3. package/dist/assets/{ReviewApp-C9K--AQE.js → ReviewApp-D_9EN4TM.js} +1 -1
  4. package/dist/assets/{_basePickBy-DR_8uFCo.js → _basePickBy-BDnj7-0Z.js} +1 -1
  5. package/dist/assets/{_baseUniq-D0njlQ_7.js → _baseUniq-Bl0JKOyl.js} +1 -1
  6. package/dist/assets/{arc-CKlr_Rec.js → arc-DY-4Kev3.js} +1 -1
  7. package/dist/assets/{architectureDiagram-2XIMDMQ5-BmO_uLUH.js → architectureDiagram-2XIMDMQ5-qw7crNVd.js} +1 -1
  8. package/dist/assets/{blockDiagram-WCTKOSBZ-DhAeO-56.js → blockDiagram-WCTKOSBZ-B9xg7ep3.js} +1 -1
  9. package/dist/assets/{c4Diagram-IC4MRINW-C67kFoXx.js → c4Diagram-IC4MRINW-H9xp3ytb.js} +1 -1
  10. package/dist/assets/channel-CyNUnRfc.js +1 -0
  11. package/dist/assets/{chunk-4BX2VUAB-mLLagvJi.js → chunk-4BX2VUAB-B3EVDUxI.js} +1 -1
  12. package/dist/assets/{chunk-55IACEB6-Lx-hOjlM.js → chunk-55IACEB6-CGv945ef.js} +1 -1
  13. package/dist/assets/{chunk-FMBD7UC4-Bt-XmVUV.js → chunk-FMBD7UC4-uAT4CKWM.js} +1 -1
  14. package/dist/assets/{chunk-JSJVCQXG-Cya6gaDV.js → chunk-JSJVCQXG-Cbvlpkf7.js} +1 -1
  15. package/dist/assets/{chunk-KX2RTZJC-Bd7Ig6tF.js → chunk-KX2RTZJC-CcqIuGat.js} +1 -1
  16. package/dist/assets/{chunk-NQ4KR5QH-5UAE0Vg-.js → chunk-NQ4KR5QH-CgrcsRuX.js} +1 -1
  17. package/dist/assets/{chunk-QZHKN3VN-BAxZ8m7w.js → chunk-QZHKN3VN-Cx0APOoV.js} +1 -1
  18. package/dist/assets/{chunk-WL4C6EOR-DjDPvUUP.js → chunk-WL4C6EOR-BbZirvBk.js} +1 -1
  19. package/dist/assets/classDiagram-VBA2DB6C-DxBtyz2A.js +1 -0
  20. package/dist/assets/classDiagram-v2-RAHNMMFH-DxBtyz2A.js +1 -0
  21. package/dist/assets/clone-C341l3d0.js +1 -0
  22. package/dist/assets/{cose-bilkent-S5V4N54A-D-60XrkJ.js → cose-bilkent-S5V4N54A-CrvmGFLD.js} +1 -1
  23. package/dist/assets/{dagre-KLK3FWXG-bqu3ZS4K.js → dagre-KLK3FWXG-C-W6VPjS.js} +1 -1
  24. package/dist/assets/{diagram-E7M64L7V-BueeqoYm.js → diagram-E7M64L7V-IP2q3bL0.js} +1 -1
  25. package/dist/assets/{diagram-IFDJBPK2-D4fDv2E7.js → diagram-IFDJBPK2-CQaL-XyV.js} +1 -1
  26. package/dist/assets/{diagram-P4PSJMXO-WqipY3fN.js → diagram-P4PSJMXO-BxBLThfv.js} +1 -1
  27. package/dist/assets/{erDiagram-INFDFZHY-D0oVnO-x.js → erDiagram-INFDFZHY-Dyl7bJTt.js} +1 -1
  28. package/dist/assets/{flowDiagram-PKNHOUZH-DzbGyxrr.js → flowDiagram-PKNHOUZH-B7NFMgFK.js} +1 -1
  29. package/dist/assets/{ganttDiagram-A5KZAMGK-BwhbbgCP.js → ganttDiagram-A5KZAMGK-hReWSDu2.js} +1 -1
  30. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-DZgAh_KM.js → gitGraphDiagram-K3NZZRJ6-gVgcr0ST.js} +1 -1
  31. package/dist/assets/{graph-DzKos-N0.js → graph-DNDiJhTn.js} +1 -1
  32. package/dist/assets/{highlighted-body-TPN3WLV5-CKDMgz3X.js → highlighted-body-TPN3WLV5-DclLmTou.js} +1 -1
  33. package/dist/assets/index-DBkz_W_P.css +1 -0
  34. package/dist/assets/index-DdRyoXKh.js +2 -0
  35. package/dist/assets/{infoDiagram-LFFYTUFH-BFicZbTf.js → infoDiagram-LFFYTUFH-CqQOOzDA.js} +1 -1
  36. package/dist/assets/{ishikawaDiagram-PHBUUO56-CtihxDxl.js → ishikawaDiagram-PHBUUO56-CZ0iLiHg.js} +1 -1
  37. package/dist/assets/{journeyDiagram-4ABVD52K-Du00J8_d.js → journeyDiagram-4ABVD52K-DdfYKfNh.js} +1 -1
  38. package/dist/assets/{kanban-definition-K7BYSVSG-BJi9S0iQ.js → kanban-definition-K7BYSVSG-C5Vf32u6.js} +1 -1
  39. package/dist/assets/{layout-B80Sityu.js → layout-rvTEu2KS.js} +1 -1
  40. package/dist/assets/{linear-sRQLOf5H.js → linear-CD9SiYze.js} +1 -1
  41. package/dist/assets/{mermaid-O7DHMXV3-CBuVs4eJ.js → mermaid-O7DHMXV3-OZ8qWWwa.js} +167 -157
  42. package/dist/assets/{mindmap-definition-YRQLILUH-C5IL_xi-.js → mindmap-definition-YRQLILUH-CQxrLNVc.js} +1 -1
  43. package/dist/assets/{pieDiagram-SKSYHLDU-CeTwlJ8z.js → pieDiagram-SKSYHLDU-XgAUByWg.js} +1 -1
  44. package/dist/assets/{quadrantDiagram-337W2JSQ-COfUcLWt.js → quadrantDiagram-337W2JSQ-CH16ls7G.js} +1 -1
  45. package/dist/assets/{requirementDiagram-Z7DCOOCP-DSb-CJ5B.js → requirementDiagram-Z7DCOOCP-B_kQO06L.js} +1 -1
  46. package/dist/assets/{sankeyDiagram-WA2Y5GQK-8jtuVb45.js → sankeyDiagram-WA2Y5GQK-ofe78CyS.js} +1 -1
  47. package/dist/assets/{sequenceDiagram-2WXFIKYE-C2VpkMwA.js → sequenceDiagram-2WXFIKYE-Ckbxwny6.js} +1 -1
  48. package/dist/assets/{stateDiagram-RAJIS63D-fmwMqxxc.js → stateDiagram-RAJIS63D-DNtzCk14.js} +1 -1
  49. package/dist/assets/stateDiagram-v2-FVOUBMTO-B3VPhiE1.js +1 -0
  50. package/dist/assets/{timeline-definition-YZTLITO2-Dx1hP5lg.js → timeline-definition-YZTLITO2-zT6CklKt.js} +1 -1
  51. package/dist/assets/{treemap-KZPCXAKY-CkLOdYCZ.js → treemap-KZPCXAKY-y0U2c3xG.js} +1 -1
  52. package/dist/assets/vendor-codemirror-CMHSJ_9p.js +9 -0
  53. package/dist/assets/{vennDiagram-LZ73GAT5-D6KWcnln.js → vennDiagram-LZ73GAT5-xKj3SjYG.js} +1 -1
  54. package/dist/assets/{xychartDiagram-JWTSCODW-6fh6qmzN.js → xychartDiagram-JWTSCODW-Da_qyEoX.js} +1 -1
  55. package/dist/index.html +3 -3
  56. package/package.json +6 -5
  57. package/server/acp-runtime/client.js +120 -14
  58. package/server/acp-runtime/index.js +54 -0
  59. package/server/acp-runtime/registry.js +2 -2
  60. package/server/acp-runtime/session-store.js +75 -1
  61. package/server/cli.js +32 -8
  62. package/server/database/db.js +20 -0
  63. package/server/external-agent/ws.js +477 -24
  64. package/server/index.js +89 -147
  65. package/server/lan-access/core.js +79 -0
  66. package/server/lan-access/state.js +102 -0
  67. package/server/middleware/auth.js +57 -14
  68. package/server/projects.js +442 -535
  69. package/server/routes/auth.js +24 -4
  70. package/server/routes/cli-auth.js +21 -25
  71. package/server/routes/codex.js +84 -298
  72. package/server/routes/commands.js +335 -407
  73. package/server/routes/lan-access.js +231 -0
  74. package/server/routes/projects.js +154 -158
  75. package/server/routes/session-core.js +13 -7
  76. package/server/routes/settings.js +113 -99
  77. package/server/session-core/eventStore.js +15 -2
  78. package/server/session-core/providerAdapters.js +28 -28
  79. package/server/session-core/sessionListMerge.js +47 -0
  80. package/shared/conversationEvents.js +96 -1
  81. package/shared/modelConstants.js +79 -99
  82. package/dist/assets/App-GBcTeeUS.js +0 -460
  83. package/dist/assets/channel-V3MBjKys.js +0 -1
  84. package/dist/assets/classDiagram-VBA2DB6C-C790yYiY.js +0 -1
  85. package/dist/assets/classDiagram-v2-RAHNMMFH-C790yYiY.js +0 -1
  86. package/dist/assets/clone-BbMGfZwt.js +0 -1
  87. package/dist/assets/index-DiQlHzGj.js +0 -2
  88. package/dist/assets/index-Drat2nB9.css +0 -1
  89. package/dist/assets/stateDiagram-v2-FVOUBMTO-9GGXVWrR.js +0 -1
  90. package/dist/assets/vendor-codemirror-BxPY6emf.js +0 -39
  91. package/server/routes/git.js +0 -1110
  92. package/server/routes/mcp-utils.js +0 -48
  93. package/server/routes/mcp.js +0 -536
  94. package/server/routes/taskmaster.js +0 -1963
  95. package/server/utils/mcp-detector.js +0 -198
  96. package/server/utils/taskmaster-websocket.js +0 -129
@@ -1,375 +1,362 @@
1
1
  import express from 'express';
2
2
  import { promises as fs } from 'fs';
3
+ import os from 'os';
3
4
  import path from 'path';
4
5
  import { fileURLToPath } from 'url';
5
- import os from 'os';
6
6
  import matter from 'gray-matter';
7
- import { CLAUDE_MODELS, CODEX_MODELS, GEMINI_MODELS, OPENCODE_MODELS } from '../../shared/modelConstants.js';
7
+ import {
8
+ CLAUDE_MODELS,
9
+ CODEX_MODELS,
10
+ GEMINI_MODELS,
11
+ OPENCODE_MODELS
12
+ } from '../../shared/modelConstants.js';
8
13
  import { discoverAllProviders } from '../session-core/providerDiscovery.js';
9
14
 
15
+ const router = express.Router();
10
16
  const __filename = fileURLToPath(import.meta.url);
11
17
  const __dirname = path.dirname(__filename);
18
+ const PACKAGE_JSON_PATH = path.join(__dirname, '..', '..', 'package.json');
19
+
20
+ const BUILTIN_COMMANDS = [];
21
+ const DISABLED_BUILTIN_COMMANDS = new Set([
22
+ '/help',
23
+ '/clear',
24
+ '/model',
25
+ '/memory',
26
+ '/config',
27
+ '/status',
28
+ '/rewind',
29
+ ]);
30
+
31
+ const FALLBACK_PROVIDER_MODELS = {
32
+ claude: CLAUDE_MODELS.OPTIONS.map((option) => option.value),
33
+ codex: CODEX_MODELS.OPTIONS.map((option) => option.value),
34
+ gemini: GEMINI_MODELS.OPTIONS.map((option) => option.value),
35
+ opencode: OPENCODE_MODELS.OPTIONS.map((option) => option.value)
36
+ };
12
37
 
13
- const router = express.Router();
38
+ function trimText(value) {
39
+ return typeof value === 'string' ? value.trim() : '';
40
+ }
14
41
 
15
- /**
16
- * Recursively scan directory for command files (.md)
17
- * @param {string} dir - Directory to scan
18
- * @param {string} baseDir - Base directory for relative paths
19
- * @param {string} namespace - Namespace for commands (e.g., 'project', 'user')
20
- * @returns {Promise<Array>} Array of command objects
21
- */
22
- async function scanCommandsDirectory(dir, baseDir, namespace) {
23
- const commands = [];
42
+ function commandRootsFor(projectPath) {
43
+ const roots = [
44
+ {
45
+ namespace: 'user',
46
+ rootPath: path.join(os.homedir(), '.claude', 'commands')
47
+ }
48
+ ];
24
49
 
25
- try {
26
- // Check if directory exists
27
- await fs.access(dir);
50
+ if (trimText(projectPath)) {
51
+ roots.unshift({
52
+ namespace: 'project',
53
+ rootPath: path.join(projectPath, '.claude', 'commands')
54
+ });
55
+ }
28
56
 
29
- const entries = await fs.readdir(dir, { withFileTypes: true });
57
+ return roots;
58
+ }
30
59
 
31
- for (const entry of entries) {
32
- const fullPath = path.join(dir, entry.name);
60
+ function normalizeCommandName(relativeFilePath) {
61
+ const normalized = relativeFilePath.replace(/\\/g, '/').replace(/\.md$/i, '');
62
+ return normalized.startsWith('/') ? normalized : `/${normalized}`;
63
+ }
33
64
 
34
- if (entry.isDirectory()) {
35
- // Recursively scan subdirectories
36
- const subCommands = await scanCommandsDirectory(fullPath, baseDir, namespace);
37
- commands.push(...subCommands);
38
- } else if (entry.isFile() && entry.name.endsWith('.md')) {
39
- // Parse markdown file for metadata
40
- try {
41
- const content = await fs.readFile(fullPath, 'utf8');
42
- const { data: frontmatter, content: commandContent } = matter(content);
43
-
44
- // Calculate relative path from baseDir for command name
45
- const relativePath = path.relative(baseDir, fullPath);
46
- // Remove .md extension and convert to command name
47
- const commandName = '/' + relativePath.replace(/\.md$/, '').replace(/\\/g, '/');
48
-
49
- // Extract description from frontmatter or first line of content
50
- let description = frontmatter.description || '';
51
- if (!description) {
52
- const firstLine = commandContent.trim().split('\n')[0];
53
- description = firstLine.replace(/^#+\s*/, '').trim();
54
- }
65
+ function inferDescription(frontmatter, markdownBody) {
66
+ const explicit = trimText(frontmatter?.description);
67
+ if (explicit) {
68
+ return explicit;
69
+ }
55
70
 
56
- commands.push({
57
- name: commandName,
58
- path: fullPath,
59
- relativePath,
60
- description,
61
- namespace,
62
- metadata: frontmatter
63
- });
64
- } catch (err) {
65
- console.error(`Error parsing command file ${fullPath}:`, err.message);
66
- }
71
+ const firstMeaningfulLine = markdownBody
72
+ .split('\n')
73
+ .map((line) => line.trim())
74
+ .find((line) => line && !line.startsWith('---'));
75
+
76
+ if (!firstMeaningfulLine) {
77
+ return 'Custom slash command';
78
+ }
79
+
80
+ return firstMeaningfulLine.replace(/^#+\s*/, '').trim() || 'Custom slash command';
81
+ }
82
+
83
+ async function walkCommandTree(rootPath, namespace) {
84
+ const collected = [];
85
+
86
+ async function visit(directory) {
87
+ let entries;
88
+ try {
89
+ entries = await fs.readdir(directory, { withFileTypes: true });
90
+ } catch (error) {
91
+ if (error.code === 'ENOENT' || error.code === 'EACCES') {
92
+ return;
67
93
  }
94
+ throw error;
68
95
  }
69
- } catch (err) {
70
- // Directory doesn't exist or can't be accessed - this is okay
71
- if (err.code !== 'ENOENT' && err.code !== 'EACCES') {
72
- console.error(`Error scanning directory ${dir}:`, err.message);
96
+
97
+ for (const entry of entries) {
98
+ const fullPath = path.join(directory, entry.name);
99
+ if (entry.isDirectory()) {
100
+ await visit(fullPath);
101
+ continue;
102
+ }
103
+
104
+ if (!entry.isFile() || !entry.name.toLowerCase().endsWith('.md')) {
105
+ continue;
106
+ }
107
+
108
+ try {
109
+ const rawDocument = await fs.readFile(fullPath, 'utf8');
110
+ const parsedDocument = matter(rawDocument);
111
+ const relativePath = path.relative(rootPath, fullPath);
112
+ collected.push({
113
+ name: normalizeCommandName(relativePath),
114
+ path: fullPath,
115
+ relativePath,
116
+ description: inferDescription(parsedDocument.data, parsedDocument.content),
117
+ namespace,
118
+ metadata: parsedDocument.data || {}
119
+ });
120
+ } catch (error) {
121
+ console.error(`Error reading slash command ${fullPath}:`, error.message);
122
+ }
73
123
  }
74
124
  }
75
125
 
76
- return commands;
126
+ await visit(rootPath);
127
+ return collected.sort((left, right) => left.name.localeCompare(right.name));
77
128
  }
78
129
 
79
- /**
80
- * Built-in commands that are always available
81
- */
82
- const builtInCommands = [
83
- {
84
- name: '/help',
85
- description: 'Show help documentation for Claude Code',
86
- namespace: 'builtin',
87
- metadata: { type: 'builtin' }
88
- },
89
- {
90
- name: '/clear',
91
- description: 'Clear the conversation history',
92
- namespace: 'builtin',
93
- metadata: { type: 'builtin' }
94
- },
95
- {
96
- name: '/model',
97
- description: 'Switch or view the current AI model',
98
- namespace: 'builtin',
99
- metadata: { type: 'builtin' }
100
- },
101
- {
102
- name: '/cost',
103
- description: 'Display token usage and cost information',
104
- namespace: 'builtin',
105
- metadata: { type: 'builtin' }
106
- },
107
- {
108
- name: '/memory',
109
- description: 'Open CLAUDE.md memory file for editing',
110
- namespace: 'builtin',
111
- metadata: { type: 'builtin' }
112
- },
113
- {
114
- name: '/config',
115
- description: 'Open settings and configuration',
116
- namespace: 'builtin',
117
- metadata: { type: 'builtin' }
118
- },
119
- {
120
- name: '/status',
121
- description: 'Show system status and version information',
122
- namespace: 'builtin',
123
- metadata: { type: 'builtin' }
124
- },
125
- {
126
- name: '/rewind',
127
- description: 'Rewind the conversation to a previous state',
130
+ function toBuiltinRecord(command) {
131
+ return {
132
+ ...command,
128
133
  namespace: 'builtin',
129
134
  metadata: { type: 'builtin' }
130
- }
131
- ];
135
+ };
136
+ }
132
137
 
133
- /**
134
- * Built-in command handlers
135
- * Each handler returns { type: 'builtin', action: string, data: any }
136
- */
137
- const builtInHandlers = {
138
- '/help': async (args, context) => {
139
- const helpText = `# Claude Code Commands
138
+ function isPathInside(basePath, targetPath) {
139
+ const relative = path.relative(basePath, targetPath);
140
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
141
+ }
140
142
 
141
- ## Built-in Commands
143
+ function resolveAllowedCommandPath(commandPath, projectPath) {
144
+ const resolvedPath = path.resolve(commandPath);
145
+ const roots = commandRootsFor(projectPath);
146
+ const matchedRoot = roots.find((entry) => isPathInside(path.resolve(entry.rootPath), resolvedPath));
142
147
 
143
- ${builtInCommands.map(cmd => `### ${cmd.name}
144
- ${cmd.description}
145
- `).join('\n')}
148
+ if (!matchedRoot) {
149
+ const error = new Error('Command must live in a .claude/commands directory');
150
+ error.statusCode = 403;
151
+ throw error;
152
+ }
146
153
 
147
- ## Custom Commands
154
+ return resolvedPath;
155
+ }
148
156
 
149
- Custom commands can be created in:
150
- - Project: \`.claude/commands/\` (project-specific)
151
- - User: \`~/.claude/commands/\` (available in all projects)
157
+ async function loadCommandDocument(commandPath, projectPath) {
158
+ const resolvedPath = resolveAllowedCommandPath(commandPath, projectPath);
159
+ const raw = await fs.readFile(resolvedPath, 'utf8');
160
+ const parsed = matter(raw);
152
161
 
153
- ### Command Syntax
162
+ return {
163
+ path: resolvedPath,
164
+ metadata: parsed.data || {},
165
+ content: parsed.content
166
+ };
167
+ }
154
168
 
155
- - **Arguments**: Use \`$ARGUMENTS\` for all args or \`$1\`, \`$2\`, etc. for positional
156
- - **File Includes**: Use \`@filename\` to include file contents
157
- - **Bash Commands**: Use \`!command\` to execute bash commands
169
+ function interpolateCommandBody(content, args) {
170
+ const joinedArgs = args.join(' ');
171
+ let nextContent = String(content || '').replace(/\$ARGUMENTS/g, joinedArgs);
158
172
 
159
- ### Examples
173
+ args.forEach((arg, index) => {
174
+ nextContent = nextContent.replace(new RegExp(`\\$${index + 1}\\b`, 'g'), arg);
175
+ });
160
176
 
161
- \`\`\`markdown
162
- /mycommand arg1 arg2
163
- \`\`\`
164
- `;
177
+ return nextContent;
178
+ }
165
179
 
180
+ async function getPackageMetadata() {
181
+ try {
182
+ const raw = await fs.readFile(PACKAGE_JSON_PATH, 'utf8');
183
+ const parsed = JSON.parse(raw);
166
184
  return {
167
- type: 'builtin',
168
- action: 'help',
169
- data: {
170
- content: helpText,
171
- format: 'markdown'
172
- }
185
+ version: parsed.version || 'unknown',
186
+ packageName: parsed.name || '@axhub/genie'
173
187
  };
174
- },
175
-
176
- '/clear': async (args, context) => {
188
+ } catch {
177
189
  return {
178
- type: 'builtin',
179
- action: 'clear',
180
- data: {
181
- message: 'Conversation history cleared'
182
- }
190
+ version: 'unknown',
191
+ packageName: '@axhub/genie'
183
192
  };
184
- },
185
-
186
- '/model': async (args, context) => {
187
- const providerDiscovery = await discoverAllProviders({ projectPath: context?.projectPath });
188
- const availableModels = providerDiscovery.reduce((acc, item) => {
189
- acc[item.provider] = Array.isArray(item.models) && item.models.length > 0
190
- ? item.models.map((entry) => entry.id)
191
- : (item.provider === 'claude'
192
- ? CLAUDE_MODELS.OPTIONS.map(o => o.value)
193
- : item.provider === 'codex'
194
- ? CODEX_MODELS.OPTIONS.map(o => o.value)
195
- : item.provider === 'gemini'
196
- ? GEMINI_MODELS.OPTIONS.map(o => o.value)
197
- : OPENCODE_MODELS.OPTIONS.map(o => o.value));
198
- return acc;
199
- }, {});
200
-
201
- const currentProvider = context?.provider || 'claude';
202
- const discoveredCurrentModel = providerDiscovery.find((item) => item.provider === currentProvider)?.currentModel || null;
203
- const currentModel = context?.model || discoveredCurrentModel || null;
204
- const opencodeDiscovery = providerDiscovery.find((item) => item.provider === 'opencode') || null;
193
+ }
194
+ }
205
195
 
206
- return {
207
- type: 'builtin',
208
- action: 'model',
209
- data: {
210
- current: {
211
- provider: currentProvider,
212
- model: currentModel
213
- },
214
- available: availableModels,
215
- opencodeDiscovery,
216
- message: args.length > 0
217
- ? `Switching to model: ${args[0]}`
218
- : (currentModel ? `Current model: ${currentModel}` : 'Current model is managed by the active provider configuration.')
219
- }
220
- };
221
- },
196
+ async function buildModelPayload(context) {
197
+ const projectPath = trimText(context?.projectPath);
198
+ const discovered = await discoverAllProviders({ projectPath: projectPath || null });
199
+ const available = {};
222
200
 
223
- '/status': async (args, context) => {
224
- // Read version from package.json
225
- const packageJsonPath = path.join(path.dirname(__dirname), '..', 'package.json');
226
- let version = 'unknown';
227
- let packageName = '@axhub/genie';
201
+ for (const provider of discovered) {
202
+ available[provider.provider] = Array.isArray(provider.models) && provider.models.length > 0
203
+ ? provider.models.map((entry) => entry.id)
204
+ : (FALLBACK_PROVIDER_MODELS[provider.provider] || []);
205
+ }
228
206
 
229
- try {
230
- const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
231
- version = packageJson.version;
232
- packageName = packageJson.name;
233
- } catch (err) {
234
- console.error('Error reading package.json:', err);
235
- }
207
+ const currentProvider = trimText(context?.provider) || 'claude';
208
+ const discoveredProvider = discovered.find((entry) => entry.provider === currentProvider);
236
209
 
237
- const uptime = process.uptime();
238
- const uptimeMinutes = Math.floor(uptime / 60);
239
- const uptimeHours = Math.floor(uptimeMinutes / 60);
240
- const uptimeFormatted = uptimeHours > 0
241
- ? `${uptimeHours}h ${uptimeMinutes % 60}m`
242
- : `${uptimeMinutes}m`;
210
+ return {
211
+ current: {
212
+ provider: currentProvider,
213
+ model: trimText(context?.model) || trimText(discoveredProvider?.currentModel) || null
214
+ },
215
+ available
216
+ };
217
+ }
243
218
 
244
- return {
245
- type: 'builtin',
246
- action: 'status',
247
- data: {
248
- version,
249
- packageName,
250
- uptime: uptimeFormatted,
251
- uptimeSeconds: Math.floor(uptime),
252
- model: context?.model || null,
253
- provider: context?.provider || 'claude',
254
- nodeVersion: process.version,
255
- platform: process.platform
219
+ async function runBuiltinCommand(commandName, args, context) {
220
+ if (BUILTIN_COMMANDS.length === 0) {
221
+ return null;
222
+ }
223
+
224
+ switch (commandName) {
225
+ case '/help':
226
+ return {
227
+ type: 'builtin',
228
+ action: 'help',
229
+ data: {
230
+ content: [
231
+ '# Slash Commands',
232
+ '',
233
+ '## Built-in',
234
+ ...BUILTIN_COMMANDS.map((command) => `- \`${command.name}\` — ${command.description}`),
235
+ '',
236
+ '## Custom command folders',
237
+ '- Project scope: `.claude/commands/`',
238
+ '- User scope: `~/.claude/commands/`',
239
+ '',
240
+ 'Use `$ARGUMENTS`, `$1`, `$2`, and friends inside markdown command files.'
241
+ ].join('\n'),
242
+ format: 'markdown'
243
+ }
244
+ };
245
+ case '/clear':
246
+ return {
247
+ type: 'builtin',
248
+ action: 'clear',
249
+ data: { message: 'Cleared the current chat preview.' }
250
+ };
251
+ case '/model':
252
+ return {
253
+ type: 'builtin',
254
+ action: 'model',
255
+ data: await buildModelPayload(context)
256
+ };
257
+ case '/memory': {
258
+ const projectPath = trimText(context?.projectPath);
259
+ if (!projectPath) {
260
+ return {
261
+ type: 'builtin',
262
+ action: 'memory',
263
+ data: {
264
+ error: 'No project selected',
265
+ message: 'Select a project before opening CLAUDE.md.'
266
+ }
267
+ };
256
268
  }
257
- };
258
- },
259
269
 
260
- '/memory': async (args, context) => {
261
- const projectPath = context?.projectPath;
270
+ const memoryPath = path.join(projectPath, 'CLAUDE.md');
271
+ let exists = true;
272
+ try {
273
+ await fs.access(memoryPath);
274
+ } catch {
275
+ exists = false;
276
+ }
262
277
 
263
- if (!projectPath) {
264
278
  return {
265
279
  type: 'builtin',
266
280
  action: 'memory',
267
281
  data: {
268
- error: 'No project selected',
269
- message: 'Please select a project to access its CLAUDE.md file'
282
+ path: memoryPath,
283
+ exists,
284
+ message: exists
285
+ ? `Opening project memory at ${memoryPath}`
286
+ : `No CLAUDE.md found at ${memoryPath}.`
270
287
  }
271
288
  };
272
289
  }
290
+ case '/config':
291
+ return {
292
+ type: 'builtin',
293
+ action: 'config',
294
+ data: { message: 'Opening settings.' }
295
+ };
296
+ case '/status': {
297
+ const metadata = await getPackageMetadata();
298
+ const uptimeSeconds = Math.floor(process.uptime());
299
+ const uptimeMinutes = Math.floor(uptimeSeconds / 60);
300
+ const uptime = uptimeMinutes >= 60
301
+ ? `${Math.floor(uptimeMinutes / 60)}h ${uptimeMinutes % 60}m`
302
+ : `${uptimeMinutes}m`;
273
303
 
274
- const claudeMdPath = path.join(projectPath, 'CLAUDE.md');
275
-
276
- // Check if CLAUDE.md exists
277
- let exists = false;
278
- try {
279
- await fs.access(claudeMdPath);
280
- exists = true;
281
- } catch (err) {
282
- // File doesn't exist
304
+ return {
305
+ type: 'builtin',
306
+ action: 'status',
307
+ data: {
308
+ ...metadata,
309
+ uptime,
310
+ uptimeSeconds,
311
+ provider: trimText(context?.provider) || 'claude',
312
+ model: trimText(context?.model) || null,
313
+ nodeVersion: process.version,
314
+ platform: process.platform
315
+ }
316
+ };
283
317
  }
284
-
285
- return {
286
- type: 'builtin',
287
- action: 'memory',
288
- data: {
289
- path: claudeMdPath,
290
- exists,
291
- message: exists
292
- ? `Opening CLAUDE.md at ${claudeMdPath}`
293
- : `CLAUDE.md not found at ${claudeMdPath}. Create it to store project-specific instructions.`
294
- }
295
- };
296
- },
297
-
298
- '/config': async (args, context) => {
299
- return {
300
- type: 'builtin',
301
- action: 'config',
302
- data: {
303
- message: 'Opening settings...'
318
+ case '/rewind': {
319
+ const steps = Number.parseInt(String(args[0] || '1'), 10);
320
+ if (!Number.isFinite(steps) || steps < 1) {
321
+ return {
322
+ type: 'builtin',
323
+ action: 'rewind',
324
+ data: {
325
+ error: 'Invalid step count',
326
+ message: 'Usage: /rewind [positive-number]'
327
+ }
328
+ };
304
329
  }
305
- };
306
- },
307
330
 
308
- '/rewind': async (args, context) => {
309
- const steps = args[0] ? parseInt(args[0]) : 1;
310
-
311
- if (isNaN(steps) || steps < 1) {
312
331
  return {
313
332
  type: 'builtin',
314
333
  action: 'rewind',
315
334
  data: {
316
- error: 'Invalid steps parameter',
317
- message: 'Usage: /rewind [number] - Rewind conversation by N steps (default: 1)'
335
+ steps,
336
+ message: `Removing the latest ${steps} turn${steps === 1 ? '' : 's'}.`
318
337
  }
319
338
  };
320
339
  }
321
-
322
- return {
323
- type: 'builtin',
324
- action: 'rewind',
325
- data: {
326
- steps,
327
- message: `Rewinding conversation by ${steps} step${steps > 1 ? 's' : ''}...`
328
- }
329
- };
340
+ default:
341
+ return null;
330
342
  }
331
- };
343
+ }
332
344
 
333
- /**
334
- * POST /api/commands/list
335
- * List all available commands from project and user directories
336
- */
337
345
  router.post('/list', async (req, res) => {
338
346
  try {
339
- const { projectPath } = req.body;
340
- const allCommands = [...builtInCommands];
341
-
342
- // Scan project-level commands (.claude/commands/)
343
- if (projectPath) {
344
- const projectCommandsDir = path.join(projectPath, '.claude', 'commands');
345
- const projectCommands = await scanCommandsDirectory(
346
- projectCommandsDir,
347
- projectCommandsDir,
348
- 'project'
349
- );
350
- allCommands.push(...projectCommands);
351
- }
352
-
353
- // Scan user-level commands (~/.claude/commands/)
354
- const homeDir = os.homedir();
355
- const userCommandsDir = path.join(homeDir, '.claude', 'commands');
356
- const userCommands = await scanCommandsDirectory(
357
- userCommandsDir,
358
- userCommandsDir,
359
- 'user'
360
- );
361
- allCommands.push(...userCommands);
347
+ const projectPath = trimText(req.body?.projectPath);
348
+ const builtin = BUILTIN_COMMANDS.map(toBuiltinRecord);
349
+ const discoveredRoots = commandRootsFor(projectPath);
350
+ const custom = [];
362
351
 
363
- // Separate built-in and custom commands
364
- const customCommands = allCommands.filter(cmd => cmd.namespace !== 'builtin');
365
-
366
- // Sort commands alphabetically by name
367
- customCommands.sort((a, b) => a.name.localeCompare(b.name));
352
+ for (const root of discoveredRoots) {
353
+ custom.push(...await walkCommandTree(root.rootPath, root.namespace));
354
+ }
368
355
 
369
356
  res.json({
370
- builtIn: builtInCommands,
371
- custom: customCommands,
372
- count: allCommands.length
357
+ builtIn: builtin,
358
+ custom,
359
+ count: builtin.length + custom.length
373
360
  });
374
361
  } catch (error) {
375
362
  console.error('Error listing commands:', error);
@@ -380,151 +367,92 @@ router.post('/list', async (req, res) => {
380
367
  }
381
368
  });
382
369
 
383
- /**
384
- * POST /api/commands/load
385
- * Load a specific command file and return its content and metadata
386
- */
387
370
  router.post('/load', async (req, res) => {
388
- try {
389
- const { commandPath } = req.body;
390
-
391
- if (!commandPath) {
392
- return res.status(400).json({
393
- error: 'Command path is required'
394
- });
395
- }
396
-
397
- // Security: Prevent path traversal
398
- const resolvedPath = path.resolve(commandPath);
399
- if (!resolvedPath.startsWith(path.resolve(os.homedir())) &&
400
- !resolvedPath.includes('.claude/commands')) {
401
- return res.status(403).json({
402
- error: 'Access denied',
403
- message: 'Command must be in .claude/commands directory'
404
- });
405
- }
406
-
407
- // Read and parse the command file
408
- const content = await fs.readFile(commandPath, 'utf8');
409
- const { data: metadata, content: commandContent } = matter(content);
371
+ const commandPath = trimText(req.body?.commandPath);
372
+ const projectPath = trimText(req.body?.projectPath);
410
373
 
411
- res.json({
412
- path: commandPath,
413
- metadata,
414
- content: commandContent
374
+ if (!commandPath) {
375
+ return res.status(400).json({
376
+ error: 'Command path is required'
415
377
  });
378
+ }
379
+
380
+ try {
381
+ const document = await loadCommandDocument(commandPath, projectPath);
382
+ res.json(document);
416
383
  } catch (error) {
417
384
  if (error.code === 'ENOENT') {
418
385
  return res.status(404).json({
419
386
  error: 'Command not found',
420
- message: `Command file not found: ${req.body.commandPath}`
387
+ message: `Command file not found: ${commandPath}`
421
388
  });
422
389
  }
423
390
 
424
- console.error('Error loading command:', error);
425
- res.status(500).json({
426
- error: 'Failed to load command',
391
+ res.status(error.statusCode || 500).json({
392
+ error: error.statusCode ? 'Access denied' : 'Failed to load command',
427
393
  message: error.message
428
394
  });
429
395
  }
430
396
  });
431
397
 
432
- /**
433
- * POST /api/commands/execute
434
- * Execute a command with argument replacement
435
- * This endpoint prepares the command content but doesn't execute bash commands yet
436
- * (that will be handled in the command parser utility)
437
- */
438
398
  router.post('/execute', async (req, res) => {
439
- try {
440
- const { commandName, commandPath, args = [], context = {} } = req.body;
399
+ const commandName = trimText(req.body?.commandName);
400
+ const commandPath = trimText(req.body?.commandPath);
401
+ const args = Array.isArray(req.body?.args) ? req.body.args.map((value) => String(value)) : [];
402
+ const context = req.body?.context && typeof req.body.context === 'object' ? req.body.context : {};
403
+
404
+ if (!commandName) {
405
+ return res.status(400).json({
406
+ error: 'Command name is required'
407
+ });
408
+ }
441
409
 
442
- if (!commandName) {
443
- return res.status(400).json({
444
- error: 'Command name is required'
445
- });
446
- }
410
+ if (DISABLED_BUILTIN_COMMANDS.has(commandName)) {
411
+ return res.status(404).json({
412
+ error: 'Command not found',
413
+ message: `${commandName} is currently disabled`,
414
+ command: commandName
415
+ });
416
+ }
447
417
 
448
- // Handle built-in commands
449
- const handler = builtInHandlers[commandName];
450
- if (handler) {
451
- try {
452
- const result = await handler(args, context);
453
- return res.json({
454
- ...result,
455
- command: commandName
456
- });
457
- } catch (error) {
458
- console.error(`Error executing built-in command ${commandName}:`, error);
459
- return res.status(500).json({
460
- error: 'Command execution failed',
461
- message: error.message,
462
- command: commandName
463
- });
464
- }
418
+ try {
419
+ const builtinResult = await runBuiltinCommand(commandName, args, context);
420
+ if (builtinResult) {
421
+ return res.json({
422
+ command: commandName,
423
+ ...builtinResult
424
+ });
465
425
  }
466
426
 
467
- // Handle custom commands
468
427
  if (!commandPath) {
469
428
  return res.status(400).json({
470
429
  error: 'Command path is required for custom commands'
471
430
  });
472
431
  }
473
432
 
474
- // Load command content
475
- // Security: validate commandPath is within allowed directories
476
- {
477
- const resolvedPath = path.resolve(commandPath);
478
- const userBase = path.resolve(path.join(os.homedir(), '.claude', 'commands'));
479
- const projectBase = context?.projectPath
480
- ? path.resolve(path.join(context.projectPath, '.claude', 'commands'))
481
- : null;
482
- const isUnder = (base) => {
483
- const rel = path.relative(base, resolvedPath);
484
- return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
485
- };
486
- if (!(isUnder(userBase) || (projectBase && isUnder(projectBase)))) {
487
- return res.status(403).json({
488
- error: 'Access denied',
489
- message: 'Command must be in .claude/commands directory'
490
- });
491
- }
492
- }
493
- const content = await fs.readFile(commandPath, 'utf8');
494
- const { data: metadata, content: commandContent } = matter(content);
495
- // Basic argument replacement (will be enhanced in command parser utility)
496
- let processedContent = commandContent;
497
-
498
- // Replace $ARGUMENTS with all arguments joined
499
- const argsString = args.join(' ');
500
- processedContent = processedContent.replace(/\$ARGUMENTS/g, argsString);
501
-
502
- // Replace $1, $2, etc. with positional arguments
503
- args.forEach((arg, index) => {
504
- const placeholder = `$${index + 1}`;
505
- processedContent = processedContent.replace(new RegExp(`\\${placeholder}\\b`, 'g'), arg);
506
- });
433
+ const document = await loadCommandDocument(commandPath, trimText(context.projectPath));
434
+ const preparedContent = interpolateCommandBody(document.content, args);
507
435
 
508
436
  res.json({
509
437
  type: 'custom',
510
438
  command: commandName,
511
- content: processedContent,
512
- metadata,
513
- hasFileIncludes: processedContent.includes('@'),
514
- hasBashCommands: processedContent.includes('!')
439
+ metadata: document.metadata,
440
+ content: preparedContent,
441
+ hasFileIncludes: /(^|\s)@[\w./-]+/m.test(preparedContent),
442
+ hasBashCommands: /(^|\n)\s*!/.test(preparedContent)
515
443
  });
516
444
  } catch (error) {
517
445
  if (error.code === 'ENOENT') {
518
446
  return res.status(404).json({
519
447
  error: 'Command not found',
520
- message: `Command file not found: ${req.body.commandPath}`
448
+ message: `Command file not found: ${commandPath}`
521
449
  });
522
450
  }
523
451
 
524
- console.error('Error executing command:', error);
525
- res.status(500).json({
526
- error: 'Failed to execute command',
527
- message: error.message
452
+ res.status(error.statusCode || 500).json({
453
+ error: error.statusCode ? 'Access denied' : 'Command execution failed',
454
+ message: error.message,
455
+ command: commandName
528
456
  });
529
457
  }
530
458
  });