@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.
- package/dist/api-docs.html +2 -2
- package/dist/assets/App-VH1wNUHs.js +259 -0
- package/dist/assets/{ReviewApp-C9K--AQE.js → ReviewApp-D_9EN4TM.js} +1 -1
- package/dist/assets/{_basePickBy-DR_8uFCo.js → _basePickBy-BDnj7-0Z.js} +1 -1
- package/dist/assets/{_baseUniq-D0njlQ_7.js → _baseUniq-Bl0JKOyl.js} +1 -1
- package/dist/assets/{arc-CKlr_Rec.js → arc-DY-4Kev3.js} +1 -1
- package/dist/assets/{architectureDiagram-2XIMDMQ5-BmO_uLUH.js → architectureDiagram-2XIMDMQ5-qw7crNVd.js} +1 -1
- package/dist/assets/{blockDiagram-WCTKOSBZ-DhAeO-56.js → blockDiagram-WCTKOSBZ-B9xg7ep3.js} +1 -1
- package/dist/assets/{c4Diagram-IC4MRINW-C67kFoXx.js → c4Diagram-IC4MRINW-H9xp3ytb.js} +1 -1
- package/dist/assets/channel-CyNUnRfc.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-mLLagvJi.js → chunk-4BX2VUAB-B3EVDUxI.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-Lx-hOjlM.js → chunk-55IACEB6-CGv945ef.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-Bt-XmVUV.js → chunk-FMBD7UC4-uAT4CKWM.js} +1 -1
- package/dist/assets/{chunk-JSJVCQXG-Cya6gaDV.js → chunk-JSJVCQXG-Cbvlpkf7.js} +1 -1
- package/dist/assets/{chunk-KX2RTZJC-Bd7Ig6tF.js → chunk-KX2RTZJC-CcqIuGat.js} +1 -1
- package/dist/assets/{chunk-NQ4KR5QH-5UAE0Vg-.js → chunk-NQ4KR5QH-CgrcsRuX.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-BAxZ8m7w.js → chunk-QZHKN3VN-Cx0APOoV.js} +1 -1
- package/dist/assets/{chunk-WL4C6EOR-DjDPvUUP.js → chunk-WL4C6EOR-BbZirvBk.js} +1 -1
- package/dist/assets/classDiagram-VBA2DB6C-DxBtyz2A.js +1 -0
- package/dist/assets/classDiagram-v2-RAHNMMFH-DxBtyz2A.js +1 -0
- package/dist/assets/clone-C341l3d0.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-D-60XrkJ.js → cose-bilkent-S5V4N54A-CrvmGFLD.js} +1 -1
- package/dist/assets/{dagre-KLK3FWXG-bqu3ZS4K.js → dagre-KLK3FWXG-C-W6VPjS.js} +1 -1
- package/dist/assets/{diagram-E7M64L7V-BueeqoYm.js → diagram-E7M64L7V-IP2q3bL0.js} +1 -1
- package/dist/assets/{diagram-IFDJBPK2-D4fDv2E7.js → diagram-IFDJBPK2-CQaL-XyV.js} +1 -1
- package/dist/assets/{diagram-P4PSJMXO-WqipY3fN.js → diagram-P4PSJMXO-BxBLThfv.js} +1 -1
- package/dist/assets/{erDiagram-INFDFZHY-D0oVnO-x.js → erDiagram-INFDFZHY-Dyl7bJTt.js} +1 -1
- package/dist/assets/{flowDiagram-PKNHOUZH-DzbGyxrr.js → flowDiagram-PKNHOUZH-B7NFMgFK.js} +1 -1
- package/dist/assets/{ganttDiagram-A5KZAMGK-BwhbbgCP.js → ganttDiagram-A5KZAMGK-hReWSDu2.js} +1 -1
- package/dist/assets/{gitGraphDiagram-K3NZZRJ6-DZgAh_KM.js → gitGraphDiagram-K3NZZRJ6-gVgcr0ST.js} +1 -1
- package/dist/assets/{graph-DzKos-N0.js → graph-DNDiJhTn.js} +1 -1
- package/dist/assets/{highlighted-body-TPN3WLV5-CKDMgz3X.js → highlighted-body-TPN3WLV5-DclLmTou.js} +1 -1
- package/dist/assets/index-DBkz_W_P.css +1 -0
- package/dist/assets/index-DdRyoXKh.js +2 -0
- package/dist/assets/{infoDiagram-LFFYTUFH-BFicZbTf.js → infoDiagram-LFFYTUFH-CqQOOzDA.js} +1 -1
- package/dist/assets/{ishikawaDiagram-PHBUUO56-CtihxDxl.js → ishikawaDiagram-PHBUUO56-CZ0iLiHg.js} +1 -1
- package/dist/assets/{journeyDiagram-4ABVD52K-Du00J8_d.js → journeyDiagram-4ABVD52K-DdfYKfNh.js} +1 -1
- package/dist/assets/{kanban-definition-K7BYSVSG-BJi9S0iQ.js → kanban-definition-K7BYSVSG-C5Vf32u6.js} +1 -1
- package/dist/assets/{layout-B80Sityu.js → layout-rvTEu2KS.js} +1 -1
- package/dist/assets/{linear-sRQLOf5H.js → linear-CD9SiYze.js} +1 -1
- package/dist/assets/{mermaid-O7DHMXV3-CBuVs4eJ.js → mermaid-O7DHMXV3-OZ8qWWwa.js} +167 -157
- package/dist/assets/{mindmap-definition-YRQLILUH-C5IL_xi-.js → mindmap-definition-YRQLILUH-CQxrLNVc.js} +1 -1
- package/dist/assets/{pieDiagram-SKSYHLDU-CeTwlJ8z.js → pieDiagram-SKSYHLDU-XgAUByWg.js} +1 -1
- package/dist/assets/{quadrantDiagram-337W2JSQ-COfUcLWt.js → quadrantDiagram-337W2JSQ-CH16ls7G.js} +1 -1
- package/dist/assets/{requirementDiagram-Z7DCOOCP-DSb-CJ5B.js → requirementDiagram-Z7DCOOCP-B_kQO06L.js} +1 -1
- package/dist/assets/{sankeyDiagram-WA2Y5GQK-8jtuVb45.js → sankeyDiagram-WA2Y5GQK-ofe78CyS.js} +1 -1
- package/dist/assets/{sequenceDiagram-2WXFIKYE-C2VpkMwA.js → sequenceDiagram-2WXFIKYE-Ckbxwny6.js} +1 -1
- package/dist/assets/{stateDiagram-RAJIS63D-fmwMqxxc.js → stateDiagram-RAJIS63D-DNtzCk14.js} +1 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-B3VPhiE1.js +1 -0
- package/dist/assets/{timeline-definition-YZTLITO2-Dx1hP5lg.js → timeline-definition-YZTLITO2-zT6CklKt.js} +1 -1
- package/dist/assets/{treemap-KZPCXAKY-CkLOdYCZ.js → treemap-KZPCXAKY-y0U2c3xG.js} +1 -1
- package/dist/assets/vendor-codemirror-CMHSJ_9p.js +9 -0
- package/dist/assets/{vennDiagram-LZ73GAT5-D6KWcnln.js → vennDiagram-LZ73GAT5-xKj3SjYG.js} +1 -1
- package/dist/assets/{xychartDiagram-JWTSCODW-6fh6qmzN.js → xychartDiagram-JWTSCODW-Da_qyEoX.js} +1 -1
- package/dist/index.html +3 -3
- package/package.json +6 -5
- package/server/acp-runtime/client.js +120 -14
- package/server/acp-runtime/index.js +54 -0
- package/server/acp-runtime/registry.js +2 -2
- package/server/acp-runtime/session-store.js +75 -1
- package/server/cli.js +32 -8
- package/server/database/db.js +20 -0
- package/server/external-agent/ws.js +477 -24
- package/server/index.js +89 -147
- package/server/lan-access/core.js +79 -0
- package/server/lan-access/state.js +102 -0
- package/server/middleware/auth.js +57 -14
- package/server/projects.js +442 -535
- package/server/routes/auth.js +24 -4
- package/server/routes/cli-auth.js +21 -25
- package/server/routes/codex.js +84 -298
- package/server/routes/commands.js +335 -407
- package/server/routes/lan-access.js +231 -0
- package/server/routes/projects.js +154 -158
- package/server/routes/session-core.js +13 -7
- package/server/routes/settings.js +113 -99
- package/server/session-core/eventStore.js +15 -2
- package/server/session-core/providerAdapters.js +28 -28
- package/server/session-core/sessionListMerge.js +47 -0
- package/shared/conversationEvents.js +96 -1
- package/shared/modelConstants.js +79 -99
- package/dist/assets/App-GBcTeeUS.js +0 -460
- package/dist/assets/channel-V3MBjKys.js +0 -1
- package/dist/assets/classDiagram-VBA2DB6C-C790yYiY.js +0 -1
- package/dist/assets/classDiagram-v2-RAHNMMFH-C790yYiY.js +0 -1
- package/dist/assets/clone-BbMGfZwt.js +0 -1
- package/dist/assets/index-DiQlHzGj.js +0 -2
- package/dist/assets/index-Drat2nB9.css +0 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-9GGXVWrR.js +0 -1
- package/dist/assets/vendor-codemirror-BxPY6emf.js +0 -39
- package/server/routes/git.js +0 -1110
- package/server/routes/mcp-utils.js +0 -48
- package/server/routes/mcp.js +0 -536
- package/server/routes/taskmaster.js +0 -1963
- package/server/utils/mcp-detector.js +0 -198
- 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 {
|
|
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
|
-
|
|
38
|
+
function trimText(value) {
|
|
39
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
40
|
+
}
|
|
14
41
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
50
|
+
if (trimText(projectPath)) {
|
|
51
|
+
roots.unshift({
|
|
52
|
+
namespace: 'project',
|
|
53
|
+
rootPath: path.join(projectPath, '.claude', 'commands')
|
|
54
|
+
});
|
|
55
|
+
}
|
|
28
56
|
|
|
29
|
-
|
|
57
|
+
return roots;
|
|
58
|
+
}
|
|
30
59
|
|
|
31
|
-
|
|
32
|
-
|
|
60
|
+
function normalizeCommandName(relativeFilePath) {
|
|
61
|
+
const normalized = relativeFilePath.replace(/\\/g, '/').replace(/\.md$/i, '');
|
|
62
|
+
return normalized.startsWith('/') ? normalized : `/${normalized}`;
|
|
63
|
+
}
|
|
33
64
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
126
|
+
await visit(rootPath);
|
|
127
|
+
return collected.sort((left, right) => left.name.localeCompare(right.name));
|
|
77
128
|
}
|
|
78
129
|
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
154
|
+
return resolvedPath;
|
|
155
|
+
}
|
|
148
156
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
162
|
+
return {
|
|
163
|
+
path: resolvedPath,
|
|
164
|
+
metadata: parsed.data || {},
|
|
165
|
+
content: parsed.content
|
|
166
|
+
};
|
|
167
|
+
}
|
|
154
168
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
169
|
+
function interpolateCommandBody(content, args) {
|
|
170
|
+
const joinedArgs = args.join(' ');
|
|
171
|
+
let nextContent = String(content || '').replace(/\$ARGUMENTS/g, joinedArgs);
|
|
158
172
|
|
|
159
|
-
|
|
173
|
+
args.forEach((arg, index) => {
|
|
174
|
+
nextContent = nextContent.replace(new RegExp(`\\$${index + 1}\\b`, 'g'), arg);
|
|
175
|
+
});
|
|
160
176
|
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
230
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
210
|
+
return {
|
|
211
|
+
current: {
|
|
212
|
+
provider: currentProvider,
|
|
213
|
+
model: trimText(context?.model) || trimText(discoveredProvider?.currentModel) || null
|
|
214
|
+
},
|
|
215
|
+
available
|
|
216
|
+
};
|
|
217
|
+
}
|
|
243
218
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
261
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
317
|
-
message:
|
|
335
|
+
steps,
|
|
336
|
+
message: `Removing the latest ${steps} turn${steps === 1 ? '' : 's'}.`
|
|
318
337
|
}
|
|
319
338
|
};
|
|
320
339
|
}
|
|
321
|
-
|
|
322
|
-
|
|
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
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
364
|
-
|
|
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:
|
|
371
|
-
custom
|
|
372
|
-
count:
|
|
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
|
-
|
|
389
|
-
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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: ${
|
|
387
|
+
message: `Command file not found: ${commandPath}`
|
|
421
388
|
});
|
|
422
389
|
}
|
|
423
390
|
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
440
|
-
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
449
|
-
const
|
|
450
|
-
if (
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
475
|
-
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
hasFileIncludes:
|
|
514
|
-
hasBashCommands:
|
|
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: ${
|
|
448
|
+
message: `Command file not found: ${commandPath}`
|
|
521
449
|
});
|
|
522
450
|
}
|
|
523
451
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
});
|