@axhub/genie 0.2.8 → 0.2.10
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/LICENSE +21 -675
- package/dist/api-docs.html +2 -2
- package/dist/assets/App-CYCCsgwf.js +264 -0
- package/dist/assets/ReviewApp-0srHIXwb.js +1 -0
- package/dist/assets/{_basePickBy-CqJbRZ9y.js → _basePickBy-DVVb07UV.js} +1 -1
- package/dist/assets/{_baseUniq-BS8YH8jO.js → _baseUniq-BtbziL5G.js} +1 -1
- package/dist/assets/{arc-BBmKEN-S.js → arc-BsCC8yBD.js} +1 -1
- package/dist/assets/{architectureDiagram-2XIMDMQ5-N5lcb82R.js → architectureDiagram-2XIMDMQ5-woFp6eNI.js} +1 -1
- package/dist/assets/{blockDiagram-WCTKOSBZ-DTMwHuLn.js → blockDiagram-WCTKOSBZ-ya8VAc2k.js} +1 -1
- package/dist/assets/{c4Diagram-IC4MRINW-BTKlkXI9.js → c4Diagram-IC4MRINW-CY1dZmIZ.js} +1 -1
- package/dist/assets/channel-BMhScXFe.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-DUdoTxAc.js → chunk-4BX2VUAB-CR1lAd74.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-Bm_92xe4.js → chunk-55IACEB6-CP98WcFC.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-CGW0g62g.js → chunk-FMBD7UC4-D9c7ijAB.js} +1 -1
- package/dist/assets/{chunk-JSJVCQXG-DYkTH3w1.js → chunk-JSJVCQXG-DQAGYOn-.js} +1 -1
- package/dist/assets/{chunk-KX2RTZJC-C9oTlISU.js → chunk-KX2RTZJC-BbTXiDq7.js} +1 -1
- package/dist/assets/{chunk-NQ4KR5QH-CM50ygWP.js → chunk-NQ4KR5QH-BI6AX0dr.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-7dzpYeNJ.js → chunk-QZHKN3VN-DB3V2Ifo.js} +1 -1
- package/dist/assets/{chunk-WL4C6EOR-Cm9nQrsr.js → chunk-WL4C6EOR-DhzTthv6.js} +1 -1
- package/dist/assets/classDiagram-VBA2DB6C-CMIxlWcT.js +1 -0
- package/dist/assets/classDiagram-v2-RAHNMMFH-CMIxlWcT.js +1 -0
- package/dist/assets/clone-BPqOt4r3.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-Ccp_p0JZ.js → cose-bilkent-S5V4N54A-BQ09ZE2j.js} +1 -1
- package/dist/assets/{dagre-KLK3FWXG-fBwTLUp9.js → dagre-KLK3FWXG-Dc2ueD_R.js} +1 -1
- package/dist/assets/{diagram-E7M64L7V-CeNVmFUp.js → diagram-E7M64L7V-DP-LsQoL.js} +1 -1
- package/dist/assets/{diagram-IFDJBPK2-CtavyLGa.js → diagram-IFDJBPK2-Cg6r42cB.js} +1 -1
- package/dist/assets/{diagram-P4PSJMXO-CpQTjQwc.js → diagram-P4PSJMXO-aHsfoUZE.js} +1 -1
- package/dist/assets/{erDiagram-INFDFZHY-B8R5vwhd.js → erDiagram-INFDFZHY-qBXJ4aAz.js} +1 -1
- package/dist/assets/{flowDiagram-PKNHOUZH-BvkVVwIQ.js → flowDiagram-PKNHOUZH-D_13emJM.js} +1 -1
- package/dist/assets/{ganttDiagram-A5KZAMGK-DOu3hSNa.js → ganttDiagram-A5KZAMGK-BvIcOLwz.js} +1 -1
- package/dist/assets/{gitGraphDiagram-K3NZZRJ6-C7zT67YE.js → gitGraphDiagram-K3NZZRJ6-ad0vvNcU.js} +1 -1
- package/dist/assets/{graph-D11wiwHo.js → graph-CeJCMjan.js} +1 -1
- package/dist/assets/{highlighted-body-TPN3WLV5-Babpthg-.js → highlighted-body-TPN3WLV5-B_novwSz.js} +1 -1
- package/dist/assets/index-C514cLyb.js +2 -0
- package/dist/assets/index-h1DBl_g3.css +1 -0
- package/dist/assets/{infoDiagram-LFFYTUFH-BmA7IpQG.js → infoDiagram-LFFYTUFH-lOxAqb3m.js} +1 -1
- package/dist/assets/{ishikawaDiagram-PHBUUO56-BEquZd3E.js → ishikawaDiagram-PHBUUO56-DIr-51gj.js} +1 -1
- package/dist/assets/{journeyDiagram-4ABVD52K-BfemGz7f.js → journeyDiagram-4ABVD52K-CYcIW0ZU.js} +1 -1
- package/dist/assets/{kanban-definition-K7BYSVSG-CWja3mln.js → kanban-definition-K7BYSVSG-C1ZK616a.js} +1 -1
- package/dist/assets/{layout-BLUNf-PJ.js → layout-CI2RM-v6.js} +1 -1
- package/dist/assets/{linear-DukIV_Xv.js → linear-DE7bISck.js} +1 -1
- package/dist/assets/{mermaid-O7DHMXV3-SgtM28qI.js → mermaid-O7DHMXV3-XxAJo8EK.js} +6 -6
- package/dist/assets/{mindmap-definition-YRQLILUH-4UjqXITU.js → mindmap-definition-YRQLILUH-Dz6EFjmn.js} +1 -1
- package/dist/assets/{pieDiagram-SKSYHLDU-8AxqJd0M.js → pieDiagram-SKSYHLDU-DPpEzUed.js} +1 -1
- package/dist/assets/{quadrantDiagram-337W2JSQ-D60m8V8r.js → quadrantDiagram-337W2JSQ-xdoXNet7.js} +1 -1
- package/dist/assets/{requirementDiagram-Z7DCOOCP-zqh9jBVf.js → requirementDiagram-Z7DCOOCP-DUq8H3CL.js} +1 -1
- package/dist/assets/{sankeyDiagram-WA2Y5GQK-CDZILTLI.js → sankeyDiagram-WA2Y5GQK-CmqEUxRu.js} +1 -1
- package/dist/assets/{sequenceDiagram-2WXFIKYE-7BReFd0L.js → sequenceDiagram-2WXFIKYE-DhtXRNiH.js} +1 -1
- package/dist/assets/{stateDiagram-RAJIS63D-HPTVdIG4.js → stateDiagram-RAJIS63D-Dj0HOlbN.js} +1 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-C9utf5gv.js +1 -0
- package/dist/assets/{timeline-definition-YZTLITO2-CTVllFgr.js → timeline-definition-YZTLITO2-DUuJzZB5.js} +1 -1
- package/dist/assets/{treemap-KZPCXAKY-BtyxboJZ.js → treemap-KZPCXAKY-DpYBQ0qr.js} +1 -1
- package/dist/assets/vendor-codemirror-CMHSJ_9p.js +9 -0
- package/dist/assets/{vendor-react-Cpt6D04s.js → vendor-react-xmA_f8ig.js} +1 -1
- package/dist/assets/{vennDiagram-LZ73GAT5-D96ZI6Mg.js → vennDiagram-LZ73GAT5-DpePUyOd.js} +1 -1
- package/dist/assets/{xychartDiagram-JWTSCODW-eRk-39YO.js → xychartDiagram-JWTSCODW-Cfp1I4_U.js} +1 -1
- package/dist/index.html +5 -5
- package/package.json +8 -7
- package/server/acp-runtime/client.js +129 -16
- package/server/acp-runtime/index.js +54 -0
- package/server/acp-runtime/registry.js +2 -2
- package/server/acp-runtime/session-store.js +79 -5
- package/server/cli.js +55 -10
- package/server/database/db.js +20 -0
- package/server/external-agent/service.js +24 -6
- package/server/external-agent/ws.js +540 -27
- package/server/index.js +112 -151
- 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 +930 -667
- 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 +322 -407
- package/server/routes/lan-access.js +231 -0
- package/server/routes/projects.js +154 -158
- package/server/routes/session-core.js +160 -91
- package/server/routes/settings.js +113 -99
- package/server/session-core/eventStore.js +60 -20
- package/server/session-core/providerAdapters.js +75 -38
- package/server/session-core/runtimeState.js +8 -0
- package/server/session-core/sessionListMerge.js +47 -0
- package/shared/conversationEvents.js +174 -15
- package/shared/modelConstants.js +79 -99
- package/dist/assets/App-CTKZtqB1.js +0 -460
- package/dist/assets/ReviewApp-DM6BNAzR.js +0 -1
- package/dist/assets/channel-1oJBvF-0.js +0 -1
- package/dist/assets/classDiagram-VBA2DB6C-d5TeKFM4.js +0 -1
- package/dist/assets/classDiagram-v2-RAHNMMFH-d5TeKFM4.js +0 -1
- package/dist/assets/clone-CinxIlEu.js +0 -1
- package/dist/assets/index-DFxzgWoO.js +0 -2
- package/dist/assets/index-YCFGDVKw.css +0 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-DTUf5_gC.js +0 -1
- package/dist/assets/vendor-codemirror-Dz7_EqNA.js +0 -39
- package/server/_legacy-providers/README.md +0 -30
- package/server/_legacy-providers/claude-sdk.js +0 -956
- package/server/_legacy-providers/gemini-cli.js +0 -368
- package/server/_legacy-providers/openai-codex.js +0 -705
- package/server/_legacy-providers/opencode-cli.js +0 -674
- 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,357 @@
|
|
|
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
|
+
{ name: '/help', description: 'Show the built-in and custom slash commands available here.' },
|
|
22
|
+
{ name: '/clear', description: 'Clear the current in-memory conversation preview.' },
|
|
23
|
+
{ name: '/model', description: 'Show the active model and available provider model lists.' },
|
|
24
|
+
{ name: '/memory', description: 'Open the project-level CLAUDE.md memory file.' },
|
|
25
|
+
{ name: '/config', description: 'Open the local settings modal.' },
|
|
26
|
+
{ name: '/status', description: 'Show runtime metadata for the local Genie server.' },
|
|
27
|
+
{ name: '/rewind', description: 'Drop the last N user/assistant turns from the current chat.' }
|
|
28
|
+
];
|
|
12
29
|
|
|
13
|
-
const
|
|
30
|
+
const FALLBACK_PROVIDER_MODELS = {
|
|
31
|
+
claude: CLAUDE_MODELS.OPTIONS.map((option) => option.value),
|
|
32
|
+
codex: CODEX_MODELS.OPTIONS.map((option) => option.value),
|
|
33
|
+
gemini: GEMINI_MODELS.OPTIONS.map((option) => option.value),
|
|
34
|
+
opencode: OPENCODE_MODELS.OPTIONS.map((option) => option.value)
|
|
35
|
+
};
|
|
14
36
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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 = [];
|
|
37
|
+
function trimText(value) {
|
|
38
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
39
|
+
}
|
|
24
40
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
41
|
+
function commandRootsFor(projectPath) {
|
|
42
|
+
const roots = [
|
|
43
|
+
{
|
|
44
|
+
namespace: 'user',
|
|
45
|
+
rootPath: path.join(os.homedir(), '.claude', 'commands')
|
|
46
|
+
}
|
|
47
|
+
];
|
|
28
48
|
|
|
29
|
-
|
|
49
|
+
if (trimText(projectPath)) {
|
|
50
|
+
roots.unshift({
|
|
51
|
+
namespace: 'project',
|
|
52
|
+
rootPath: path.join(projectPath, '.claude', 'commands')
|
|
53
|
+
});
|
|
54
|
+
}
|
|
30
55
|
|
|
31
|
-
|
|
32
|
-
|
|
56
|
+
return roots;
|
|
57
|
+
}
|
|
33
58
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
}
|
|
59
|
+
function normalizeCommandName(relativeFilePath) {
|
|
60
|
+
const normalized = relativeFilePath.replace(/\\/g, '/').replace(/\.md$/i, '');
|
|
61
|
+
return normalized.startsWith('/') ? normalized : `/${normalized}`;
|
|
62
|
+
}
|
|
55
63
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
function inferDescription(frontmatter, markdownBody) {
|
|
65
|
+
const explicit = trimText(frontmatter?.description);
|
|
66
|
+
if (explicit) {
|
|
67
|
+
return explicit;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const firstMeaningfulLine = markdownBody
|
|
71
|
+
.split('\n')
|
|
72
|
+
.map((line) => line.trim())
|
|
73
|
+
.find((line) => line && !line.startsWith('---'));
|
|
74
|
+
|
|
75
|
+
if (!firstMeaningfulLine) {
|
|
76
|
+
return 'Custom slash command';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return firstMeaningfulLine.replace(/^#+\s*/, '').trim() || 'Custom slash command';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function walkCommandTree(rootPath, namespace) {
|
|
83
|
+
const collected = [];
|
|
84
|
+
|
|
85
|
+
async function visit(directory) {
|
|
86
|
+
let entries;
|
|
87
|
+
try {
|
|
88
|
+
entries = await fs.readdir(directory, { withFileTypes: true });
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error.code === 'ENOENT' || error.code === 'EACCES') {
|
|
91
|
+
return;
|
|
67
92
|
}
|
|
93
|
+
throw error;
|
|
68
94
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
95
|
+
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
const fullPath = path.join(directory, entry.name);
|
|
98
|
+
if (entry.isDirectory()) {
|
|
99
|
+
await visit(fullPath);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!entry.isFile() || !entry.name.toLowerCase().endsWith('.md')) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const rawDocument = await fs.readFile(fullPath, 'utf8');
|
|
109
|
+
const parsedDocument = matter(rawDocument);
|
|
110
|
+
const relativePath = path.relative(rootPath, fullPath);
|
|
111
|
+
collected.push({
|
|
112
|
+
name: normalizeCommandName(relativePath),
|
|
113
|
+
path: fullPath,
|
|
114
|
+
relativePath,
|
|
115
|
+
description: inferDescription(parsedDocument.data, parsedDocument.content),
|
|
116
|
+
namespace,
|
|
117
|
+
metadata: parsedDocument.data || {}
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error(`Error reading slash command ${fullPath}:`, error.message);
|
|
121
|
+
}
|
|
73
122
|
}
|
|
74
123
|
}
|
|
75
124
|
|
|
76
|
-
|
|
125
|
+
await visit(rootPath);
|
|
126
|
+
return collected.sort((left, right) => left.name.localeCompare(right.name));
|
|
77
127
|
}
|
|
78
128
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const builtInCommands = [
|
|
83
|
-
{
|
|
84
|
-
name: '/help',
|
|
85
|
-
description: 'Show help documentation for Claude Code',
|
|
129
|
+
function toBuiltinRecord(command) {
|
|
130
|
+
return {
|
|
131
|
+
...command,
|
|
86
132
|
namespace: 'builtin',
|
|
87
133
|
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',
|
|
128
|
-
namespace: 'builtin',
|
|
129
|
-
metadata: { type: 'builtin' }
|
|
130
|
-
}
|
|
131
|
-
];
|
|
134
|
+
};
|
|
135
|
+
}
|
|
132
136
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const builtInHandlers = {
|
|
138
|
-
'/help': async (args, context) => {
|
|
139
|
-
const helpText = `# Claude Code Commands
|
|
137
|
+
function isPathInside(basePath, targetPath) {
|
|
138
|
+
const relative = path.relative(basePath, targetPath);
|
|
139
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
140
|
+
}
|
|
140
141
|
|
|
141
|
-
|
|
142
|
+
function resolveAllowedCommandPath(commandPath, projectPath) {
|
|
143
|
+
const resolvedPath = path.resolve(commandPath);
|
|
144
|
+
const roots = commandRootsFor(projectPath);
|
|
145
|
+
const matchedRoot = roots.find((entry) => isPathInside(path.resolve(entry.rootPath), resolvedPath));
|
|
142
146
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
147
|
+
if (!matchedRoot) {
|
|
148
|
+
const error = new Error('Command must live in a .claude/commands directory');
|
|
149
|
+
error.statusCode = 403;
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
146
152
|
|
|
147
|
-
|
|
153
|
+
return resolvedPath;
|
|
154
|
+
}
|
|
148
155
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
156
|
+
async function loadCommandDocument(commandPath, projectPath) {
|
|
157
|
+
const resolvedPath = resolveAllowedCommandPath(commandPath, projectPath);
|
|
158
|
+
const raw = await fs.readFile(resolvedPath, 'utf8');
|
|
159
|
+
const parsed = matter(raw);
|
|
152
160
|
|
|
153
|
-
|
|
161
|
+
return {
|
|
162
|
+
path: resolvedPath,
|
|
163
|
+
metadata: parsed.data || {},
|
|
164
|
+
content: parsed.content
|
|
165
|
+
};
|
|
166
|
+
}
|
|
154
167
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
168
|
+
function interpolateCommandBody(content, args) {
|
|
169
|
+
const joinedArgs = args.join(' ');
|
|
170
|
+
let nextContent = String(content || '').replace(/\$ARGUMENTS/g, joinedArgs);
|
|
158
171
|
|
|
159
|
-
|
|
172
|
+
args.forEach((arg, index) => {
|
|
173
|
+
nextContent = nextContent.replace(new RegExp(`\\$${index + 1}\\b`, 'g'), arg);
|
|
174
|
+
});
|
|
160
175
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
\`\`\`
|
|
164
|
-
`;
|
|
176
|
+
return nextContent;
|
|
177
|
+
}
|
|
165
178
|
|
|
179
|
+
async function getPackageMetadata() {
|
|
180
|
+
try {
|
|
181
|
+
const raw = await fs.readFile(PACKAGE_JSON_PATH, 'utf8');
|
|
182
|
+
const parsed = JSON.parse(raw);
|
|
166
183
|
return {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
data: {
|
|
170
|
-
content: helpText,
|
|
171
|
-
format: 'markdown'
|
|
172
|
-
}
|
|
184
|
+
version: parsed.version || 'unknown',
|
|
185
|
+
packageName: parsed.name || '@axhub/genie'
|
|
173
186
|
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
'/clear': async (args, context) => {
|
|
187
|
+
} catch {
|
|
177
188
|
return {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
data: {
|
|
181
|
-
message: 'Conversation history cleared'
|
|
182
|
-
}
|
|
189
|
+
version: 'unknown',
|
|
190
|
+
packageName: '@axhub/genie'
|
|
183
191
|
};
|
|
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;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
205
194
|
|
|
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
|
-
},
|
|
195
|
+
async function buildModelPayload(context) {
|
|
196
|
+
const projectPath = trimText(context?.projectPath);
|
|
197
|
+
const discovered = await discoverAllProviders({ projectPath: projectPath || null });
|
|
198
|
+
const available = {};
|
|
222
199
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
200
|
+
for (const provider of discovered) {
|
|
201
|
+
available[provider.provider] = Array.isArray(provider.models) && provider.models.length > 0
|
|
202
|
+
? provider.models.map((entry) => entry.id)
|
|
203
|
+
: (FALLBACK_PROVIDER_MODELS[provider.provider] || []);
|
|
204
|
+
}
|
|
228
205
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
version = packageJson.version;
|
|
232
|
-
packageName = packageJson.name;
|
|
233
|
-
} catch (err) {
|
|
234
|
-
console.error('Error reading package.json:', err);
|
|
235
|
-
}
|
|
206
|
+
const currentProvider = trimText(context?.provider) || 'claude';
|
|
207
|
+
const discoveredProvider = discovered.find((entry) => entry.provider === currentProvider);
|
|
236
208
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
209
|
+
return {
|
|
210
|
+
current: {
|
|
211
|
+
provider: currentProvider,
|
|
212
|
+
model: trimText(context?.model) || trimText(discoveredProvider?.currentModel) || null
|
|
213
|
+
},
|
|
214
|
+
available
|
|
215
|
+
};
|
|
216
|
+
}
|
|
243
217
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
218
|
+
async function runBuiltinCommand(commandName, args, context) {
|
|
219
|
+
switch (commandName) {
|
|
220
|
+
case '/help':
|
|
221
|
+
return {
|
|
222
|
+
type: 'builtin',
|
|
223
|
+
action: 'help',
|
|
224
|
+
data: {
|
|
225
|
+
content: [
|
|
226
|
+
'# Slash Commands',
|
|
227
|
+
'',
|
|
228
|
+
'## Built-in',
|
|
229
|
+
...BUILTIN_COMMANDS.map((command) => `- \`${command.name}\` — ${command.description}`),
|
|
230
|
+
'',
|
|
231
|
+
'## Custom command folders',
|
|
232
|
+
'- Project scope: `.claude/commands/`',
|
|
233
|
+
'- User scope: `~/.claude/commands/`',
|
|
234
|
+
'',
|
|
235
|
+
'Use `$ARGUMENTS`, `$1`, `$2`, and friends inside markdown command files.'
|
|
236
|
+
].join('\n'),
|
|
237
|
+
format: 'markdown'
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
case '/clear':
|
|
241
|
+
return {
|
|
242
|
+
type: 'builtin',
|
|
243
|
+
action: 'clear',
|
|
244
|
+
data: { message: 'Cleared the current chat preview.' }
|
|
245
|
+
};
|
|
246
|
+
case '/model':
|
|
247
|
+
return {
|
|
248
|
+
type: 'builtin',
|
|
249
|
+
action: 'model',
|
|
250
|
+
data: await buildModelPayload(context)
|
|
251
|
+
};
|
|
252
|
+
case '/memory': {
|
|
253
|
+
const projectPath = trimText(context?.projectPath);
|
|
254
|
+
if (!projectPath) {
|
|
255
|
+
return {
|
|
256
|
+
type: 'builtin',
|
|
257
|
+
action: 'memory',
|
|
258
|
+
data: {
|
|
259
|
+
error: 'No project selected',
|
|
260
|
+
message: 'Select a project before opening CLAUDE.md.'
|
|
261
|
+
}
|
|
262
|
+
};
|
|
256
263
|
}
|
|
257
|
-
};
|
|
258
|
-
},
|
|
259
264
|
|
|
260
|
-
|
|
261
|
-
|
|
265
|
+
const memoryPath = path.join(projectPath, 'CLAUDE.md');
|
|
266
|
+
let exists = true;
|
|
267
|
+
try {
|
|
268
|
+
await fs.access(memoryPath);
|
|
269
|
+
} catch {
|
|
270
|
+
exists = false;
|
|
271
|
+
}
|
|
262
272
|
|
|
263
|
-
if (!projectPath) {
|
|
264
273
|
return {
|
|
265
274
|
type: 'builtin',
|
|
266
275
|
action: 'memory',
|
|
267
276
|
data: {
|
|
268
|
-
|
|
269
|
-
|
|
277
|
+
path: memoryPath,
|
|
278
|
+
exists,
|
|
279
|
+
message: exists
|
|
280
|
+
? `Opening project memory at ${memoryPath}`
|
|
281
|
+
: `No CLAUDE.md found at ${memoryPath}.`
|
|
270
282
|
}
|
|
271
283
|
};
|
|
272
284
|
}
|
|
285
|
+
case '/config':
|
|
286
|
+
return {
|
|
287
|
+
type: 'builtin',
|
|
288
|
+
action: 'config',
|
|
289
|
+
data: { message: 'Opening settings.' }
|
|
290
|
+
};
|
|
291
|
+
case '/status': {
|
|
292
|
+
const metadata = await getPackageMetadata();
|
|
293
|
+
const uptimeSeconds = Math.floor(process.uptime());
|
|
294
|
+
const uptimeMinutes = Math.floor(uptimeSeconds / 60);
|
|
295
|
+
const uptime = uptimeMinutes >= 60
|
|
296
|
+
? `${Math.floor(uptimeMinutes / 60)}h ${uptimeMinutes % 60}m`
|
|
297
|
+
: `${uptimeMinutes}m`;
|
|
273
298
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
299
|
+
return {
|
|
300
|
+
type: 'builtin',
|
|
301
|
+
action: 'status',
|
|
302
|
+
data: {
|
|
303
|
+
...metadata,
|
|
304
|
+
uptime,
|
|
305
|
+
uptimeSeconds,
|
|
306
|
+
provider: trimText(context?.provider) || 'claude',
|
|
307
|
+
model: trimText(context?.model) || null,
|
|
308
|
+
nodeVersion: process.version,
|
|
309
|
+
platform: process.platform
|
|
310
|
+
}
|
|
311
|
+
};
|
|
283
312
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
313
|
+
case '/rewind': {
|
|
314
|
+
const steps = Number.parseInt(String(args[0] || '1'), 10);
|
|
315
|
+
if (!Number.isFinite(steps) || steps < 1) {
|
|
316
|
+
return {
|
|
317
|
+
type: 'builtin',
|
|
318
|
+
action: 'rewind',
|
|
319
|
+
data: {
|
|
320
|
+
error: 'Invalid step count',
|
|
321
|
+
message: 'Usage: /rewind [positive-number]'
|
|
322
|
+
}
|
|
323
|
+
};
|
|
294
324
|
}
|
|
295
|
-
};
|
|
296
|
-
},
|
|
297
325
|
|
|
298
|
-
'/config': async (args, context) => {
|
|
299
|
-
return {
|
|
300
|
-
type: 'builtin',
|
|
301
|
-
action: 'config',
|
|
302
|
-
data: {
|
|
303
|
-
message: 'Opening settings...'
|
|
304
|
-
}
|
|
305
|
-
};
|
|
306
|
-
},
|
|
307
|
-
|
|
308
|
-
'/rewind': async (args, context) => {
|
|
309
|
-
const steps = args[0] ? parseInt(args[0]) : 1;
|
|
310
|
-
|
|
311
|
-
if (isNaN(steps) || steps < 1) {
|
|
312
326
|
return {
|
|
313
327
|
type: 'builtin',
|
|
314
328
|
action: 'rewind',
|
|
315
329
|
data: {
|
|
316
|
-
|
|
317
|
-
message:
|
|
330
|
+
steps,
|
|
331
|
+
message: `Removing the latest ${steps} turn${steps === 1 ? '' : 's'}.`
|
|
318
332
|
}
|
|
319
333
|
};
|
|
320
334
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
type: 'builtin',
|
|
324
|
-
action: 'rewind',
|
|
325
|
-
data: {
|
|
326
|
-
steps,
|
|
327
|
-
message: `Rewinding conversation by ${steps} step${steps > 1 ? 's' : ''}...`
|
|
328
|
-
}
|
|
329
|
-
};
|
|
335
|
+
default:
|
|
336
|
+
return null;
|
|
330
337
|
}
|
|
331
|
-
}
|
|
338
|
+
}
|
|
332
339
|
|
|
333
|
-
/**
|
|
334
|
-
* POST /api/commands/list
|
|
335
|
-
* List all available commands from project and user directories
|
|
336
|
-
*/
|
|
337
340
|
router.post('/list', async (req, res) => {
|
|
338
341
|
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
|
-
}
|
|
342
|
+
const projectPath = trimText(req.body?.projectPath);
|
|
343
|
+
const builtin = BUILTIN_COMMANDS.map(toBuiltinRecord);
|
|
344
|
+
const discoveredRoots = commandRootsFor(projectPath);
|
|
345
|
+
const custom = [];
|
|
352
346
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const userCommands = await scanCommandsDirectory(
|
|
357
|
-
userCommandsDir,
|
|
358
|
-
userCommandsDir,
|
|
359
|
-
'user'
|
|
360
|
-
);
|
|
361
|
-
allCommands.push(...userCommands);
|
|
362
|
-
|
|
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));
|
|
347
|
+
for (const root of discoveredRoots) {
|
|
348
|
+
custom.push(...await walkCommandTree(root.rootPath, root.namespace));
|
|
349
|
+
}
|
|
368
350
|
|
|
369
351
|
res.json({
|
|
370
|
-
builtIn:
|
|
371
|
-
custom
|
|
372
|
-
count:
|
|
352
|
+
builtIn: builtin,
|
|
353
|
+
custom,
|
|
354
|
+
count: builtin.length + custom.length
|
|
373
355
|
});
|
|
374
356
|
} catch (error) {
|
|
375
357
|
console.error('Error listing commands:', error);
|
|
@@ -380,151 +362,84 @@ router.post('/list', async (req, res) => {
|
|
|
380
362
|
}
|
|
381
363
|
});
|
|
382
364
|
|
|
383
|
-
/**
|
|
384
|
-
* POST /api/commands/load
|
|
385
|
-
* Load a specific command file and return its content and metadata
|
|
386
|
-
*/
|
|
387
365
|
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
|
-
}
|
|
366
|
+
const commandPath = trimText(req.body?.commandPath);
|
|
367
|
+
const projectPath = trimText(req.body?.projectPath);
|
|
396
368
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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);
|
|
410
|
-
|
|
411
|
-
res.json({
|
|
412
|
-
path: commandPath,
|
|
413
|
-
metadata,
|
|
414
|
-
content: commandContent
|
|
369
|
+
if (!commandPath) {
|
|
370
|
+
return res.status(400).json({
|
|
371
|
+
error: 'Command path is required'
|
|
415
372
|
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
const document = await loadCommandDocument(commandPath, projectPath);
|
|
377
|
+
res.json(document);
|
|
416
378
|
} catch (error) {
|
|
417
379
|
if (error.code === 'ENOENT') {
|
|
418
380
|
return res.status(404).json({
|
|
419
381
|
error: 'Command not found',
|
|
420
|
-
message: `Command file not found: ${
|
|
382
|
+
message: `Command file not found: ${commandPath}`
|
|
421
383
|
});
|
|
422
384
|
}
|
|
423
385
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
error: 'Failed to load command',
|
|
386
|
+
res.status(error.statusCode || 500).json({
|
|
387
|
+
error: error.statusCode ? 'Access denied' : 'Failed to load command',
|
|
427
388
|
message: error.message
|
|
428
389
|
});
|
|
429
390
|
}
|
|
430
391
|
});
|
|
431
392
|
|
|
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
393
|
router.post('/execute', async (req, res) => {
|
|
439
|
-
|
|
440
|
-
|
|
394
|
+
const commandName = trimText(req.body?.commandName);
|
|
395
|
+
const commandPath = trimText(req.body?.commandPath);
|
|
396
|
+
const args = Array.isArray(req.body?.args) ? req.body.args.map((value) => String(value)) : [];
|
|
397
|
+
const context = req.body?.context && typeof req.body.context === 'object' ? req.body.context : {};
|
|
398
|
+
|
|
399
|
+
if (!commandName) {
|
|
400
|
+
return res.status(400).json({
|
|
401
|
+
error: 'Command name is required'
|
|
402
|
+
});
|
|
403
|
+
}
|
|
441
404
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
405
|
+
try {
|
|
406
|
+
const builtinResult = await runBuiltinCommand(commandName, args, context);
|
|
407
|
+
if (builtinResult) {
|
|
408
|
+
return res.json({
|
|
409
|
+
command: commandName,
|
|
410
|
+
...builtinResult
|
|
445
411
|
});
|
|
446
412
|
}
|
|
447
413
|
|
|
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
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Handle custom commands
|
|
468
414
|
if (!commandPath) {
|
|
469
415
|
return res.status(400).json({
|
|
470
416
|
error: 'Command path is required for custom commands'
|
|
471
417
|
});
|
|
472
418
|
}
|
|
473
419
|
|
|
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
|
-
});
|
|
420
|
+
const document = await loadCommandDocument(commandPath, trimText(context.projectPath));
|
|
421
|
+
const preparedContent = interpolateCommandBody(document.content, args);
|
|
507
422
|
|
|
508
423
|
res.json({
|
|
509
424
|
type: 'custom',
|
|
510
425
|
command: commandName,
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
hasFileIncludes:
|
|
514
|
-
hasBashCommands:
|
|
426
|
+
metadata: document.metadata,
|
|
427
|
+
content: preparedContent,
|
|
428
|
+
hasFileIncludes: /(^|\s)@[\w./-]+/m.test(preparedContent),
|
|
429
|
+
hasBashCommands: /(^|\n)\s*!/.test(preparedContent)
|
|
515
430
|
});
|
|
516
431
|
} catch (error) {
|
|
517
432
|
if (error.code === 'ENOENT') {
|
|
518
433
|
return res.status(404).json({
|
|
519
434
|
error: 'Command not found',
|
|
520
|
-
message: `Command file not found: ${
|
|
435
|
+
message: `Command file not found: ${commandPath}`
|
|
521
436
|
});
|
|
522
437
|
}
|
|
523
438
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
439
|
+
res.status(error.statusCode || 500).json({
|
|
440
|
+
error: error.statusCode ? 'Access denied' : 'Command execution failed',
|
|
441
|
+
message: error.message,
|
|
442
|
+
command: commandName
|
|
528
443
|
});
|
|
529
444
|
}
|
|
530
445
|
});
|