jinzd-ai-cli 0.4.222 → 0.4.223
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/{batch-QOOO7HRW.js → batch-PJIRZJCT.js} +2 -2
- package/dist/{chunk-OJL3PY36.js → chunk-2X2DOHA6.js} +3 -2
- package/dist/{chunk-TNW22OUY.js → chunk-35AVZVXZ.js} +3 -3
- package/dist/{chunk-NZ4X6GUC.js → chunk-3ARPAVNV.js} +2 -2
- package/dist/{chunk-5EE4GRGG.js → chunk-5ZPO47YE.js} +3 -3
- package/dist/{chunk-AV5OPK7Q.js → chunk-GH37CLOE.js} +1 -1
- package/dist/{chunk-NNWWMGYK.js → chunk-GXAGBILB.js} +1 -1
- package/dist/{chunk-VWTUYDZG.js → chunk-OABKF7MR.js} +1 -1
- package/dist/{chunk-QT5FA2ZY.js → chunk-OJXYL2W2.js} +4 -4
- package/dist/{chunk-FBK7NSIK.js → chunk-OPAOQZDC.js} +2 -2
- package/dist/{chunk-VBRCWH55.js → chunk-PNFOWBKL.js} +3 -3
- package/dist/{chunk-SISFAT6W.js → chunk-RGL2KKCO.js} +1 -1
- package/dist/{chunk-Q3ZUDA6S.js → chunk-VT6NRDD2.js} +1 -1
- package/dist/{chunk-UWUTLIOH.js → chunk-WDUKU5HH.js} +1 -1
- package/dist/{chunk-S6L5R6SS.js → chunk-ZILWADNL.js} +1 -1
- package/dist/{ci-IB7NWHZN.js → ci-ESZOGOGZ.js} +4 -4
- package/dist/{ci-format-YLGZN3D4.js → ci-format-TIDA4V7T.js} +2 -2
- package/dist/{constants-BOMLZEXC.js → constants-VFBSBHFO.js} +1 -1
- package/dist/{doctor-cli-2ND4LONW.js → doctor-cli-W3I33E4Q.js} +4 -4
- package/dist/electron-server.js +1736 -1735
- package/dist/{hub-6WADFJNC.js → hub-7BLT2JUU.js} +6 -6
- package/dist/{hub-server-LJ2JSKZ2.js → hub-server-XX36DX7O.js} +2 -2
- package/dist/index.js +18 -18
- package/dist/{persist-GTBEKVFL.js → persist-GJ2JGZFR.js} +1 -1
- package/dist/{pr-F7WXUH27.js → pr-7YET7ODT.js} +4 -4
- package/dist/{run-tests-NIY24YDP.js → run-tests-4PNOUHC3.js} +1 -1
- package/dist/{run-tests-5NEMM6EF.js → run-tests-HGBTHWNT.js} +2 -2
- package/dist/{server-QGZYDDZW.js → server-AN7HZVNG.js} +1265 -1264
- package/dist/{server-ACFX2J66.js → server-APQHU4TR.js} +4 -4
- package/dist/{task-orchestrator-4GOBFXPN.js → task-orchestrator-L7LBMQUQ.js} +15 -7
- package/dist/{usage-JXT2YVA6.js → usage-BTP5SSGC.js} +2 -2
- package/package.json +1 -1
package/dist/electron-server.js
CHANGED
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
VERSION,
|
|
39
39
|
buildUserIdentityPrompt,
|
|
40
40
|
runTestsTool
|
|
41
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-GH37CLOE.js";
|
|
42
42
|
import {
|
|
43
43
|
hasSemanticIndex,
|
|
44
44
|
semanticSearch
|
|
@@ -64,8 +64,8 @@ import {
|
|
|
64
64
|
import express from "express";
|
|
65
65
|
import { createServer } from "http";
|
|
66
66
|
import { WebSocketServer } from "ws";
|
|
67
|
-
import { join as
|
|
68
|
-
import { existsSync as
|
|
67
|
+
import { join as join28, dirname as dirname11, resolve as resolve13, relative as relative4, sep as sep3 } from "path";
|
|
68
|
+
import { existsSync as existsSync33, readFileSync as readFileSync26, readdirSync as readdirSync17, statSync as statSync15, realpathSync } from "fs";
|
|
69
69
|
import { networkInterfaces } from "os";
|
|
70
70
|
|
|
71
71
|
// src/config/config-manager.ts
|
|
@@ -7257,7 +7257,7 @@ var ToolExecutor = class {
|
|
|
7257
7257
|
rl.resume();
|
|
7258
7258
|
process.stdout.write(prompt);
|
|
7259
7259
|
this.confirming = true;
|
|
7260
|
-
return new Promise((
|
|
7260
|
+
return new Promise((resolve14) => {
|
|
7261
7261
|
let completed = false;
|
|
7262
7262
|
const cleanup = (result) => {
|
|
7263
7263
|
if (completed) return;
|
|
@@ -7267,7 +7267,7 @@ var ToolExecutor = class {
|
|
|
7267
7267
|
rl.pause();
|
|
7268
7268
|
rlAny.output = savedOutput;
|
|
7269
7269
|
this.confirming = false;
|
|
7270
|
-
|
|
7270
|
+
resolve14(result);
|
|
7271
7271
|
};
|
|
7272
7272
|
const onLine = (line) => {
|
|
7273
7273
|
const trimmed = line.trim();
|
|
@@ -7441,7 +7441,7 @@ var ToolExecutor = class {
|
|
|
7441
7441
|
rl.resume();
|
|
7442
7442
|
process.stdout.write(color("Proceed? [y/N] (type y + Enter to confirm) "));
|
|
7443
7443
|
this.confirming = true;
|
|
7444
|
-
return new Promise((
|
|
7444
|
+
return new Promise((resolve14) => {
|
|
7445
7445
|
let completed = false;
|
|
7446
7446
|
const cleanup = (answer) => {
|
|
7447
7447
|
if (completed) return;
|
|
@@ -7451,7 +7451,7 @@ var ToolExecutor = class {
|
|
|
7451
7451
|
rl.pause();
|
|
7452
7452
|
rlAny.output = savedOutput;
|
|
7453
7453
|
this.confirming = false;
|
|
7454
|
-
|
|
7454
|
+
resolve14(answer === "y");
|
|
7455
7455
|
};
|
|
7456
7456
|
const onLine = (line) => {
|
|
7457
7457
|
const trimmed = line.trim();
|
|
@@ -8700,7 +8700,7 @@ var runInteractiveTool = {
|
|
|
8700
8700
|
PYTHONDONTWRITEBYTECODE: "1"
|
|
8701
8701
|
};
|
|
8702
8702
|
const prefixWarnings = [argsTypeWarning, stdinTypeWarning].filter(Boolean).join("");
|
|
8703
|
-
return new Promise((
|
|
8703
|
+
return new Promise((resolve14) => {
|
|
8704
8704
|
const child = spawn2(executable, cmdArgs.map(String), {
|
|
8705
8705
|
cwd: process.cwd(),
|
|
8706
8706
|
env,
|
|
@@ -8733,22 +8733,22 @@ var runInteractiveTool = {
|
|
|
8733
8733
|
setTimeout(writeNextLine, 400);
|
|
8734
8734
|
const timer = setTimeout(() => {
|
|
8735
8735
|
child.kill();
|
|
8736
|
-
|
|
8736
|
+
resolve14(`${prefixWarnings}[Timeout after ${timeout}ms]
|
|
8737
8737
|
${buildOutput(stdout, stderr)}`);
|
|
8738
8738
|
}, timeout);
|
|
8739
8739
|
child.on("close", (code) => {
|
|
8740
8740
|
clearTimeout(timer);
|
|
8741
8741
|
const output = buildOutput(stdout, stderr);
|
|
8742
8742
|
if (code !== 0 && code !== null) {
|
|
8743
|
-
|
|
8743
|
+
resolve14(`${prefixWarnings}Exit code ${code}:
|
|
8744
8744
|
${output}`);
|
|
8745
8745
|
} else {
|
|
8746
|
-
|
|
8746
|
+
resolve14(`${prefixWarnings}${output || "(no output)"}`);
|
|
8747
8747
|
}
|
|
8748
8748
|
});
|
|
8749
8749
|
child.on("error", (err) => {
|
|
8750
8750
|
clearTimeout(timer);
|
|
8751
|
-
|
|
8751
|
+
resolve14(
|
|
8752
8752
|
`${prefixWarnings}Failed to start process "${executable}": ${err.message}
|
|
8753
8753
|
Hint: On Windows, use the full path to the executable, e.g.:
|
|
8754
8754
|
C:\\Users\\Jinzd\\anaconda3\\envs\\python312\\python.exe`
|
|
@@ -9798,7 +9798,7 @@ function promptUser(rl, question) {
|
|
|
9798
9798
|
console.log();
|
|
9799
9799
|
console.log(chalk4.cyan("\u2753 ") + chalk4.bold(question));
|
|
9800
9800
|
process.stdout.write(chalk4.cyan("> "));
|
|
9801
|
-
return new Promise((
|
|
9801
|
+
return new Promise((resolve14) => {
|
|
9802
9802
|
let completed = false;
|
|
9803
9803
|
const cleanup = (answer) => {
|
|
9804
9804
|
if (completed) return;
|
|
@@ -9808,7 +9808,7 @@ function promptUser(rl, question) {
|
|
|
9808
9808
|
rl.pause();
|
|
9809
9809
|
rlAny.output = savedOutput;
|
|
9810
9810
|
askUserContext.prompting = false;
|
|
9811
|
-
|
|
9811
|
+
resolve14(answer);
|
|
9812
9812
|
};
|
|
9813
9813
|
const onLine = (line) => {
|
|
9814
9814
|
cleanup(line);
|
|
@@ -12253,7 +12253,7 @@ var McpClient = class {
|
|
|
12253
12253
|
// 内部方法:JSON-RPC 通信
|
|
12254
12254
|
// ══════════════════════════════════════════════════════════════════
|
|
12255
12255
|
sendRequest(method, params) {
|
|
12256
|
-
return new Promise((
|
|
12256
|
+
return new Promise((resolve14, reject) => {
|
|
12257
12257
|
if (!this.process?.stdin?.writable) {
|
|
12258
12258
|
return reject(new Error(`MCP server [${this.serverId}] stdin not writable`));
|
|
12259
12259
|
}
|
|
@@ -12277,7 +12277,7 @@ var McpClient = class {
|
|
|
12277
12277
|
this.pendingRequests.set(id, {
|
|
12278
12278
|
resolve: (result) => {
|
|
12279
12279
|
cleanup();
|
|
12280
|
-
|
|
12280
|
+
resolve14(result);
|
|
12281
12281
|
},
|
|
12282
12282
|
reject: (error) => {
|
|
12283
12283
|
cleanup();
|
|
@@ -12354,13 +12354,13 @@ var McpClient = class {
|
|
|
12354
12354
|
}
|
|
12355
12355
|
/** Promise 超时包装 */
|
|
12356
12356
|
withTimeout(promise, ms, label) {
|
|
12357
|
-
return new Promise((
|
|
12357
|
+
return new Promise((resolve14, reject) => {
|
|
12358
12358
|
const timer = setTimeout(() => {
|
|
12359
12359
|
reject(new Error(`MCP [${this.serverId}] ${label} timed out after ${ms}ms`));
|
|
12360
12360
|
}, ms);
|
|
12361
12361
|
promise.then((val) => {
|
|
12362
12362
|
clearTimeout(timer);
|
|
12363
|
-
|
|
12363
|
+
resolve14(val);
|
|
12364
12364
|
}).catch((err) => {
|
|
12365
12365
|
clearTimeout(timer);
|
|
12366
12366
|
reject(err);
|
|
@@ -12864,33 +12864,33 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
12864
12864
|
}
|
|
12865
12865
|
/** Resolve a pending confirm from client response */
|
|
12866
12866
|
resolveConfirm(requestId, approved) {
|
|
12867
|
-
const
|
|
12868
|
-
if (
|
|
12867
|
+
const resolve14 = this.pendingConfirms.get(requestId);
|
|
12868
|
+
if (resolve14) {
|
|
12869
12869
|
this.clearPendingTimer(requestId);
|
|
12870
12870
|
this.pendingConfirms.delete(requestId);
|
|
12871
12871
|
this.confirming = false;
|
|
12872
|
-
|
|
12872
|
+
resolve14(approved);
|
|
12873
12873
|
}
|
|
12874
12874
|
}
|
|
12875
12875
|
/** Resolve a pending batch confirm from client response */
|
|
12876
12876
|
resolveBatchConfirm(requestId, decision) {
|
|
12877
|
-
const
|
|
12878
|
-
if (
|
|
12877
|
+
const resolve14 = this.pendingBatchConfirms.get(requestId);
|
|
12878
|
+
if (resolve14) {
|
|
12879
12879
|
this.clearPendingTimer(requestId);
|
|
12880
12880
|
this.pendingBatchConfirms.delete(requestId);
|
|
12881
12881
|
this.confirming = false;
|
|
12882
12882
|
if (decision === "all" || decision === "none") {
|
|
12883
|
-
|
|
12883
|
+
resolve14(decision);
|
|
12884
12884
|
} else {
|
|
12885
|
-
|
|
12885
|
+
resolve14(new Set(decision));
|
|
12886
12886
|
}
|
|
12887
12887
|
}
|
|
12888
12888
|
}
|
|
12889
12889
|
/** Cancel all pending confirms (e.g., on disconnect) */
|
|
12890
12890
|
cancelAll() {
|
|
12891
|
-
for (const
|
|
12891
|
+
for (const resolve14 of this.pendingConfirms.values()) resolve14(false);
|
|
12892
12892
|
this.pendingConfirms.clear();
|
|
12893
|
-
for (const
|
|
12893
|
+
for (const resolve14 of this.pendingBatchConfirms.values()) resolve14("none");
|
|
12894
12894
|
this.pendingBatchConfirms.clear();
|
|
12895
12895
|
this.confirming = false;
|
|
12896
12896
|
}
|
|
@@ -12966,8 +12966,8 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
12966
12966
|
diff: this.getDiffPreview(call)
|
|
12967
12967
|
};
|
|
12968
12968
|
this.send(msg);
|
|
12969
|
-
return new Promise((
|
|
12970
|
-
this.pendingConfirms.set(requestId,
|
|
12969
|
+
return new Promise((resolve14) => {
|
|
12970
|
+
this.pendingConfirms.set(requestId, resolve14);
|
|
12971
12971
|
this.pendingTimers.set(requestId, setTimeout(() => {
|
|
12972
12972
|
if (this.pendingConfirms.has(requestId)) {
|
|
12973
12973
|
this.resolveConfirm(requestId, false);
|
|
@@ -12991,8 +12991,8 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
12991
12991
|
files
|
|
12992
12992
|
};
|
|
12993
12993
|
this.send(msg);
|
|
12994
|
-
return new Promise((
|
|
12995
|
-
this.pendingBatchConfirms.set(requestId,
|
|
12994
|
+
return new Promise((resolve14) => {
|
|
12995
|
+
this.pendingBatchConfirms.set(requestId, resolve14);
|
|
12996
12996
|
this.pendingTimers.set(requestId, setTimeout(() => {
|
|
12997
12997
|
if (this.pendingBatchConfirms.has(requestId)) {
|
|
12998
12998
|
this.resolveBatchConfirm(requestId, "none");
|
|
@@ -13661,8 +13661,8 @@ function loadContextFiles(options) {
|
|
|
13661
13661
|
}
|
|
13662
13662
|
|
|
13663
13663
|
// src/web/session-handler.ts
|
|
13664
|
-
import { mkdirSync as
|
|
13665
|
-
import { join as
|
|
13664
|
+
import { mkdirSync as mkdirSync16, createWriteStream as createWriteStream3 } from "fs";
|
|
13665
|
+
import { join as join26, dirname as dirname10 } from "path";
|
|
13666
13666
|
|
|
13667
13667
|
// src/repl/commands/project-init.ts
|
|
13668
13668
|
import { existsSync as existsSync26, readFileSync as readFileSync20, readdirSync as readdirSync11, statSync as statSync9 } from "fs";
|
|
@@ -14026,7 +14026,7 @@ ${topic}${contextSection}`;
|
|
|
14026
14026
|
messages.push({
|
|
14027
14027
|
role: "assistant",
|
|
14028
14028
|
content: msg.content,
|
|
14029
|
-
timestamp: msg.timestamp
|
|
14029
|
+
timestamp: new Date(msg.timestamp)
|
|
14030
14030
|
});
|
|
14031
14031
|
} else {
|
|
14032
14032
|
const prefix = `[${msg.speakerName} (${msg.speaker})]:`;
|
|
@@ -14034,7 +14034,7 @@ ${topic}${contextSection}`;
|
|
|
14034
14034
|
role: "user",
|
|
14035
14035
|
content: `${prefix}
|
|
14036
14036
|
${msg.content}`,
|
|
14037
|
-
timestamp: msg.timestamp
|
|
14037
|
+
timestamp: new Date(msg.timestamp)
|
|
14038
14038
|
});
|
|
14039
14039
|
}
|
|
14040
14040
|
}
|
|
@@ -14429,14 +14429,14 @@ import { join as join21 } from "path";
|
|
|
14429
14429
|
function discussionToMessages(state2) {
|
|
14430
14430
|
const out = [];
|
|
14431
14431
|
const t0 = state2.messages[0]?.timestamp ?? /* @__PURE__ */ new Date();
|
|
14432
|
-
out.push({ role: "user", content: `\u{1F3DB} Topic: ${state2.topic}`, timestamp: t0 });
|
|
14432
|
+
out.push({ role: "user", content: `\u{1F3DB} Topic: ${state2.topic}`, timestamp: new Date(t0) });
|
|
14433
14433
|
for (const m of state2.messages) {
|
|
14434
14434
|
if (m.speaker === "system") {
|
|
14435
|
-
out.push({ role: "system", content: m.content, timestamp: m.timestamp });
|
|
14435
|
+
out.push({ role: "system", content: m.content, timestamp: new Date(m.timestamp) });
|
|
14436
14436
|
continue;
|
|
14437
14437
|
}
|
|
14438
14438
|
if (m.speaker === "human") {
|
|
14439
|
-
out.push({ role: "user", content: `\u{1F9ED} ${m.speakerName}: ${m.content}`, timestamp: m.timestamp });
|
|
14439
|
+
out.push({ role: "user", content: `\u{1F9ED} ${m.speakerName}: ${m.content}`, timestamp: new Date(m.timestamp) });
|
|
14440
14440
|
continue;
|
|
14441
14441
|
}
|
|
14442
14442
|
if (m.passed || !m.content.trim()) continue;
|
|
@@ -14446,7 +14446,7 @@ function discussionToMessages(state2) {
|
|
|
14446
14446
|
content: `**${m.speakerName}** (${m.speaker})${tag}
|
|
14447
14447
|
|
|
14448
14448
|
${m.content}`,
|
|
14449
|
-
timestamp: m.timestamp
|
|
14449
|
+
timestamp: new Date(m.timestamp)
|
|
14450
14450
|
});
|
|
14451
14451
|
}
|
|
14452
14452
|
if (state2.summary && state2.summary.trim()) {
|
|
@@ -14468,1708 +14468,1726 @@ async function persistDiscussion(state2, config, defaultProvider, defaultModel)
|
|
|
14468
14468
|
|
|
14469
14469
|
// src/web/commands/provider-commands.ts
|
|
14470
14470
|
async function handleProvider(args, ctx) {
|
|
14471
|
-
|
|
14472
|
-
|
|
14473
|
-
|
|
14474
|
-
|
|
14475
|
-
|
|
14476
|
-
|
|
14477
|
-
|
|
14478
|
-
|
|
14479
|
-
|
|
14480
|
-
return;
|
|
14481
|
-
}
|
|
14482
|
-
ctx.currentProvider = id;
|
|
14483
|
-
ctx.currentModel = p.info.defaultModel;
|
|
14484
|
-
ctx.updateContextWindow();
|
|
14485
|
-
ctx.send({ type: "info", message: `Switched to provider: ${id} (${ctx.currentModel})` });
|
|
14486
|
-
ctx.sendStatus();
|
|
14471
|
+
const id = args[0];
|
|
14472
|
+
if (!id) {
|
|
14473
|
+
ctx.send({ type: "error", message: "Usage: /provider <id>" });
|
|
14474
|
+
return;
|
|
14475
|
+
}
|
|
14476
|
+
const p = ctx.providers.get(id);
|
|
14477
|
+
if (!p) {
|
|
14478
|
+
ctx.send({ type: "error", message: `Provider "${id}" not available` });
|
|
14479
|
+
return;
|
|
14487
14480
|
}
|
|
14481
|
+
ctx.currentProvider = id;
|
|
14482
|
+
ctx.currentModel = p.info.defaultModel;
|
|
14483
|
+
ctx.updateContextWindow();
|
|
14484
|
+
ctx.send({ type: "info", message: `Switched to provider: ${id} (${ctx.currentModel})` });
|
|
14485
|
+
ctx.sendStatus();
|
|
14488
14486
|
}
|
|
14489
14487
|
async function handleModel(args, ctx) {
|
|
14490
|
-
|
|
14491
|
-
|
|
14492
|
-
|
|
14493
|
-
|
|
14494
|
-
|
|
14495
|
-
|
|
14496
|
-
|
|
14497
|
-
|
|
14498
|
-
|
|
14499
|
-
|
|
14500
|
-
|
|
14501
|
-
|
|
14502
|
-
|
|
14503
|
-
ctx.send({ type: "error", message: `Model refresh failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
14504
|
-
}
|
|
14505
|
-
return;
|
|
14506
|
-
}
|
|
14507
|
-
if (sub === "cache") {
|
|
14508
|
-
if ((args[1] ?? "").toLowerCase() === "clear") {
|
|
14509
|
-
const providerId = args[2];
|
|
14510
|
-
ctx.providers.clearModelCache(providerId);
|
|
14511
|
-
ctx.send({ type: "info", message: providerId ? `Cleared model cache for ${providerId}.` : "Cleared model cache." });
|
|
14512
|
-
return;
|
|
14513
|
-
}
|
|
14514
|
-
const rows = ctx.providers.getModelCacheStatus();
|
|
14515
|
-
ctx.send({
|
|
14516
|
-
type: "info",
|
|
14517
|
-
message: rows.length === 0 ? "Model cache is empty. Use /model refresh to populate it." : rows.map((row) => `${row.providerId}: ${row.count} model(s), fetched ${row.fetchedAt}`).join("\n")
|
|
14518
|
-
});
|
|
14519
|
-
return;
|
|
14488
|
+
const sub = args[0];
|
|
14489
|
+
if (!sub) {
|
|
14490
|
+
ctx.send({ type: "error", message: "Usage: /model <id>|refresh [provider]|cache [clear [provider]]" });
|
|
14491
|
+
return;
|
|
14492
|
+
}
|
|
14493
|
+
if (sub === "refresh") {
|
|
14494
|
+
const providerId = args[1] ?? ctx.currentProvider;
|
|
14495
|
+
try {
|
|
14496
|
+
const models2 = await ctx.providers.refreshModels(providerId);
|
|
14497
|
+
ctx.send({ type: "info", message: `Refreshed ${models2.length} model(s) for ${providerId}.` });
|
|
14498
|
+
ctx.sendStatus();
|
|
14499
|
+
} catch (err) {
|
|
14500
|
+
ctx.send({ type: "error", message: `Model refresh failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
14520
14501
|
}
|
|
14521
|
-
|
|
14522
|
-
|
|
14523
|
-
|
|
14524
|
-
if (
|
|
14525
|
-
|
|
14502
|
+
return;
|
|
14503
|
+
}
|
|
14504
|
+
if (sub === "cache") {
|
|
14505
|
+
if ((args[1] ?? "").toLowerCase() === "clear") {
|
|
14506
|
+
const providerId = args[2];
|
|
14507
|
+
ctx.providers.clearModelCache(providerId);
|
|
14508
|
+
ctx.send({ type: "info", message: providerId ? `Cleared model cache for ${providerId}.` : "Cleared model cache." });
|
|
14526
14509
|
return;
|
|
14527
14510
|
}
|
|
14528
|
-
|
|
14529
|
-
ctx.
|
|
14530
|
-
|
|
14531
|
-
|
|
14511
|
+
const rows = ctx.providers.getModelCacheStatus();
|
|
14512
|
+
ctx.send({
|
|
14513
|
+
type: "info",
|
|
14514
|
+
message: rows.length === 0 ? "Model cache is empty. Use /model refresh to populate it." : rows.map((row) => `${row.providerId}: ${row.count} model(s), fetched ${row.fetchedAt}`).join("\n")
|
|
14515
|
+
});
|
|
14516
|
+
return;
|
|
14532
14517
|
}
|
|
14518
|
+
const modelId = sub;
|
|
14519
|
+
const models = await ctx.providers.listModels(ctx.currentProvider);
|
|
14520
|
+
const found = models.find((m) => m.id === modelId);
|
|
14521
|
+
if (!found) {
|
|
14522
|
+
ctx.send({ type: "error", message: `Model "${modelId}" not found` });
|
|
14523
|
+
return;
|
|
14524
|
+
}
|
|
14525
|
+
ctx.currentModel = modelId;
|
|
14526
|
+
ctx.updateContextWindow();
|
|
14527
|
+
ctx.send({ type: "info", message: `Switched to model: ${modelId}` });
|
|
14528
|
+
ctx.sendStatus();
|
|
14533
14529
|
}
|
|
14534
14530
|
async function handleThink(args, ctx) {
|
|
14535
|
-
|
|
14536
|
-
|
|
14537
|
-
|
|
14538
|
-
|
|
14539
|
-
|
|
14540
|
-
|
|
14541
|
-
ctx.sendStatus();
|
|
14542
|
-
}
|
|
14531
|
+
const sub = args[0];
|
|
14532
|
+
if (sub === "on") ctx.runtimeThinking = true;
|
|
14533
|
+
else if (sub === "off") ctx.runtimeThinking = false;
|
|
14534
|
+
else ctx.runtimeThinking = !ctx.runtimeThinking;
|
|
14535
|
+
ctx.send({ type: "info", message: `Thinking mode: ${ctx.runtimeThinking ? "ON" : "OFF"}` });
|
|
14536
|
+
ctx.sendStatus();
|
|
14543
14537
|
}
|
|
14544
14538
|
async function handlePlan(args, ctx) {
|
|
14545
|
-
|
|
14546
|
-
|
|
14547
|
-
|
|
14548
|
-
|
|
14549
|
-
|
|
14550
|
-
|
|
14551
|
-
|
|
14552
|
-
|
|
14553
|
-
|
|
14554
|
-
|
|
14555
|
-
ctx.send({ type: "info", message: `Plan mode: ${ctx.planMode ? "ON (read-only tools only)" : "OFF"}` });
|
|
14556
|
-
}
|
|
14557
|
-
ctx.sendStatus();
|
|
14539
|
+
const sub = args[0];
|
|
14540
|
+
if (sub === "exit" || sub === "cancel") {
|
|
14541
|
+
ctx.planMode = false;
|
|
14542
|
+
ctx.send({ type: "info", message: "Plan mode OFF." });
|
|
14543
|
+
} else if (sub === "execute") {
|
|
14544
|
+
ctx.planMode = false;
|
|
14545
|
+
ctx.send({ type: "info", message: "Plan mode OFF. Executing with all tools enabled." });
|
|
14546
|
+
} else {
|
|
14547
|
+
ctx.planMode = !ctx.planMode;
|
|
14548
|
+
ctx.send({ type: "info", message: `Plan mode: ${ctx.planMode ? "ON (read-only tools only)" : "OFF"}` });
|
|
14558
14549
|
}
|
|
14550
|
+
ctx.sendStatus();
|
|
14559
14551
|
}
|
|
14560
14552
|
|
|
14561
|
-
// src/web/commands/
|
|
14562
|
-
|
|
14563
|
-
|
|
14564
|
-
|
|
14565
|
-
|
|
14566
|
-
|
|
14567
|
-
|
|
14568
|
-
|
|
14553
|
+
// src/web/commands/_shared.ts
|
|
14554
|
+
import { existsSync as existsSync29, readFileSync as readFileSync22, writeFileSync as writeFileSync3, mkdirSync as mkdirSync14, readdirSync as readdirSync13, statSync as statSync12, createWriteStream } from "fs";
|
|
14555
|
+
import { join as join24, resolve as resolve10, dirname as dirname8 } from "path";
|
|
14556
|
+
|
|
14557
|
+
// src/cli/review-prompts.ts
|
|
14558
|
+
function buildReviewPrompt(diff, gitContextStr, detailed) {
|
|
14559
|
+
const level = detailed ? "Please perform a detailed in-depth review covering: security, performance, maintainability, error handling, naming conventions, and code duplication." : "Please perform a concise code review focusing on bugs, security issues, and key improvement suggestions.";
|
|
14560
|
+
return `# Code Review Request
|
|
14561
|
+
|
|
14562
|
+
${level}
|
|
14563
|
+
|
|
14564
|
+
## Git Status
|
|
14565
|
+
${gitContextStr}
|
|
14566
|
+
|
|
14567
|
+
## Code Changes (diff)
|
|
14568
|
+
\`\`\`diff
|
|
14569
|
+
${diff}
|
|
14570
|
+
\`\`\`
|
|
14571
|
+
|
|
14572
|
+
## Output Format
|
|
14573
|
+
Please structure your review as follows:
|
|
14574
|
+
1. **Overall Assessment**: One-sentence summary of the change quality
|
|
14575
|
+
2. **Issues** (if any): Each issue with [Severity] file:line \u2014 description + suggested fix
|
|
14576
|
+
3. **Improvement Suggestions** (if any): Non-critical but recommended optimizations
|
|
14577
|
+
4. **Highlights** (if any): Good practices worth acknowledging
|
|
14578
|
+
|
|
14579
|
+
Severity levels: \u{1F534} Critical / \u{1F7E1} Warning / \u{1F535} Info`;
|
|
14569
14580
|
}
|
|
14570
|
-
|
|
14571
|
-
|
|
14581
|
+
function buildSecurityReviewPrompt(diff, gitContextStr) {
|
|
14582
|
+
return `# Security Vulnerability Review
|
|
14583
|
+
|
|
14584
|
+
Analyze the following code changes **exclusively for security vulnerabilities**.
|
|
14585
|
+
|
|
14586
|
+
## Categories to check:
|
|
14587
|
+
1. **Injection** \u2014 SQL, command, path traversal, XSS, template injection
|
|
14588
|
+
2. **Authentication & Authorization** \u2014 hardcoded credentials, missing auth checks, privilege escalation
|
|
14589
|
+
3. **Secrets & Sensitive Data** \u2014 API keys, tokens, passwords in code, logging sensitive data
|
|
14590
|
+
4. **Input Validation** \u2014 missing validation, unsafe deserialization, buffer issues
|
|
14591
|
+
5. **Cryptography** \u2014 weak algorithms, improper random, hardcoded IVs/salts
|
|
14592
|
+
6. **Dependencies** \u2014 known vulnerable packages, unsafe dynamic imports
|
|
14593
|
+
7. **File System** \u2014 path traversal, unsafe file permissions, symlink attacks
|
|
14594
|
+
8. **Network** \u2014 SSRF, insecure protocols, missing TLS validation
|
|
14595
|
+
|
|
14596
|
+
## Git Status
|
|
14597
|
+
${gitContextStr}
|
|
14598
|
+
|
|
14599
|
+
## Code Changes (diff)
|
|
14600
|
+
\`\`\`diff
|
|
14601
|
+
${diff}
|
|
14602
|
+
\`\`\`
|
|
14603
|
+
|
|
14604
|
+
## Output Format
|
|
14605
|
+
For each finding:
|
|
14606
|
+
- **Severity**: \u{1F534} CRITICAL / \u{1F7E0} HIGH / \u{1F7E1} MEDIUM / \u{1F535} LOW / \u2139\uFE0F INFO
|
|
14607
|
+
- **Category**: (from list above)
|
|
14608
|
+
- **File & location**: file:line
|
|
14609
|
+
- **Description**: what the vulnerability is and how it could be exploited
|
|
14610
|
+
- **Recommended fix**: specific code change to resolve
|
|
14611
|
+
|
|
14612
|
+
If no security issues found, state "\u2705 No security vulnerabilities detected" with a brief explanation of what was checked.`;
|
|
14572
14613
|
}
|
|
14573
|
-
|
|
14574
|
-
|
|
14575
|
-
|
|
14576
|
-
|
|
14577
|
-
|
|
14578
|
-
|
|
14579
|
-
|
|
14580
|
-
|
|
14581
|
-
|
|
14582
|
-
|
|
14583
|
-
ctx.send({
|
|
14584
|
-
type: "info",
|
|
14585
|
-
message: `Provider: ${ctx.currentProvider}
|
|
14586
|
-
Model: ${ctx.currentModel}
|
|
14587
|
-
Session: ${session?.id ?? "none"} (${session?.messages.length ?? 0} messages)
|
|
14588
|
-
Tokens: in=${ctx.sessionTokenUsage.inputTokens} out=${ctx.sessionTokenUsage.outputTokens}${cacheLine}${costLine}`
|
|
14589
|
-
});
|
|
14590
|
-
}
|
|
14614
|
+
|
|
14615
|
+
// src/core/git-diff.ts
|
|
14616
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
14617
|
+
function readGitDiff(options = {}) {
|
|
14618
|
+
const args = ["diff"];
|
|
14619
|
+
if (options.staged) args.push("--staged");
|
|
14620
|
+
return execFileSync3("git", args, {
|
|
14621
|
+
encoding: "utf-8",
|
|
14622
|
+
timeout: options.timeoutMs ?? 1e4
|
|
14623
|
+
}).trim();
|
|
14591
14624
|
}
|
|
14592
|
-
|
|
14593
|
-
|
|
14594
|
-
|
|
14595
|
-
|
|
14596
|
-
|
|
14597
|
-
|
|
14598
|
-
|
|
14599
|
-
|
|
14600
|
-
|
|
14601
|
-
|
|
14602
|
-
|
|
14603
|
-
|
|
14604
|
-
|
|
14605
|
-
|
|
14606
|
-
|
|
14607
|
-
|
|
14608
|
-
|
|
14609
|
-
|
|
14610
|
-
|
|
14611
|
-
|
|
14612
|
-
|
|
14613
|
-
|
|
14614
|
-
|
|
14615
|
-
ctx.sendSessionMessages();
|
|
14616
|
-
ctx.sendStatus();
|
|
14617
|
-
ctx.sendSessionList();
|
|
14618
|
-
}
|
|
14619
|
-
const list = ctx.sessions.listSessions();
|
|
14620
|
-
const found = list.find((s) => s.id.startsWith(targetId));
|
|
14621
|
-
if (found) {
|
|
14622
|
-
ctx.sessions.loadSession(found.id);
|
|
14623
|
-
ctx.resetWebSessionUsage();
|
|
14624
|
-
ctx.send({ type: "info", message: `Loaded session: ${found.id.slice(0, 8)} "${found.title ?? ""}" (${found.messageCount} messages)` });
|
|
14625
|
-
const loadedSession = ctx.sessions.current;
|
|
14626
|
-
if (loadedSession && loadedSession.messages.length > 0) {
|
|
14627
|
-
const lastMsg = loadedSession.messages[loadedSession.messages.length - 1];
|
|
14628
|
-
const isIncomplete = lastMsg.role === "tool" || lastMsg.role === "assistant" && lastMsg.toolCalls && lastMsg.toolCalls.length > 0;
|
|
14629
|
-
if (isIncomplete) {
|
|
14630
|
-
ctx.send({
|
|
14631
|
-
type: "info",
|
|
14632
|
-
message: '\u26A0 This session appears to have been interrupted mid-task. The AI will see the tool history and can continue where it left off. Type "continue where you left off" to resume.'
|
|
14633
|
-
});
|
|
14634
|
-
}
|
|
14635
|
-
}
|
|
14636
|
-
ctx.sendSessionMessages();
|
|
14637
|
-
ctx.sendStatus();
|
|
14638
|
-
ctx.sendSessionList();
|
|
14639
|
-
} else {
|
|
14640
|
-
const recreated = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
|
|
14641
|
-
ctx.unsavedSessions.set(recreated.id, recreated);
|
|
14642
|
-
ctx.resetWebSessionUsage();
|
|
14643
|
-
ctx.send({
|
|
14644
|
-
type: "info",
|
|
14645
|
-
message: `Previous session (${targetId.slice(0, 8)}) is no longer available \u2014 started a new one.`
|
|
14646
|
-
});
|
|
14647
|
-
ctx.sendStatus();
|
|
14648
|
-
ctx.sendSessionList();
|
|
14649
|
-
}
|
|
14650
|
-
} else if (sub === "list") {
|
|
14651
|
-
ctx.sendSessionList();
|
|
14652
|
-
} else if (sub === "delete" && args[1]) {
|
|
14653
|
-
const targetId = args[1];
|
|
14654
|
-
const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
|
|
14655
|
-
if (cachedKey) {
|
|
14656
|
-
ctx.unsavedSessions.delete(cachedKey);
|
|
14657
|
-
if (ctx.sessions.current?.id === cachedKey) {
|
|
14658
|
-
ctx.sessions.clearCurrent();
|
|
14659
|
-
}
|
|
14660
|
-
ctx.send({ type: "info", message: `Deleted session: ${cachedKey.slice(0, 8)}` });
|
|
14661
|
-
ctx.sendSessionList();
|
|
14662
|
-
}
|
|
14663
|
-
const list = ctx.sessions.listSessions();
|
|
14664
|
-
const found = list.find((s) => s.id.startsWith(targetId));
|
|
14665
|
-
if (found) {
|
|
14666
|
-
ctx.sessions.deleteSession(found.id);
|
|
14667
|
-
ctx.send({ type: "info", message: `Deleted session: ${found.id.slice(0, 8)}` });
|
|
14668
|
-
ctx.sendSessionList();
|
|
14669
|
-
} else {
|
|
14670
|
-
ctx.send({ type: "error", message: `Session not found: ${targetId}` });
|
|
14671
|
-
}
|
|
14672
|
-
} else if (sub === "batch-delete") {
|
|
14673
|
-
const ids = args.slice(1).filter(Boolean);
|
|
14674
|
-
if (ids.length === 0) {
|
|
14675
|
-
ctx.send({ type: "error", message: "No session IDs provided." });
|
|
14676
|
-
}
|
|
14677
|
-
const list = ctx.sessions.listSessions();
|
|
14678
|
-
let deleted = 0;
|
|
14679
|
-
for (const targetId of ids) {
|
|
14680
|
-
const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
|
|
14681
|
-
if (cachedKey) {
|
|
14682
|
-
ctx.unsavedSessions.delete(cachedKey);
|
|
14683
|
-
deleted++;
|
|
14684
|
-
continue;
|
|
14685
|
-
}
|
|
14686
|
-
const found = list.find((s) => s.id.startsWith(targetId));
|
|
14687
|
-
if (found) {
|
|
14688
|
-
ctx.sessions.deleteSession(found.id);
|
|
14689
|
-
deleted++;
|
|
14690
|
-
}
|
|
14691
|
-
}
|
|
14692
|
-
ctx.send({ type: "info", message: `Deleted ${deleted} session(s).` });
|
|
14693
|
-
ctx.sendSessionList();
|
|
14694
|
-
} else if (sub === "rename" && args[1] && args.length >= 3) {
|
|
14695
|
-
const targetId = args[1];
|
|
14696
|
-
const newTitle = args.slice(2).join(" ").trim();
|
|
14697
|
-
if (!newTitle) {
|
|
14698
|
-
ctx.send({ type: "error", message: "Title cannot be empty." });
|
|
14699
|
-
}
|
|
14700
|
-
const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
|
|
14701
|
-
if (cachedKey) {
|
|
14702
|
-
const session = ctx.unsavedSessions.get(cachedKey);
|
|
14703
|
-
session.title = newTitle;
|
|
14704
|
-
ctx.send({ type: "info", message: `Renamed session: "${newTitle}"` });
|
|
14705
|
-
ctx.sendSessionList();
|
|
14706
|
-
}
|
|
14707
|
-
const list = ctx.sessions.listSessions();
|
|
14708
|
-
const found = list.find((s) => s.id.startsWith(targetId));
|
|
14709
|
-
if (found) {
|
|
14710
|
-
const session = ctx.sessions.loadSession(found.id);
|
|
14711
|
-
session.title = newTitle;
|
|
14712
|
-
ctx.sessions.save();
|
|
14713
|
-
ctx.send({ type: "info", message: `Renamed session: "${newTitle}"` });
|
|
14714
|
-
ctx.sendSessionList();
|
|
14715
|
-
} else {
|
|
14716
|
-
ctx.send({ type: "error", message: `Session not found: ${targetId}` });
|
|
14717
|
-
}
|
|
14718
|
-
} else {
|
|
14719
|
-
ctx.send({ type: "info", message: "Usage: /session new | list | load <id> | delete <id> | rename <id> <title>" });
|
|
14720
|
-
}
|
|
14721
|
-
}
|
|
14625
|
+
|
|
14626
|
+
// src/diagnostics/doctor-report.ts
|
|
14627
|
+
import { existsSync as existsSync28, statSync as statSync11 } from "fs";
|
|
14628
|
+
import { join as join23 } from "path";
|
|
14629
|
+
import { arch as arch2, platform as platform6, release as release2 } from "os";
|
|
14630
|
+
|
|
14631
|
+
// src/diagnostics/crash-log.ts
|
|
14632
|
+
import {
|
|
14633
|
+
existsSync as existsSync27,
|
|
14634
|
+
mkdirSync as mkdirSync13,
|
|
14635
|
+
readdirSync as readdirSync12,
|
|
14636
|
+
readFileSync as readFileSync21,
|
|
14637
|
+
statSync as statSync10,
|
|
14638
|
+
unlinkSync as unlinkSync6,
|
|
14639
|
+
writeFileSync as writeFileSync2
|
|
14640
|
+
} from "fs";
|
|
14641
|
+
import { join as join22 } from "path";
|
|
14642
|
+
import { homedir as homedir8, platform as platform5, release, arch } from "os";
|
|
14643
|
+
var LOGS_DIR_NAME = "logs";
|
|
14644
|
+
var CRASH_LOG_PREFIX = "crash-";
|
|
14645
|
+
function getLogsDir(configDir) {
|
|
14646
|
+
const base = configDir ?? join22(homedir8(), CONFIG_DIR_NAME);
|
|
14647
|
+
return join22(base, LOGS_DIR_NAME);
|
|
14722
14648
|
}
|
|
14723
|
-
|
|
14724
|
-
|
|
14725
|
-
const format = args[0] === "json" ? "json" : "md";
|
|
14726
|
-
ctx.exportConversation(format);
|
|
14727
|
-
}
|
|
14649
|
+
function truncate(s, n) {
|
|
14650
|
+
return s.length <= n ? s : s.slice(0, n) + "\u2026";
|
|
14728
14651
|
}
|
|
14729
|
-
|
|
14730
|
-
|
|
14731
|
-
|
|
14732
|
-
|
|
14733
|
-
|
|
14734
|
-
|
|
14735
|
-
|
|
14736
|
-
|
|
14737
|
-
|
|
14738
|
-
|
|
14739
|
-
|
|
14740
|
-
|
|
14652
|
+
function listRecentCrashes(limit = 10, configDir) {
|
|
14653
|
+
const dir = getLogsDir(configDir);
|
|
14654
|
+
if (!existsSync27(dir)) return [];
|
|
14655
|
+
const files = [];
|
|
14656
|
+
let names;
|
|
14657
|
+
try {
|
|
14658
|
+
names = readdirSync12(dir);
|
|
14659
|
+
} catch {
|
|
14660
|
+
return [];
|
|
14661
|
+
}
|
|
14662
|
+
for (const name of names) {
|
|
14663
|
+
if (!name.startsWith(CRASH_LOG_PREFIX) || !name.endsWith(".log")) continue;
|
|
14664
|
+
const full = join22(dir, name);
|
|
14665
|
+
try {
|
|
14666
|
+
const st = statSync10(full);
|
|
14667
|
+
const head = readFileSync21(full, "utf-8").split("\n").slice(0, 20);
|
|
14668
|
+
const tsLine = head.find((l) => l.startsWith("timestamp:"));
|
|
14669
|
+
const kindLine = head.find((l) => l.startsWith("kind:"));
|
|
14670
|
+
const msgIdx = head.findIndex((l) => l.trim() === "## message");
|
|
14671
|
+
const message = msgIdx >= 0 && head[msgIdx + 1] ? head[msgIdx + 1] : "";
|
|
14672
|
+
files.push({
|
|
14673
|
+
file: full,
|
|
14674
|
+
timestamp: tsLine ? tsLine.replace("timestamp:", "").trim() : st.mtime.toISOString(),
|
|
14675
|
+
kind: kindLine ? kindLine.replace("kind:", "").trim() : "unknown",
|
|
14676
|
+
message: truncate(message, 200),
|
|
14677
|
+
sizeBytes: st.size
|
|
14741
14678
|
});
|
|
14742
|
-
|
|
14743
|
-
}
|
|
14744
|
-
const lines = [
|
|
14745
|
-
"\u{1F4B0} Session Cost & Token Usage",
|
|
14746
|
-
` Provider : ${ctx.currentProvider}`,
|
|
14747
|
-
` Model : ${ctx.currentModel}`,
|
|
14748
|
-
` Input (uncached): ${u.inputTokens.toLocaleString()}`,
|
|
14749
|
-
` Output : ${u.outputTokens.toLocaleString()}`
|
|
14750
|
-
];
|
|
14751
|
-
if (u.cacheCreationTokens > 0) lines.push(` Cache write : ${u.cacheCreationTokens.toLocaleString()}`);
|
|
14752
|
-
if (u.cacheReadTokens > 0) {
|
|
14753
|
-
const pct = Math.round(u.cacheReadTokens / (u.cacheReadTokens + u.inputTokens) * 100);
|
|
14754
|
-
lines.push(` Cache read : ${u.cacheReadTokens.toLocaleString()} (${pct}% hit rate)`);
|
|
14755
|
-
}
|
|
14756
|
-
lines.push(` Total tokens : ${total.toLocaleString()}`);
|
|
14757
|
-
if (cost != null) {
|
|
14758
|
-
lines.push(` Cost : ${formatCost(cost)}`);
|
|
14759
|
-
if (savings != null && savings > 0) {
|
|
14760
|
-
lines.push(` Cache savings : -${formatCost(savings)} (vs no cache)`);
|
|
14761
|
-
}
|
|
14762
|
-
} else {
|
|
14763
|
-
lines.push(` Cost : \u2014 (pricing unknown for this model)`);
|
|
14679
|
+
} catch {
|
|
14764
14680
|
}
|
|
14765
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
14766
14681
|
}
|
|
14682
|
+
files.sort((a, b) => a.timestamp < b.timestamp ? 1 : -1);
|
|
14683
|
+
return files.slice(0, limit);
|
|
14767
14684
|
}
|
|
14768
|
-
|
|
14769
|
-
|
|
14770
|
-
|
|
14771
|
-
|
|
14772
|
-
|
|
14773
|
-
|
|
14774
|
-
|
|
14775
|
-
|
|
14776
|
-
|
|
14777
|
-
|
|
14778
|
-
|
|
14779
|
-
|
|
14780
|
-
|
|
14781
|
-
|
|
14782
|
-
|
|
14783
|
-
|
|
14784
|
-
}
|
|
14785
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
14786
|
-
}
|
|
14787
|
-
}
|
|
14788
|
-
const cpName = args.slice(1).join(" ").trim();
|
|
14789
|
-
if (sub === "save") {
|
|
14790
|
-
if (!cpName) {
|
|
14791
|
-
ctx.send({ type: "error", message: "Usage: /checkpoint save <name>" });
|
|
14792
|
-
}
|
|
14793
|
-
session.createCheckpoint(cpName);
|
|
14794
|
-
ctx.send({ type: "info", message: `\u2713 Checkpoint "${cpName}" saved at message #${session.messages.length}` });
|
|
14795
|
-
} else if (sub === "restore") {
|
|
14796
|
-
if (!cpName) {
|
|
14797
|
-
ctx.send({ type: "error", message: "Usage: /checkpoint restore <name>" });
|
|
14798
|
-
}
|
|
14799
|
-
const ok = session.restoreCheckpoint(cpName);
|
|
14800
|
-
if (ok) {
|
|
14801
|
-
ctx.send({ type: "info", message: `\u2713 Restored to checkpoint "${cpName}" (${session.messages.length} messages)` });
|
|
14802
|
-
ctx.sendSessionMessages();
|
|
14803
|
-
ctx.sendStatus();
|
|
14804
|
-
} else {
|
|
14805
|
-
ctx.send({ type: "error", message: `Checkpoint "${cpName}" not found.` });
|
|
14806
|
-
}
|
|
14807
|
-
} else if (sub === "delete") {
|
|
14808
|
-
if (!cpName) {
|
|
14809
|
-
ctx.send({ type: "error", message: "Usage: /checkpoint delete <name>" });
|
|
14810
|
-
}
|
|
14811
|
-
const ok = session.deleteCheckpoint(cpName);
|
|
14812
|
-
if (ok) {
|
|
14813
|
-
ctx.send({ type: "info", message: `\u2713 Deleted checkpoint "${cpName}"` });
|
|
14814
|
-
} else {
|
|
14815
|
-
ctx.send({ type: "error", message: `Checkpoint "${cpName}" not found.` });
|
|
14816
|
-
}
|
|
14817
|
-
} else {
|
|
14818
|
-
ctx.send({ type: "error", message: `Unknown subcommand: ${sub}. Use save/restore/list/delete.` });
|
|
14819
|
-
}
|
|
14685
|
+
function getConfigDirUsage(configDir) {
|
|
14686
|
+
const base = configDir ?? join22(homedir8(), CONFIG_DIR_NAME);
|
|
14687
|
+
if (!existsSync27(base)) return { totalBytes: 0, entries: [] };
|
|
14688
|
+
const entries = [];
|
|
14689
|
+
let total = 0;
|
|
14690
|
+
let names;
|
|
14691
|
+
try {
|
|
14692
|
+
names = readdirSync12(base);
|
|
14693
|
+
} catch {
|
|
14694
|
+
return { totalBytes: 0, entries: [] };
|
|
14695
|
+
}
|
|
14696
|
+
for (const name of names) {
|
|
14697
|
+
const full = join22(base, name);
|
|
14698
|
+
const bytes = dirSize(full);
|
|
14699
|
+
entries.push({ name, bytes });
|
|
14700
|
+
total += bytes;
|
|
14820
14701
|
}
|
|
14702
|
+
entries.sort((a, b) => b.bytes - a.bytes);
|
|
14703
|
+
return { totalBytes: total, entries };
|
|
14821
14704
|
}
|
|
14822
|
-
|
|
14823
|
-
{
|
|
14824
|
-
const
|
|
14825
|
-
if (
|
|
14826
|
-
|
|
14827
|
-
|
|
14828
|
-
const
|
|
14829
|
-
|
|
14830
|
-
const r = session.resolveBranchRef(ref);
|
|
14831
|
-
if (r.ok) return r.id;
|
|
14832
|
-
if (r.reason === "ambiguous") {
|
|
14833
|
-
const list = r.matches.map((m) => `${m.id} (${m.title})`).join(", ");
|
|
14834
|
-
ctx.send({ type: "error", message: `Ambiguous branch reference "${ref}". Matches: ${list}. Use the 6-char id.` });
|
|
14835
|
-
} else {
|
|
14836
|
-
ctx.send({ type: "error", message: `Branch "${ref}" not found. Use /branch list to see all branches.` });
|
|
14837
|
-
}
|
|
14838
|
-
return null;
|
|
14839
|
-
};
|
|
14840
|
-
if (!sub || sub === "list") {
|
|
14841
|
-
const branches = session.listBranches();
|
|
14842
|
-
const lines = [`\u{1F33F} Branches (${branches.length}):`, ""];
|
|
14843
|
-
for (const b of branches) {
|
|
14844
|
-
const marker = b.id === session.activeBranchId ? "\u25CF " : " ";
|
|
14845
|
-
const count = b.id === session.activeBranchId ? session.messages.length : session.getBranchMessages(b.id)?.length ?? 0;
|
|
14846
|
-
const parent = b.parentBranchId ? ` \u2190 ${b.parentBranchId}@${b.parentMessageIndex}` : "";
|
|
14847
|
-
lines.push(` ${marker}${b.id.padEnd(10)} ${b.title.padEnd(20)} (${count} msgs)${parent}`);
|
|
14848
|
-
}
|
|
14849
|
-
lines.push("");
|
|
14850
|
-
lines.push("Usage: /branch new <msgIndex> [title] | switch <id|title> | delete <id|title> | rename <id|title> <new title> | diff <id|title> | cherry-pick <id|title> <msgIndex>");
|
|
14851
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
14852
|
-
}
|
|
14853
|
-
if (sub === "new") {
|
|
14854
|
-
const idxArg = args[1];
|
|
14855
|
-
if (!idxArg) {
|
|
14856
|
-
ctx.send({ type: "error", message: "Usage: /branch new <msgIndex> [title]" });
|
|
14857
|
-
}
|
|
14858
|
-
const fromIndex = parseInt(idxArg, 10);
|
|
14859
|
-
if (isNaN(fromIndex) || fromIndex < 0 || fromIndex > session.messages.length) {
|
|
14860
|
-
ctx.send({ type: "error", message: `Invalid msgIndex: ${idxArg}. Range: 0-${session.messages.length}` });
|
|
14861
|
-
}
|
|
14862
|
-
const title = args.slice(2).join(" ").trim() || void 0;
|
|
14863
|
-
try {
|
|
14864
|
-
const newId = session.createBranch(fromIndex, title);
|
|
14865
|
-
await ctx.sessions.save();
|
|
14866
|
-
ctx.send({ type: "info", message: `\u2713 Created branch "${newId}" from message #${fromIndex}. Now active (${session.messages.length} messages).` });
|
|
14867
|
-
ctx.sendSessionMessages();
|
|
14868
|
-
ctx.sendStatus();
|
|
14869
|
-
} catch (err) {
|
|
14870
|
-
ctx.send({ type: "error", message: err.message });
|
|
14871
|
-
}
|
|
14872
|
-
}
|
|
14873
|
-
if (sub === "switch") {
|
|
14874
|
-
const ref = args[1];
|
|
14875
|
-
if (!ref) {
|
|
14876
|
-
ctx.send({ type: "error", message: "Usage: /branch switch <id|title>" });
|
|
14877
|
-
}
|
|
14878
|
-
const id = resolve15(ref);
|
|
14879
|
-
if (!id) {
|
|
14880
|
-
}
|
|
14881
|
-
const ok = session.switchBranch(id);
|
|
14882
|
-
if (ok) {
|
|
14883
|
-
await ctx.sessions.save();
|
|
14884
|
-
const b = session.getActiveBranch();
|
|
14885
|
-
ctx.send({ type: "info", message: `\u2713 Switched to branch "${b.id}" \u2014 ${b.title} (${session.messages.length} messages)` });
|
|
14886
|
-
ctx.sendSessionMessages();
|
|
14887
|
-
ctx.sendStatus();
|
|
14888
|
-
} else {
|
|
14889
|
-
ctx.send({ type: "error", message: `Cannot switch to "${id}" (already active).` });
|
|
14890
|
-
}
|
|
14891
|
-
}
|
|
14892
|
-
if (sub === "delete") {
|
|
14893
|
-
const ref = args[1];
|
|
14894
|
-
if (!ref) {
|
|
14895
|
-
ctx.send({ type: "error", message: "Usage: /branch delete <id|title>" });
|
|
14896
|
-
}
|
|
14897
|
-
const id = resolve15(ref);
|
|
14898
|
-
if (!id) {
|
|
14899
|
-
}
|
|
14900
|
-
const ok = session.deleteBranch(id);
|
|
14901
|
-
if (ok) {
|
|
14902
|
-
await ctx.sessions.save();
|
|
14903
|
-
ctx.send({ type: "info", message: `\u2713 Deleted branch "${id}"` });
|
|
14904
|
-
ctx.sendStatus();
|
|
14905
|
-
} else {
|
|
14906
|
-
ctx.send({ type: "error", message: `Cannot delete "${id}" (active, or last remaining branch).` });
|
|
14907
|
-
}
|
|
14908
|
-
}
|
|
14909
|
-
if (sub === "rename") {
|
|
14910
|
-
const ref = args[1];
|
|
14911
|
-
const title = args.slice(2).join(" ").trim();
|
|
14912
|
-
if (!ref || !title) {
|
|
14913
|
-
ctx.send({ type: "error", message: "Usage: /branch rename <id|title> <new title>" });
|
|
14914
|
-
}
|
|
14915
|
-
const id = resolve15(ref);
|
|
14916
|
-
if (!id) {
|
|
14917
|
-
}
|
|
14918
|
-
const ok = session.renameBranch(id, title);
|
|
14919
|
-
if (ok) {
|
|
14920
|
-
await ctx.sessions.save();
|
|
14921
|
-
ctx.send({ type: "info", message: `\u2713 Renamed branch "${id}" \u2192 "${title}"` });
|
|
14922
|
-
ctx.sendStatus();
|
|
14923
|
-
} else {
|
|
14924
|
-
ctx.send({ type: "error", message: `Branch "${id}" not found.` });
|
|
14925
|
-
}
|
|
14926
|
-
}
|
|
14927
|
-
if (sub === "diff") {
|
|
14928
|
-
const ref = args[1];
|
|
14929
|
-
if (!ref) {
|
|
14930
|
-
ctx.send({ type: "error", message: "Usage: /branch diff <id|title>" });
|
|
14931
|
-
}
|
|
14932
|
-
const id = resolve15(ref);
|
|
14933
|
-
if (!id) {
|
|
14934
|
-
}
|
|
14935
|
-
const d = session.diffBranches(id);
|
|
14936
|
-
if (!d) {
|
|
14937
|
-
ctx.send({ type: "error", message: `Branch "${id}" not found.` });
|
|
14938
|
-
}
|
|
14939
|
-
const preview = (m) => {
|
|
14940
|
-
const text = typeof m.content === "string" ? m.content : "[complex content]";
|
|
14941
|
-
return text.replace(/\s+/g, " ").trim().slice(0, 70);
|
|
14942
|
-
};
|
|
14943
|
-
const lines = [
|
|
14944
|
-
`\u{1F500} diff ${id} (source) \u2194 ${session.activeBranchId} (active)`,
|
|
14945
|
-
`Common prefix: ${d.commonPrefix} message(s)`
|
|
14946
|
-
];
|
|
14947
|
-
if (d.sourceOnly.length === 0 && d.targetOnly.length === 0) {
|
|
14948
|
-
lines.push("Branches are identical.");
|
|
14949
|
-
} else {
|
|
14950
|
-
if (d.sourceOnly.length > 0) {
|
|
14951
|
-
lines.push("", `Only in ${id} (${d.sourceOnly.length}):`);
|
|
14952
|
-
d.sourceOnly.forEach(
|
|
14953
|
-
(m, i) => lines.push(` [${d.commonPrefix + i}] ${m.role}: ${preview(m)}`)
|
|
14954
|
-
);
|
|
14955
|
-
}
|
|
14956
|
-
if (d.targetOnly.length > 0) {
|
|
14957
|
-
lines.push("", `Only in ${session.activeBranchId} (active) (${d.targetOnly.length}):`);
|
|
14958
|
-
d.targetOnly.forEach(
|
|
14959
|
-
(m, i) => lines.push(` [${d.commonPrefix + i}] ${m.role}: ${preview(m)}`)
|
|
14960
|
-
);
|
|
14961
|
-
}
|
|
14962
|
-
lines.push("", `Use /branch cherry-pick ${id} <index> to copy a message into the active branch.`);
|
|
14963
|
-
}
|
|
14964
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
14965
|
-
}
|
|
14966
|
-
if (sub === "cherry-pick") {
|
|
14967
|
-
const ref = args[1];
|
|
14968
|
-
const idxStr = args[2];
|
|
14969
|
-
if (!ref || !idxStr) {
|
|
14970
|
-
ctx.send({ type: "error", message: "Usage: /branch cherry-pick <source-id|title> <msg-index>" });
|
|
14971
|
-
}
|
|
14972
|
-
const id = resolve15(ref);
|
|
14973
|
-
if (!id) {
|
|
14974
|
-
}
|
|
14975
|
-
const idx = parseInt(idxStr, 10);
|
|
14976
|
-
if (Number.isNaN(idx)) {
|
|
14977
|
-
ctx.send({ type: "error", message: `Invalid message index: "${idxStr}"` });
|
|
14978
|
-
}
|
|
14979
|
-
const picked = session.cherryPickMessage(id, idx);
|
|
14980
|
-
if (!picked) {
|
|
14981
|
-
ctx.send({ type: "error", message: `Cherry-pick failed \u2014 index ${idx} out of range for branch "${id}".` });
|
|
14982
|
-
}
|
|
14983
|
-
await ctx.sessions.save();
|
|
14984
|
-
const preview = typeof picked.content === "string" ? picked.content.replace(/\s+/g, " ").trim().slice(0, 60) : "[complex content]";
|
|
14985
|
-
ctx.send({ type: "info", message: `\u2713 Cherry-picked ${picked.role} message from "${id}"[${idx}] \u2192 active branch (${session.messages.length} msgs): ${preview}` });
|
|
14986
|
-
ctx.sendSessionMessages();
|
|
14987
|
-
ctx.sendStatus();
|
|
14705
|
+
function dirSize(path3) {
|
|
14706
|
+
try {
|
|
14707
|
+
const st = statSync10(path3);
|
|
14708
|
+
if (st.isFile()) return st.size;
|
|
14709
|
+
if (!st.isDirectory()) return 0;
|
|
14710
|
+
let sum = 0;
|
|
14711
|
+
for (const name of readdirSync12(path3)) {
|
|
14712
|
+
sum += dirSize(join22(path3, name));
|
|
14988
14713
|
}
|
|
14989
|
-
|
|
14714
|
+
return sum;
|
|
14715
|
+
} catch {
|
|
14716
|
+
return 0;
|
|
14990
14717
|
}
|
|
14991
14718
|
}
|
|
14992
|
-
|
|
14993
|
-
|
|
14994
|
-
|
|
14995
|
-
|
|
14996
|
-
|
|
14997
|
-
|
|
14998
|
-
const sub = args.join(" ").trim();
|
|
14999
|
-
let messageCount = session.messages.length;
|
|
15000
|
-
let fromLabel = "current position";
|
|
15001
|
-
if (sub) {
|
|
15002
|
-
const cp = session.checkpoints.find((c) => c.name === sub);
|
|
15003
|
-
if (!cp) {
|
|
15004
|
-
const available = session.checkpoints.map((c) => c.name);
|
|
15005
|
-
ctx.send({ type: "error", message: available.length > 0 ? `Checkpoint "${sub}" not found. Available: ${available.join(", ")}` : `Checkpoint "${sub}" not found. No checkpoints saved.` });
|
|
15006
|
-
}
|
|
15007
|
-
messageCount = cp.messageIndex;
|
|
15008
|
-
fromLabel = `checkpoint "${cp.name}" (${cp.messageIndex} messages)`;
|
|
15009
|
-
}
|
|
14719
|
+
|
|
14720
|
+
// src/diagnostics/doctor-report.ts
|
|
14721
|
+
function checkFile(label, path3) {
|
|
14722
|
+
const exists = existsSync28(path3);
|
|
14723
|
+
let sizeBytes = null;
|
|
14724
|
+
if (exists) {
|
|
15010
14725
|
try {
|
|
15011
|
-
|
|
15012
|
-
|
|
15013
|
-
ctx.send({ type: "info", message: `\u{1F500} Session Forked
|
|
15014
|
-
Original: ${originalId}
|
|
15015
|
-
Forked: ${forked.id.slice(0, 8)} "${forked.title ?? "(untitled)"}"
|
|
15016
|
-
From: ${fromLabel}
|
|
15017
|
-
Messages: ${forked.messages.length} copied` });
|
|
15018
|
-
ctx.sendSessionMessages();
|
|
15019
|
-
ctx.sendStatus();
|
|
15020
|
-
ctx.sendSessionList();
|
|
15021
|
-
} catch (err) {
|
|
15022
|
-
ctx.send({ type: "error", message: `Fork failed: ${err.message}` });
|
|
14726
|
+
sizeBytes = statSync11(path3).size;
|
|
14727
|
+
} catch {
|
|
15023
14728
|
}
|
|
15024
14729
|
}
|
|
14730
|
+
return { label, path: path3, exists, sizeBytes };
|
|
15025
14731
|
}
|
|
15026
|
-
|
|
15027
|
-
|
|
15028
|
-
|
|
15029
|
-
|
|
15030
|
-
|
|
15031
|
-
|
|
15032
|
-
|
|
15033
|
-
|
|
15034
|
-
|
|
15035
|
-
|
|
15036
|
-
|
|
15037
|
-
|
|
15038
|
-
|
|
15039
|
-
|
|
15040
|
-
|
|
15041
|
-
|
|
15042
|
-
|
|
15043
|
-
|
|
15044
|
-
|
|
15045
|
-
|
|
15046
|
-
|
|
15047
|
-
|
|
15048
|
-
|
|
15049
|
-
|
|
15050
|
-
|
|
15051
|
-
|
|
15052
|
-
|
|
15053
|
-
|
|
15054
|
-
|
|
15055
|
-
|
|
15056
|
-
|
|
15057
|
-
|
|
15058
|
-
|
|
15059
|
-
|
|
15060
|
-
|
|
15061
|
-
|
|
15062
|
-
|
|
14732
|
+
function buildDoctorReport(options) {
|
|
14733
|
+
const config = options.config;
|
|
14734
|
+
const cwd = options.cwd ?? process.cwd();
|
|
14735
|
+
const configDir = config.getConfigDir();
|
|
14736
|
+
const projectRoot = getGitRoot(cwd) ?? cwd;
|
|
14737
|
+
const contextCfg = config.get("context");
|
|
14738
|
+
const contextSetting = config.get("contextFile");
|
|
14739
|
+
const context = loadContextFiles({
|
|
14740
|
+
cwd,
|
|
14741
|
+
configDir,
|
|
14742
|
+
projectRoot,
|
|
14743
|
+
setting: contextSetting,
|
|
14744
|
+
maxBytes: contextCfg.projectDocMaxBytes,
|
|
14745
|
+
fallbackFilenames: contextCfg.projectDocFallbackFilenames
|
|
14746
|
+
});
|
|
14747
|
+
const hooksConfig = config.get("hooks");
|
|
14748
|
+
const hooks = listHooks(hooksConfig, configDir);
|
|
14749
|
+
const pendingHooks = getPendingHookTrust(hooksConfig, configDir);
|
|
14750
|
+
const plugins = listInstalledPlugins(configDir);
|
|
14751
|
+
const activeAssets = getActivePluginAssets(configDir);
|
|
14752
|
+
const network = config.get("networkPolicy");
|
|
14753
|
+
const mcpServers = config.get("mcpServers");
|
|
14754
|
+
const allStats = getStatsSnapshot();
|
|
14755
|
+
return {
|
|
14756
|
+
version: VERSION,
|
|
14757
|
+
npmCheck: options.npmCheck,
|
|
14758
|
+
node: process.version,
|
|
14759
|
+
platform: `${platform6()} ${release2()} (${arch2()})`,
|
|
14760
|
+
cwd,
|
|
14761
|
+
projectRoot,
|
|
14762
|
+
configDir,
|
|
14763
|
+
providers: options.providers.listAll().map((p) => ({ id: p.id, displayName: p.displayName, configured: p.configured })),
|
|
14764
|
+
files: [
|
|
14765
|
+
checkFile("config.json", join23(configDir, "config.json")),
|
|
14766
|
+
checkFile("memory.md", join23(configDir, MEMORY_FILE_NAME)),
|
|
14767
|
+
checkFile("memory.jsonl", join23(configDir, "memory.jsonl")),
|
|
14768
|
+
checkFile("dev-state.md", join23(configDir, DEV_STATE_FILE_NAME)),
|
|
14769
|
+
checkFile("hooks-trust.json", join23(configDir, "hooks-trust.json")),
|
|
14770
|
+
checkFile("plugin-state.json", join23(configDir, "plugins", "plugin-state.json"))
|
|
14771
|
+
],
|
|
14772
|
+
context: {
|
|
14773
|
+
enabled: contextSetting !== false,
|
|
14774
|
+
setting: contextSetting,
|
|
14775
|
+
candidates: context.candidates,
|
|
14776
|
+
maxBytes: context.maxBytes,
|
|
14777
|
+
layers: context.layers.map((l) => ({ level: l.level, fileName: l.fileName, displayPath: l.displayPath, bytes: l.byteCount, chars: l.charCount, truncated: l.truncated })),
|
|
14778
|
+
skipped: context.skipped.map((s) => ({ level: s.level, fileName: s.fileName, displayPath: s.displayPath, reason: s.reason, message: s.message }))
|
|
14779
|
+
},
|
|
14780
|
+
permissions: {
|
|
14781
|
+
defaultProfile: config.get("defaultPermissionProfile"),
|
|
14782
|
+
allowedProfiles: config.get("allowedPermissionProfiles"),
|
|
14783
|
+
customProfileCount: Object.keys(config.get("permissionProfiles")).length,
|
|
14784
|
+
legacyDefaultAction: config.get("defaultPermission"),
|
|
14785
|
+
ruleCount: config.get("permissionRules").length
|
|
14786
|
+
},
|
|
14787
|
+
networkPolicy: {
|
|
14788
|
+
enabled: network.enabled,
|
|
14789
|
+
defaultAction: network.defaultAction,
|
|
14790
|
+
allowDomains: network.allowDomains.length,
|
|
14791
|
+
denyDomains: network.denyDomains.length,
|
|
14792
|
+
allowPorts: network.allowPorts.length,
|
|
14793
|
+
denyPorts: network.denyPorts.length,
|
|
14794
|
+
allowPrivateNetwork: network.allowPrivateNetwork,
|
|
14795
|
+
toolOverrides: Object.fromEntries(Object.entries(network.tools).filter(([, v]) => v !== void 0))
|
|
14796
|
+
},
|
|
14797
|
+
hooks: {
|
|
14798
|
+
enabled: hooksConfig?.enabled !== false,
|
|
14799
|
+
total: hooks.length,
|
|
14800
|
+
project: hooks.filter((h) => h.source === "project").length,
|
|
14801
|
+
pendingTrust: pendingHooks.length,
|
|
14802
|
+
disabled: hooks.filter((h) => h.disabled).length
|
|
14803
|
+
},
|
|
14804
|
+
plugins: {
|
|
14805
|
+
root: join23(configDir, "plugins"),
|
|
14806
|
+
installed: plugins.length,
|
|
14807
|
+
active: activeAssets.plugins.length,
|
|
14808
|
+
invalid: plugins.filter((p) => !p.valid).length,
|
|
14809
|
+
untrusted: plugins.filter((p) => p.valid && !p.trusted).length,
|
|
14810
|
+
disabled: plugins.filter((p) => p.valid && !p.enabled).length,
|
|
14811
|
+
mcpServers: Object.keys(activeAssets.mcpServers).length,
|
|
14812
|
+
skillDirs: activeAssets.skillDirs.length,
|
|
14813
|
+
commandDirs: activeAssets.commandDirs.length,
|
|
14814
|
+
agentDirs: activeAssets.agentDirs.length,
|
|
14815
|
+
entries: plugins.map((p) => ({ name: p.name, version: p.manifest.version, enabled: p.enabled, trusted: p.trusted, valid: p.valid, error: p.error }))
|
|
14816
|
+
},
|
|
14817
|
+
mcp: {
|
|
14818
|
+
enabled: config.get("mcpEnabled"),
|
|
14819
|
+
configured: Object.keys(mcpServers).length,
|
|
14820
|
+
projectConfigExists: existsSync28(join23(projectRoot, MCP_PROJECT_CONFIG_NAME)),
|
|
14821
|
+
statuses: options.mcpStatuses ?? []
|
|
14822
|
+
},
|
|
14823
|
+
recentCrashes: listRecentCrashes(5, configDir),
|
|
14824
|
+
diskUsage: getConfigDirUsage(configDir),
|
|
14825
|
+
toolStats: {
|
|
14826
|
+
totalCalls: allStats.reduce((a, b) => a + b.calls, 0),
|
|
14827
|
+
totalFailures: allStats.reduce((a, b) => a + b.failures, 0),
|
|
14828
|
+
topUsed: getTopUsedTools(5),
|
|
14829
|
+
topFailing: getTopFailingTools(5)
|
|
15063
14830
|
}
|
|
15064
|
-
|
|
15065
|
-
ctx.sendSessionMessages();
|
|
15066
|
-
ctx.sendStatus();
|
|
15067
|
-
}
|
|
14831
|
+
};
|
|
15068
14832
|
}
|
|
15069
14833
|
|
|
15070
|
-
// src/
|
|
15071
|
-
|
|
15072
|
-
|
|
15073
|
-
|
|
15074
|
-
|
|
14834
|
+
// src/diagnostics/doctor-cli.ts
|
|
14835
|
+
function formatBytes(n) {
|
|
14836
|
+
if (n < 1024) return `${n} B`;
|
|
14837
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
14838
|
+
if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
|
14839
|
+
return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
|
15075
14840
|
}
|
|
15076
|
-
|
|
15077
|
-
{
|
|
15078
|
-
|
|
15079
|
-
|
|
15080
|
-
|
|
15081
|
-
|
|
15082
|
-
|
|
15083
|
-
|
|
15084
|
-
|
|
15085
|
-
|
|
15086
|
-
|
|
15087
|
-
|
|
15088
|
-
|
|
15089
|
-
|
|
15090
|
-
|
|
15091
|
-
|
|
15092
|
-
|
|
15093
|
-
|
|
15094
|
-
|
|
15095
|
-
}
|
|
14841
|
+
function formatDoctorReport(report, ansi = true) {
|
|
14842
|
+
const wrap = (code, s) => ansi ? `${code}${s}\x1B[0m` : s;
|
|
14843
|
+
const B = (s) => wrap("\x1B[1m", s);
|
|
14844
|
+
const D = (s) => wrap("\x1B[2m", s);
|
|
14845
|
+
const G = (s) => wrap("\x1B[32m", s);
|
|
14846
|
+
const Y = (s) => wrap("\x1B[33m", s);
|
|
14847
|
+
const R = (s) => wrap("\x1B[31m", s);
|
|
14848
|
+
const out = [];
|
|
14849
|
+
out.push("");
|
|
14850
|
+
out.push(B("\u{1FA7A} AI-CLI Health Check"));
|
|
14851
|
+
out.push("");
|
|
14852
|
+
const nc = report.npmCheck;
|
|
14853
|
+
let versionLine = ` version: ${report.version}`;
|
|
14854
|
+
if (nc?.status === "up-to-date") versionLine += ` ${D("(latest on npm)")}`;
|
|
14855
|
+
else if (nc?.status === "outdated" && nc.latest) versionLine += ` ${Y(`\u2192 ${nc.latest} available`)} ${D("(npm i -g jinzd-ai-cli@latest)")}`;
|
|
14856
|
+
else if (nc?.status === "skipped") versionLine += ` ${D(`(npm check skipped: ${nc.reason ?? "unknown"})`)}`;
|
|
14857
|
+
out.push(versionLine);
|
|
14858
|
+
out.push(` node: ${report.node}`);
|
|
14859
|
+
out.push(` platform: ${report.platform}`);
|
|
14860
|
+
out.push(` cwd: ${report.cwd}`);
|
|
14861
|
+
out.push(` project: ${report.projectRoot}`);
|
|
14862
|
+
out.push(` config: ${report.configDir}`);
|
|
14863
|
+
out.push("");
|
|
14864
|
+
out.push(B("API Keys:"));
|
|
14865
|
+
for (const p of report.providers) {
|
|
14866
|
+
out.push(` ${p.configured ? G("\u2713") : D("\u25CB")} ${p.id.padEnd(14)} ${p.configured ? G("configured") : D("not configured")}`);
|
|
14867
|
+
}
|
|
14868
|
+
out.push("");
|
|
14869
|
+
out.push(B("Context Files:"));
|
|
14870
|
+
out.push(` status: ${report.context.enabled ? G("enabled") : D("disabled")} \xB7 max ${formatBytes(report.context.maxBytes)}`);
|
|
14871
|
+
if (report.context.layers.length === 0) out.push(` ${D("(no context files loaded)")}`);
|
|
14872
|
+
for (const l of report.context.layers) {
|
|
14873
|
+
out.push(` ${G("\u2713")} ${l.level.padEnd(7)} ${l.displayPath} ${D(`(${formatBytes(l.bytes)}${l.truncated ? ", truncated" : ""})`)}`);
|
|
14874
|
+
}
|
|
14875
|
+
if (report.context.skipped.length > 0) {
|
|
14876
|
+
out.push(` ${D("skipped:")}`);
|
|
14877
|
+
for (const s of report.context.skipped.slice(0, 5)) {
|
|
14878
|
+
out.push(` ${Y("!")} ${s.level}:${s.fileName} ${s.reason}${s.message ? ` \xB7 ${s.message}` : ""}`);
|
|
15096
14879
|
}
|
|
15097
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
15098
14880
|
}
|
|
14881
|
+
out.push("");
|
|
14882
|
+
out.push(B("Permissions / Network:"));
|
|
14883
|
+
out.push(` profile: ${report.permissions.defaultProfile} \xB7 allowed ${report.permissions.allowedProfiles.join(", ")} \xB7 rules ${report.permissions.ruleCount} \xB7 custom profiles ${report.permissions.customProfileCount}`);
|
|
14884
|
+
const np = report.networkPolicy;
|
|
14885
|
+
out.push(` networkPolicy: ${np.enabled ? G("enabled") : D("disabled")} \xB7 default ${np.defaultAction} \xB7 allow ${np.allowDomains} domain(s)/${np.allowPorts} port(s) \xB7 deny ${np.denyDomains} domain(s)/${np.denyPorts} port(s) \xB7 private ${np.allowPrivateNetwork ? "allow" : "block/confirm"}`);
|
|
14886
|
+
const overrides = Object.entries(np.toolOverrides).map(([k, v]) => `${k}=${v}`).join(", ");
|
|
14887
|
+
if (overrides) out.push(` tool overrides: ${overrides}`);
|
|
14888
|
+
out.push("");
|
|
14889
|
+
out.push(B("Hooks / Plugins:"));
|
|
14890
|
+
out.push(` hooks: ${report.hooks.enabled ? G("enabled") : D("disabled")} \xB7 total ${report.hooks.total} \xB7 project ${report.hooks.project} \xB7 pending trust ${report.hooks.pendingTrust} \xB7 disabled ${report.hooks.disabled}`);
|
|
14891
|
+
out.push(` plugins: installed ${report.plugins.installed} \xB7 active ${report.plugins.active} \xB7 disabled ${report.plugins.disabled} \xB7 untrusted ${report.plugins.untrusted} \xB7 invalid ${report.plugins.invalid}`);
|
|
14892
|
+
out.push(` plugin assets: ${report.plugins.skillDirs} skill dir(s), ${report.plugins.commandDirs} command dir(s), ${report.plugins.agentDirs} agent dir(s), ${report.plugins.mcpServers} MCP server(s)`);
|
|
14893
|
+
if (report.plugins.entries.length > 0) {
|
|
14894
|
+
for (const p of report.plugins.entries.slice(0, 5)) {
|
|
14895
|
+
const state2 = p.valid ? `${p.enabled ? "enabled" : "disabled"} \xB7 ${p.trusted ? "trusted" : "untrusted"}` : `invalid \xB7 ${p.error ?? ""}`;
|
|
14896
|
+
out.push(` ${p.valid ? G("\u2713") : R("\u2717")} ${p.name}@${p.version} ${D(state2)}`);
|
|
14897
|
+
}
|
|
14898
|
+
}
|
|
14899
|
+
out.push("");
|
|
14900
|
+
out.push(B("MCP Servers:"));
|
|
14901
|
+
out.push(` global configured: ${report.mcp.configured} \xB7 project .mcp.json: ${report.mcp.projectConfigExists ? "yes" : "no"} \xB7 mcpEnabled: ${report.mcp.enabled ? "true" : "false"}`);
|
|
14902
|
+
if (report.mcp.statuses.length === 0) out.push(` ${D("(no connected manager status in this mode)")}`);
|
|
14903
|
+
for (const s of report.mcp.statuses) {
|
|
14904
|
+
const state2 = s.connected ? `${G("connected")} \xB7 ${s.serverName} \xB7 ${s.toolCount} tools` : `${R("disconnected")}${s.error ? D(` \xB7 ${s.error}`) : ""}`;
|
|
14905
|
+
out.push(` ${s.connected ? G("\u2713") : R("\u2717")} ${s.serverId.padEnd(16)} ${state2}`);
|
|
14906
|
+
}
|
|
14907
|
+
out.push("");
|
|
14908
|
+
out.push(B("Config Files:"));
|
|
14909
|
+
for (const f of report.files) {
|
|
14910
|
+
const extra = f.exists && f.sizeBytes !== null ? ` ${D(`(${formatBytes(f.sizeBytes)})`)}` : "";
|
|
14911
|
+
out.push(` ${f.exists ? G("\u2713") : D("\u2013")} ${f.label.padEnd(16)} ${f.exists ? f.path : D("(not found)")}${extra}`);
|
|
14912
|
+
}
|
|
14913
|
+
out.push("");
|
|
14914
|
+
out.push(B("Recent Crashes (last 5):"));
|
|
14915
|
+
if (report.recentCrashes.length === 0) out.push(` ${D("(none - clean!)")}`);
|
|
14916
|
+
for (const c of report.recentCrashes) {
|
|
14917
|
+
out.push(` ${R("\u2717")} ${c.timestamp} ${Y(c.kind)}`);
|
|
14918
|
+
out.push(` ${D(c.message)}`);
|
|
14919
|
+
out.push(` ${D(c.file)}`);
|
|
14920
|
+
}
|
|
14921
|
+
out.push("");
|
|
14922
|
+
const ts = report.toolStats;
|
|
14923
|
+
out.push(B("Tool Usage:"));
|
|
14924
|
+
out.push(` total: ${ts.totalCalls} calls \xB7 ${ts.totalFailures} failures (${ts.totalCalls ? (ts.totalFailures * 100 / ts.totalCalls).toFixed(1) : "0.0"}%)`);
|
|
14925
|
+
if (ts.topUsed.length > 0) {
|
|
14926
|
+
out.push(` ${D("top used:")}`);
|
|
14927
|
+
for (const t of ts.topUsed) out.push(` ${t.name.padEnd(20)} ${String(t.calls).padStart(6)} calls \xB7 avg ${t.calls ? (t.totalDurationMs / t.calls).toFixed(0) : "0"}ms`);
|
|
14928
|
+
}
|
|
14929
|
+
if (ts.topFailing.length > 0) {
|
|
14930
|
+
out.push(` ${D("top failing:")}`);
|
|
14931
|
+
for (const t of ts.topFailing) out.push(` ${R(t.name.padEnd(20))} ${t.failures}/${t.calls} (${(t.failures * 100 / Math.max(1, t.calls)).toFixed(1)}%) ${D(t.lastFailureMessage ?? "")}`.slice(0, 200));
|
|
14932
|
+
}
|
|
14933
|
+
out.push("");
|
|
14934
|
+
const dirAnnotations = {
|
|
14935
|
+
models: "semantic search model -> /index semantic-clear",
|
|
14936
|
+
index: "symbol index -> /index clear",
|
|
14937
|
+
history: "conversation history",
|
|
14938
|
+
logs: "crash logs",
|
|
14939
|
+
"memory-index": "chat memory index -> /memory index-clear",
|
|
14940
|
+
plugins: "installed package plugins"
|
|
14941
|
+
};
|
|
14942
|
+
out.push(B("Disk Usage (~/.aicli):"));
|
|
14943
|
+
out.push(` total: ${formatBytes(report.diskUsage.totalBytes)}`);
|
|
14944
|
+
for (const e of report.diskUsage.entries.slice(0, 10)) {
|
|
14945
|
+
const note = dirAnnotations[e.name];
|
|
14946
|
+
out.push(` ${e.name.padEnd(18)} ${formatBytes(e.bytes)}${note ? ` ${D(`\xB7 ${note}`)}` : ""}`);
|
|
14947
|
+
}
|
|
14948
|
+
out.push("");
|
|
14949
|
+
out.push(G("\u2713 Health check complete"));
|
|
14950
|
+
out.push("");
|
|
14951
|
+
return out.join("\n");
|
|
15099
14952
|
}
|
|
15100
|
-
|
|
15101
|
-
|
|
15102
|
-
|
|
15103
|
-
|
|
15104
|
-
|
|
15105
|
-
|
|
15106
|
-
|
|
15107
|
-
|
|
15108
|
-
|
|
15109
|
-
|
|
15110
|
-
|
|
15111
|
-
}
|
|
15112
|
-
if (sub === "install") {
|
|
15113
|
-
const input = args[1];
|
|
15114
|
-
if (!input) throw new Error("Usage: /plugin install <path>");
|
|
15115
|
-
const plugin = installPlugin(configDir, input);
|
|
15116
|
-
ctx.send({ type: "info", message: `Installed plugin ${plugin.name}@${plugin.manifest.version}. Run /plugin trust ${plugin.name}, then /plugin enable ${plugin.name}.` });
|
|
15117
|
-
}
|
|
15118
|
-
if (sub === "trust") {
|
|
15119
|
-
const name = args[1];
|
|
15120
|
-
if (!name) throw new Error("Usage: /plugin trust <name>");
|
|
15121
|
-
const state2 = trustPlugin(configDir, name);
|
|
15122
|
-
ctx.send({ type: "info", message: `Trusted plugin ${state2.name}.` });
|
|
15123
|
-
}
|
|
15124
|
-
if (sub === "enable" || sub === "disable") {
|
|
15125
|
-
const name = args[1];
|
|
15126
|
-
if (!name) throw new Error(`Usage: /plugin ${sub} <name>`);
|
|
15127
|
-
const state2 = setPluginEnabled(configDir, name, sub === "enable");
|
|
15128
|
-
ctx.send({ type: "info", message: `${sub === "enable" ? "Enabled" : "Disabled"} plugin ${state2.name}. Restart Web UI for changes to take effect.` });
|
|
15129
|
-
}
|
|
15130
|
-
if (sub === "inspect") {
|
|
15131
|
-
const name = args[1];
|
|
15132
|
-
if (!name) throw new Error("Usage: /plugin inspect <name>");
|
|
15133
|
-
ctx.send({ type: "info", message: describePlugin(getInstalledPlugin(configDir, name)).join("\n") });
|
|
15134
|
-
}
|
|
15135
|
-
throw new Error("Usage: /plugin [list|install|enable|disable|inspect|trust]");
|
|
15136
|
-
} catch (err) {
|
|
15137
|
-
ctx.send({ type: "error", message: err instanceof Error ? err.message : String(err) });
|
|
14953
|
+
|
|
14954
|
+
// src/web/commands/_shared.ts
|
|
14955
|
+
function lastAssistantText(messages) {
|
|
14956
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
14957
|
+
const m = messages[i];
|
|
14958
|
+
if (!m || m.role !== "assistant") continue;
|
|
14959
|
+
const c = m.content;
|
|
14960
|
+
if (typeof c === "string") return c;
|
|
14961
|
+
if (Array.isArray(c)) {
|
|
14962
|
+
const text = c.filter((p) => p && p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
|
|
14963
|
+
if (text) return text;
|
|
15138
14964
|
}
|
|
14965
|
+
return void 0;
|
|
15139
14966
|
}
|
|
14967
|
+
return void 0;
|
|
15140
14968
|
}
|
|
15141
|
-
|
|
15142
|
-
|
|
15143
|
-
|
|
15144
|
-
|
|
14969
|
+
|
|
14970
|
+
// src/web/commands/session-commands.ts
|
|
14971
|
+
async function handleClear(_args, ctx) {
|
|
14972
|
+
ctx.saveIfNeeded();
|
|
14973
|
+
ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
|
|
14974
|
+
ctx.resetWebSessionUsage();
|
|
14975
|
+
ctx.send({ type: "info", message: "Conversation cleared." });
|
|
14976
|
+
ctx.sendStatus();
|
|
14977
|
+
ctx.sendSessionList();
|
|
14978
|
+
}
|
|
14979
|
+
async function handleCompact(args, ctx) {
|
|
14980
|
+
await ctx.compactSession(args.join(" ") || void 0);
|
|
14981
|
+
}
|
|
14982
|
+
async function handleStatus(_args, ctx) {
|
|
14983
|
+
const session = ctx.sessions.current;
|
|
14984
|
+
const cacheRead = ctx.sessionTokenUsage.cacheReadTokens;
|
|
14985
|
+
const cacheCreate = ctx.sessionTokenUsage.cacheCreationTokens;
|
|
14986
|
+
const cost = computeCost(ctx.currentProvider, ctx.currentModel, ctx.sessionTokenUsage);
|
|
14987
|
+
const cacheLine = cacheRead > 0 || cacheCreate > 0 ? `
|
|
14988
|
+
Cache: write=${cacheCreate} read=${cacheRead}` : "";
|
|
14989
|
+
const costLine = cost != null ? `
|
|
14990
|
+
Cost: ${formatCost(cost)}` : "";
|
|
14991
|
+
ctx.send({
|
|
14992
|
+
type: "info",
|
|
14993
|
+
message: `Provider: ${ctx.currentProvider}
|
|
14994
|
+
Model: ${ctx.currentModel}
|
|
14995
|
+
Session: ${session?.id ?? "none"} (${session?.messages.length ?? 0} messages)
|
|
14996
|
+
Tokens: in=${ctx.sessionTokenUsage.inputTokens} out=${ctx.sessionTokenUsage.outputTokens}${cacheLine}${costLine}`
|
|
14997
|
+
});
|
|
14998
|
+
}
|
|
14999
|
+
async function handleSession(args, ctx) {
|
|
15000
|
+
const sub = args[0];
|
|
15001
|
+
if (sub === "new") {
|
|
15002
|
+
ctx.saveIfNeeded();
|
|
15003
|
+
const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
|
|
15004
|
+
ctx.unsavedSessions.set(created.id, created);
|
|
15005
|
+
ctx.resetWebSessionUsage();
|
|
15006
|
+
ctx.send({ type: "info", message: "New session created." });
|
|
15007
|
+
ctx.sendStatus();
|
|
15008
|
+
ctx.sendSessionList();
|
|
15009
|
+
return;
|
|
15010
|
+
}
|
|
15011
|
+
if (sub === "load" && args[1]) {
|
|
15012
|
+
const targetId = args[1];
|
|
15013
|
+
ctx.saveIfNeeded();
|
|
15014
|
+
const cachedExact = ctx.unsavedSessions.get(targetId);
|
|
15015
|
+
const cached = cachedExact ?? [...ctx.unsavedSessions.values()].find((s) => s.id.startsWith(targetId));
|
|
15016
|
+
if (cached) {
|
|
15017
|
+
ctx.sessions.setCurrent(cached);
|
|
15018
|
+
ctx.resetWebSessionUsage();
|
|
15019
|
+
ctx.send({
|
|
15020
|
+
type: "info",
|
|
15021
|
+
message: `Loaded session: ${cached.id.slice(0, 8)} "${cached.title ?? ""}" (${cached.messages.length} messages)`
|
|
15022
|
+
});
|
|
15023
|
+
ctx.sendSessionMessages();
|
|
15024
|
+
ctx.sendStatus();
|
|
15025
|
+
ctx.sendSessionList();
|
|
15026
|
+
return;
|
|
15145
15027
|
}
|
|
15146
|
-
const
|
|
15147
|
-
|
|
15148
|
-
|
|
15149
|
-
|
|
15150
|
-
|
|
15151
|
-
|
|
15152
|
-
|
|
15153
|
-
|
|
15028
|
+
const list = ctx.sessions.listSessions();
|
|
15029
|
+
const found = list.find((s) => s.id.startsWith(targetId));
|
|
15030
|
+
if (found) {
|
|
15031
|
+
ctx.sessions.loadSession(found.id);
|
|
15032
|
+
ctx.resetWebSessionUsage();
|
|
15033
|
+
ctx.send({ type: "info", message: `Loaded session: ${found.id.slice(0, 8)} "${found.title ?? ""}" (${found.messageCount} messages)` });
|
|
15034
|
+
const loadedSession = ctx.sessions.current;
|
|
15035
|
+
if (loadedSession && loadedSession.messages.length > 0) {
|
|
15036
|
+
const lastMsg = loadedSession.messages[loadedSession.messages.length - 1];
|
|
15037
|
+
const isIncomplete = lastMsg.role === "tool" || lastMsg.role === "assistant" && lastMsg.toolCalls && lastMsg.toolCalls.length > 0;
|
|
15038
|
+
if (isIncomplete) {
|
|
15039
|
+
ctx.send({
|
|
15040
|
+
type: "info",
|
|
15041
|
+
message: '\u26A0 This session appears to have been interrupted mid-task. The AI will see the tool history and can continue where it left off. Type "continue where you left off" to resume.'
|
|
15042
|
+
});
|
|
15043
|
+
}
|
|
15154
15044
|
}
|
|
15155
|
-
ctx.
|
|
15156
|
-
|
|
15157
|
-
|
|
15158
|
-
|
|
15159
|
-
ctx.send({ type: "info", message: "No MCP servers configured." });
|
|
15160
|
-
}
|
|
15161
|
-
const lines = [`\u{1F50C} **MCP Servers (${statuses.length}):**`, ""];
|
|
15162
|
-
for (const s of statuses) {
|
|
15163
|
-
const state2 = s.connected ? `\u2713 connected \xB7 ${s.serverName} \xB7 ${s.toolCount} tools` : `\u2717 disconnected${s.error ? ` \xB7 ${s.error}` : ""}`;
|
|
15164
|
-
lines.push(` ${s.serverId}: ${state2}`);
|
|
15045
|
+
ctx.sendSessionMessages();
|
|
15046
|
+
ctx.sendStatus();
|
|
15047
|
+
ctx.sendSessionList();
|
|
15048
|
+
return;
|
|
15165
15049
|
}
|
|
15166
|
-
|
|
15167
|
-
|
|
15168
|
-
ctx.
|
|
15050
|
+
const recreated = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
|
|
15051
|
+
ctx.unsavedSessions.set(recreated.id, recreated);
|
|
15052
|
+
ctx.resetWebSessionUsage();
|
|
15053
|
+
ctx.send({
|
|
15054
|
+
type: "info",
|
|
15055
|
+
message: `Previous session (${targetId.slice(0, 8)}) is no longer available \u2014 started a new one.`
|
|
15056
|
+
});
|
|
15057
|
+
ctx.sendStatus();
|
|
15058
|
+
ctx.sendSessionList();
|
|
15059
|
+
return;
|
|
15169
15060
|
}
|
|
15170
|
-
|
|
15171
|
-
|
|
15172
|
-
|
|
15173
|
-
|
|
15174
|
-
|
|
15175
|
-
|
|
15176
|
-
|
|
15177
|
-
if (
|
|
15178
|
-
|
|
15179
|
-
|
|
15180
|
-
|
|
15181
|
-
ctx.send({ type: "info", message: "No skills available. Add .md files to ~/.aicli/skills/" });
|
|
15182
|
-
} else {
|
|
15183
|
-
const lines = skills.map((s) => {
|
|
15184
|
-
const isActive = active?.meta.name === s.meta.name;
|
|
15185
|
-
const marker = isActive ? " \u2705 (active)" : "";
|
|
15186
|
-
return ` ${s.meta.name}${marker} \u2014 ${s.meta.description || "(no description)"}`;
|
|
15187
|
-
});
|
|
15188
|
-
ctx.send({ type: "info", message: `\u{1F4DA} Skills:
|
|
15189
|
-
${lines.join("\n")}` });
|
|
15061
|
+
if (sub === "list") {
|
|
15062
|
+
ctx.sendSessionList();
|
|
15063
|
+
return;
|
|
15064
|
+
}
|
|
15065
|
+
if (sub === "delete" && args[1]) {
|
|
15066
|
+
const targetId = args[1];
|
|
15067
|
+
const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
|
|
15068
|
+
if (cachedKey) {
|
|
15069
|
+
ctx.unsavedSessions.delete(cachedKey);
|
|
15070
|
+
if (ctx.sessions.current?.id === cachedKey) {
|
|
15071
|
+
ctx.sessions.clearCurrent();
|
|
15190
15072
|
}
|
|
15191
|
-
|
|
15192
|
-
ctx.
|
|
15193
|
-
|
|
15194
|
-
|
|
15195
|
-
|
|
15196
|
-
|
|
15197
|
-
|
|
15198
|
-
ctx.
|
|
15199
|
-
ctx.
|
|
15073
|
+
ctx.send({ type: "info", message: `Deleted session: ${cachedKey.slice(0, 8)}` });
|
|
15074
|
+
ctx.sendSessionList();
|
|
15075
|
+
return;
|
|
15076
|
+
}
|
|
15077
|
+
const list = ctx.sessions.listSessions();
|
|
15078
|
+
const found = list.find((s) => s.id.startsWith(targetId));
|
|
15079
|
+
if (found) {
|
|
15080
|
+
ctx.sessions.deleteSession(found.id);
|
|
15081
|
+
ctx.send({ type: "info", message: `Deleted session: ${found.id.slice(0, 8)}` });
|
|
15082
|
+
ctx.sendSessionList();
|
|
15200
15083
|
} else {
|
|
15201
|
-
|
|
15202
|
-
if (activated) {
|
|
15203
|
-
ctx.send({ type: "info", message: `\u2705 Skill activated: ${activated.meta.name}
|
|
15204
|
-
${activated.meta.description || ""}` });
|
|
15205
|
-
ctx.sendToolsList();
|
|
15206
|
-
ctx.sendStatus();
|
|
15207
|
-
} else {
|
|
15208
|
-
const available = ctx.skillManager.listSkills().map((s) => s.meta.name).join(", ");
|
|
15209
|
-
ctx.send({ type: "error", message: `Skill "${sub}" not found. Available: ${available}` });
|
|
15210
|
-
}
|
|
15084
|
+
ctx.send({ type: "error", message: `Session not found: ${targetId}` });
|
|
15211
15085
|
}
|
|
15086
|
+
return;
|
|
15212
15087
|
}
|
|
15213
|
-
|
|
15214
|
-
|
|
15215
|
-
|
|
15216
|
-
|
|
15217
|
-
|
|
15218
|
-
if (!existsSync27(commandsDir)) {
|
|
15219
|
-
ctx.send({ type: "info", message: `No custom commands directory.
|
|
15220
|
-
Create: ${commandsDir}/ with .md files.` });
|
|
15088
|
+
if (sub === "batch-delete") {
|
|
15089
|
+
const ids = args.slice(1).filter(Boolean);
|
|
15090
|
+
if (ids.length === 0) {
|
|
15091
|
+
ctx.send({ type: "error", message: "No session IDs provided." });
|
|
15092
|
+
return;
|
|
15221
15093
|
}
|
|
15222
|
-
|
|
15223
|
-
|
|
15224
|
-
|
|
15225
|
-
|
|
15226
|
-
|
|
15227
|
-
|
|
15228
|
-
|
|
15229
|
-
|
|
15230
|
-
|
|
15231
|
-
|
|
15232
|
-
|
|
15233
|
-
ctx.
|
|
15094
|
+
const list = ctx.sessions.listSessions();
|
|
15095
|
+
let deleted = 0;
|
|
15096
|
+
for (const targetId of ids) {
|
|
15097
|
+
const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
|
|
15098
|
+
if (cachedKey) {
|
|
15099
|
+
ctx.unsavedSessions.delete(cachedKey);
|
|
15100
|
+
deleted++;
|
|
15101
|
+
continue;
|
|
15102
|
+
}
|
|
15103
|
+
const found = list.find((s) => s.id.startsWith(targetId));
|
|
15104
|
+
if (found) {
|
|
15105
|
+
ctx.sessions.deleteSession(found.id);
|
|
15106
|
+
deleted++;
|
|
15234
15107
|
}
|
|
15235
|
-
} catch {
|
|
15236
|
-
ctx.send({ type: "info", message: `Cannot read: ${commandsDir}` });
|
|
15237
15108
|
}
|
|
15109
|
+
ctx.send({ type: "info", message: `Deleted ${deleted} session(s).` });
|
|
15110
|
+
ctx.sendSessionList();
|
|
15111
|
+
return;
|
|
15238
15112
|
}
|
|
15113
|
+
if (sub === "rename" && args[1] && args.length >= 3) {
|
|
15114
|
+
const targetId = args[1];
|
|
15115
|
+
const newTitle = args.slice(2).join(" ").trim();
|
|
15116
|
+
if (!newTitle) {
|
|
15117
|
+
ctx.send({ type: "error", message: "Title cannot be empty." });
|
|
15118
|
+
return;
|
|
15119
|
+
}
|
|
15120
|
+
const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
|
|
15121
|
+
if (cachedKey) {
|
|
15122
|
+
const session = ctx.unsavedSessions.get(cachedKey);
|
|
15123
|
+
session.title = newTitle;
|
|
15124
|
+
ctx.send({ type: "info", message: `Renamed session: "${newTitle}"` });
|
|
15125
|
+
ctx.sendSessionList();
|
|
15126
|
+
return;
|
|
15127
|
+
}
|
|
15128
|
+
const list = ctx.sessions.listSessions();
|
|
15129
|
+
const found = list.find((s) => s.id.startsWith(targetId));
|
|
15130
|
+
if (found) {
|
|
15131
|
+
const session = ctx.sessions.loadSession(found.id);
|
|
15132
|
+
session.title = newTitle;
|
|
15133
|
+
ctx.sessions.save();
|
|
15134
|
+
ctx.send({ type: "info", message: `Renamed session: "${newTitle}"` });
|
|
15135
|
+
ctx.sendSessionList();
|
|
15136
|
+
} else {
|
|
15137
|
+
ctx.send({ type: "error", message: `Session not found: ${targetId}` });
|
|
15138
|
+
}
|
|
15139
|
+
return;
|
|
15140
|
+
}
|
|
15141
|
+
ctx.send({ type: "info", message: "Usage: /session new | list | load <id> | delete <id> | rename <id> <title>" });
|
|
15239
15142
|
}
|
|
15240
|
-
|
|
15241
|
-
|
|
15242
|
-
|
|
15243
|
-
|
|
15244
|
-
|
|
15245
|
-
|
|
15246
|
-
|
|
15247
|
-
const
|
|
15248
|
-
|
|
15249
|
-
|
|
15250
|
-
|
|
15251
|
-
|
|
15252
|
-
|
|
15253
|
-
|
|
15254
|
-
|
|
15255
|
-
|
|
15256
|
-
|
|
15257
|
-
|
|
15258
|
-
|
|
15259
|
-
|
|
15260
|
-
|
|
15261
|
-
|
|
15262
|
-
|
|
15263
|
-
|
|
15264
|
-
|
|
15265
|
-
|
|
15266
|
-
|
|
15267
|
-
|
|
15268
|
-
}
|
|
15269
|
-
|
|
15270
|
-
|
|
15271
|
-
|
|
15272
|
-
|
|
15273
|
-
|
|
15274
|
-
|
|
15275
|
-
|
|
15276
|
-
|
|
15277
|
-
|
|
15278
|
-
|
|
15279
|
-
|
|
15280
|
-
6. **Dependencies** \u2014 known vulnerable packages, unsafe dynamic imports
|
|
15281
|
-
7. **File System** \u2014 path traversal, unsafe file permissions, symlink attacks
|
|
15282
|
-
8. **Network** \u2014 SSRF, insecure protocols, missing TLS validation
|
|
15283
|
-
|
|
15284
|
-
## Git Status
|
|
15285
|
-
${gitContextStr}
|
|
15286
|
-
|
|
15287
|
-
## Code Changes (diff)
|
|
15288
|
-
\`\`\`diff
|
|
15289
|
-
${diff}
|
|
15290
|
-
\`\`\`
|
|
15291
|
-
|
|
15292
|
-
## Output Format
|
|
15293
|
-
For each finding:
|
|
15294
|
-
- **Severity**: \u{1F534} CRITICAL / \u{1F7E0} HIGH / \u{1F7E1} MEDIUM / \u{1F535} LOW / \u2139\uFE0F INFO
|
|
15295
|
-
- **Category**: (from list above)
|
|
15296
|
-
- **File & location**: file:line
|
|
15297
|
-
- **Description**: what the vulnerability is and how it could be exploited
|
|
15298
|
-
- **Recommended fix**: specific code change to resolve
|
|
15299
|
-
|
|
15300
|
-
If no security issues found, state "\u2705 No security vulnerabilities detected" with a brief explanation of what was checked.`;
|
|
15301
|
-
}
|
|
15302
|
-
|
|
15303
|
-
// src/core/git-diff.ts
|
|
15304
|
-
import { execFileSync as execFileSync3 } from "child_process";
|
|
15305
|
-
function readGitDiff(options = {}) {
|
|
15306
|
-
const args = ["diff"];
|
|
15307
|
-
if (options.staged) args.push("--staged");
|
|
15308
|
-
return execFileSync3("git", args, {
|
|
15309
|
-
encoding: "utf-8",
|
|
15310
|
-
timeout: options.timeoutMs ?? 1e4
|
|
15311
|
-
}).trim();
|
|
15143
|
+
async function handleExport(args, ctx) {
|
|
15144
|
+
const format = args[0] === "json" ? "json" : "md";
|
|
15145
|
+
ctx.exportConversation(format);
|
|
15146
|
+
}
|
|
15147
|
+
async function handleCost(_args, ctx) {
|
|
15148
|
+
const u = ctx.sessionTokenUsage;
|
|
15149
|
+
const total = u.inputTokens + u.outputTokens + u.cacheCreationTokens + u.cacheReadTokens;
|
|
15150
|
+
const cost = computeCost(ctx.currentProvider, ctx.currentModel, u);
|
|
15151
|
+
let savings = null;
|
|
15152
|
+
if (cost != null && u.cacheReadTokens > 0) {
|
|
15153
|
+
const withoutCache = computeCost(ctx.currentProvider, ctx.currentModel, {
|
|
15154
|
+
inputTokens: u.inputTokens + u.cacheReadTokens,
|
|
15155
|
+
outputTokens: u.outputTokens,
|
|
15156
|
+
cacheCreationTokens: u.cacheCreationTokens,
|
|
15157
|
+
cacheReadTokens: 0
|
|
15158
|
+
});
|
|
15159
|
+
if (withoutCache != null) savings = withoutCache - cost;
|
|
15160
|
+
}
|
|
15161
|
+
const lines = [
|
|
15162
|
+
"\u{1F4B0} Session Cost & Token Usage",
|
|
15163
|
+
` Provider : ${ctx.currentProvider}`,
|
|
15164
|
+
` Model : ${ctx.currentModel}`,
|
|
15165
|
+
` Input (uncached): ${u.inputTokens.toLocaleString()}`,
|
|
15166
|
+
` Output : ${u.outputTokens.toLocaleString()}`
|
|
15167
|
+
];
|
|
15168
|
+
if (u.cacheCreationTokens > 0) lines.push(` Cache write : ${u.cacheCreationTokens.toLocaleString()}`);
|
|
15169
|
+
if (u.cacheReadTokens > 0) {
|
|
15170
|
+
const pct = Math.round(u.cacheReadTokens / (u.cacheReadTokens + u.inputTokens) * 100);
|
|
15171
|
+
lines.push(` Cache read : ${u.cacheReadTokens.toLocaleString()} (${pct}% hit rate)`);
|
|
15172
|
+
}
|
|
15173
|
+
lines.push(` Total tokens : ${total.toLocaleString()}`);
|
|
15174
|
+
if (cost != null) {
|
|
15175
|
+
lines.push(` Cost : ${formatCost(cost)}`);
|
|
15176
|
+
if (savings != null && savings > 0) {
|
|
15177
|
+
lines.push(` Cache savings : -${formatCost(savings)} (vs no cache)`);
|
|
15178
|
+
}
|
|
15179
|
+
} else {
|
|
15180
|
+
lines.push(` Cost : \u2014 (pricing unknown for this model)`);
|
|
15181
|
+
}
|
|
15182
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
15312
15183
|
}
|
|
15313
|
-
|
|
15314
|
-
|
|
15315
|
-
|
|
15316
|
-
|
|
15317
|
-
|
|
15318
|
-
|
|
15319
|
-
|
|
15184
|
+
async function handleCheckpoint(args, ctx) {
|
|
15185
|
+
const session = ctx.sessions.current;
|
|
15186
|
+
if (!session) {
|
|
15187
|
+
ctx.send({ type: "error", message: "No active session." });
|
|
15188
|
+
return;
|
|
15189
|
+
}
|
|
15190
|
+
const sub = args[0]?.toLowerCase();
|
|
15191
|
+
if (!sub || sub === "list") {
|
|
15192
|
+
const cps = session.checkpoints;
|
|
15193
|
+
if (cps.length === 0) {
|
|
15194
|
+
ctx.send({ type: "info", message: "No checkpoints. Use /checkpoint save <name> to create one." });
|
|
15195
|
+
} else {
|
|
15196
|
+
const lines = [`\u{1F4CC} Checkpoints (${cps.length}):`, ""];
|
|
15197
|
+
for (const cp of cps) {
|
|
15198
|
+
const ts = cp.timestamp.toLocaleString();
|
|
15199
|
+
lines.push(` ${cp.name.padEnd(20)} msg #${cp.messageIndex} ${ts}`);
|
|
15200
|
+
}
|
|
15201
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
15320
15202
|
}
|
|
15321
|
-
|
|
15322
|
-
|
|
15323
|
-
|
|
15324
|
-
|
|
15325
|
-
|
|
15326
|
-
|
|
15327
|
-
|
|
15203
|
+
return;
|
|
15204
|
+
}
|
|
15205
|
+
const cpName = args.slice(1).join(" ").trim();
|
|
15206
|
+
if (sub === "save") {
|
|
15207
|
+
if (!cpName) {
|
|
15208
|
+
ctx.send({ type: "error", message: "Usage: /checkpoint save <name>" });
|
|
15209
|
+
return;
|
|
15328
15210
|
}
|
|
15329
|
-
|
|
15330
|
-
|
|
15211
|
+
session.createCheckpoint(cpName);
|
|
15212
|
+
ctx.send({ type: "info", message: `\u2713 Checkpoint "${cpName}" saved at message #${session.messages.length}` });
|
|
15213
|
+
} else if (sub === "restore") {
|
|
15214
|
+
if (!cpName) {
|
|
15215
|
+
ctx.send({ type: "error", message: "Usage: /checkpoint restore <name>" });
|
|
15216
|
+
return;
|
|
15217
|
+
}
|
|
15218
|
+
const ok = session.restoreCheckpoint(cpName);
|
|
15219
|
+
if (ok) {
|
|
15220
|
+
ctx.send({ type: "info", message: `\u2713 Restored to checkpoint "${cpName}" (${session.messages.length} messages)` });
|
|
15221
|
+
ctx.sendSessionMessages();
|
|
15222
|
+
ctx.sendStatus();
|
|
15223
|
+
} else {
|
|
15224
|
+
ctx.send({ type: "error", message: `Checkpoint "${cpName}" not found.` });
|
|
15331
15225
|
}
|
|
15332
|
-
|
|
15333
|
-
|
|
15334
|
-
|
|
15335
|
-
|
|
15336
|
-
const tail = diff.slice(diff.length - Math.floor(MAX_DIFF * 0.2));
|
|
15337
|
-
diff = head + "\n\n... [diff truncated, " + diff.length + " chars total] ...\n\n" + tail;
|
|
15338
|
-
truncated = true;
|
|
15226
|
+
} else if (sub === "delete") {
|
|
15227
|
+
if (!cpName) {
|
|
15228
|
+
ctx.send({ type: "error", message: "Usage: /checkpoint delete <name>" });
|
|
15229
|
+
return;
|
|
15339
15230
|
}
|
|
15340
|
-
const
|
|
15341
|
-
|
|
15342
|
-
|
|
15343
|
-
|
|
15344
|
-
|
|
15345
|
-
ctx.send({ type: "info", message: msg });
|
|
15346
|
-
} catch (err) {
|
|
15347
|
-
ctx.send({ type: "error", message: `Review failed: ${err.message}` });
|
|
15231
|
+
const ok = session.deleteCheckpoint(cpName);
|
|
15232
|
+
if (ok) {
|
|
15233
|
+
ctx.send({ type: "info", message: `\u2713 Deleted checkpoint "${cpName}"` });
|
|
15234
|
+
} else {
|
|
15235
|
+
ctx.send({ type: "error", message: `Checkpoint "${cpName}" not found.` });
|
|
15348
15236
|
}
|
|
15237
|
+
} else {
|
|
15238
|
+
ctx.send({ type: "error", message: `Unknown subcommand: ${sub}. Use save/restore/list/delete.` });
|
|
15349
15239
|
}
|
|
15350
15240
|
}
|
|
15351
|
-
async function
|
|
15352
|
-
|
|
15353
|
-
|
|
15354
|
-
|
|
15355
|
-
|
|
15241
|
+
async function handleBranch(args, ctx) {
|
|
15242
|
+
const session = ctx.sessions.current;
|
|
15243
|
+
if (!session) {
|
|
15244
|
+
ctx.send({ type: "error", message: "No active session." });
|
|
15245
|
+
return;
|
|
15246
|
+
}
|
|
15247
|
+
const sub = args[0]?.toLowerCase();
|
|
15248
|
+
const resolve14 = (ref) => {
|
|
15249
|
+
const r = session.resolveBranchRef(ref);
|
|
15250
|
+
if (r.ok) return r.id;
|
|
15251
|
+
if (r.reason === "ambiguous") {
|
|
15252
|
+
const list = r.matches.map((m) => `${m.id} (${m.title})`).join(", ");
|
|
15253
|
+
ctx.send({ type: "error", message: `Ambiguous branch reference "${ref}". Matches: ${list}. Use the 6-char id.` });
|
|
15254
|
+
} else {
|
|
15255
|
+
ctx.send({ type: "error", message: `Branch "${ref}" not found. Use /branch list to see all branches.` });
|
|
15356
15256
|
}
|
|
15357
|
-
|
|
15358
|
-
|
|
15359
|
-
|
|
15360
|
-
|
|
15361
|
-
}
|
|
15362
|
-
|
|
15257
|
+
return null;
|
|
15258
|
+
};
|
|
15259
|
+
if (!sub || sub === "list") {
|
|
15260
|
+
const branches = session.listBranches();
|
|
15261
|
+
const lines = [`\u{1F33F} Branches (${branches.length}):`, ""];
|
|
15262
|
+
for (const b of branches) {
|
|
15263
|
+
const marker = b.id === session.activeBranchId ? "\u25CF " : " ";
|
|
15264
|
+
const count = b.id === session.activeBranchId ? session.messages.length : session.getBranchMessages(b.id)?.length ?? 0;
|
|
15265
|
+
const parent = b.parentBranchId ? ` \u2190 ${b.parentBranchId}@${b.parentMessageIndex}` : "";
|
|
15266
|
+
lines.push(` ${marker}${b.id.padEnd(10)} ${b.title.padEnd(20)} (${count} msgs)${parent}`);
|
|
15363
15267
|
}
|
|
15364
|
-
|
|
15365
|
-
|
|
15268
|
+
lines.push("");
|
|
15269
|
+
lines.push("Usage: /branch new <msgIndex> [title] | switch <id|title> | delete <id|title> | rename <id|title> <new title> | diff <id|title> | cherry-pick <id|title> <msgIndex>");
|
|
15270
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
15271
|
+
return;
|
|
15272
|
+
}
|
|
15273
|
+
if (sub === "new") {
|
|
15274
|
+
const idxArg = args[1];
|
|
15275
|
+
if (!idxArg) {
|
|
15276
|
+
ctx.send({ type: "error", message: "Usage: /branch new <msgIndex> [title]" });
|
|
15277
|
+
return;
|
|
15366
15278
|
}
|
|
15367
|
-
const
|
|
15368
|
-
|
|
15369
|
-
|
|
15370
|
-
|
|
15371
|
-
const tail = secDiff.slice(secDiff.length - Math.floor(SEC_MAX_DIFF * 0.2));
|
|
15372
|
-
secDiff = head + "\n\n... [diff truncated, " + secDiff.length + " chars total] ...\n\n" + tail;
|
|
15373
|
-
secTruncated = true;
|
|
15279
|
+
const fromIndex = parseInt(idxArg, 10);
|
|
15280
|
+
if (isNaN(fromIndex) || fromIndex < 0 || fromIndex > session.messages.length) {
|
|
15281
|
+
ctx.send({ type: "error", message: `Invalid msgIndex: ${idxArg}. Range: 0-${session.messages.length}` });
|
|
15282
|
+
return;
|
|
15374
15283
|
}
|
|
15375
|
-
const
|
|
15376
|
-
ctx.send({ type: "info", message: "\u{1F512} Scanning for security vulnerabilities..." });
|
|
15284
|
+
const title = args.slice(2).join(" ").trim() || void 0;
|
|
15377
15285
|
try {
|
|
15378
|
-
const
|
|
15379
|
-
|
|
15380
|
-
ctx.send({ type: "info", message:
|
|
15286
|
+
const newId = session.createBranch(fromIndex, title);
|
|
15287
|
+
await ctx.sessions.save();
|
|
15288
|
+
ctx.send({ type: "info", message: `\u2713 Created branch "${newId}" from message #${fromIndex}. Now active (${session.messages.length} messages).` });
|
|
15289
|
+
ctx.sendSessionMessages();
|
|
15290
|
+
ctx.sendStatus();
|
|
15381
15291
|
} catch (err) {
|
|
15382
|
-
ctx.send({ type: "error", message:
|
|
15292
|
+
ctx.send({ type: "error", message: err.message });
|
|
15383
15293
|
}
|
|
15294
|
+
return;
|
|
15384
15295
|
}
|
|
15385
|
-
|
|
15386
|
-
|
|
15387
|
-
|
|
15388
|
-
|
|
15389
|
-
|
|
15390
|
-
|
|
15391
|
-
|
|
15392
|
-
|
|
15393
|
-
|
|
15394
|
-
|
|
15395
|
-
|
|
15396
|
-
|
|
15397
|
-
|
|
15398
|
-
ctx.
|
|
15399
|
-
|
|
15400
|
-
|
|
15296
|
+
if (sub === "switch") {
|
|
15297
|
+
const ref = args[1];
|
|
15298
|
+
if (!ref) {
|
|
15299
|
+
ctx.send({ type: "error", message: "Usage: /branch switch <id|title>" });
|
|
15300
|
+
return;
|
|
15301
|
+
}
|
|
15302
|
+
const id = resolve14(ref);
|
|
15303
|
+
if (!id) return;
|
|
15304
|
+
const ok = session.switchBranch(id);
|
|
15305
|
+
if (ok) {
|
|
15306
|
+
await ctx.sessions.save();
|
|
15307
|
+
const b = session.getActiveBranch();
|
|
15308
|
+
ctx.send({ type: "info", message: `\u2713 Switched to branch "${b.id}" \u2014 ${b.title} (${session.messages.length} messages)` });
|
|
15309
|
+
ctx.sendSessionMessages();
|
|
15310
|
+
ctx.sendStatus();
|
|
15311
|
+
} else {
|
|
15312
|
+
ctx.send({ type: "error", message: `Cannot switch to "${id}" (already active).` });
|
|
15401
15313
|
}
|
|
15314
|
+
return;
|
|
15402
15315
|
}
|
|
15403
|
-
|
|
15404
|
-
|
|
15405
|
-
|
|
15406
|
-
|
|
15407
|
-
|
|
15408
|
-
ctx.send({ type: "info", message: "No file modifications in this session." });
|
|
15316
|
+
if (sub === "delete") {
|
|
15317
|
+
const ref = args[1];
|
|
15318
|
+
if (!ref) {
|
|
15319
|
+
ctx.send({ type: "error", message: "Usage: /branch delete <id|title>" });
|
|
15320
|
+
return;
|
|
15409
15321
|
}
|
|
15410
|
-
const
|
|
15411
|
-
|
|
15412
|
-
|
|
15413
|
-
|
|
15414
|
-
|
|
15322
|
+
const id = resolve14(ref);
|
|
15323
|
+
if (!id) return;
|
|
15324
|
+
const ok = session.deleteBranch(id);
|
|
15325
|
+
if (ok) {
|
|
15326
|
+
await ctx.sessions.save();
|
|
15327
|
+
ctx.send({ type: "info", message: `\u2713 Deleted branch "${id}"` });
|
|
15328
|
+
ctx.sendStatus();
|
|
15329
|
+
} else {
|
|
15330
|
+
ctx.send({ type: "error", message: `Cannot delete "${id}" (active, or last remaining branch).` });
|
|
15415
15331
|
}
|
|
15416
|
-
|
|
15417
|
-
|
|
15418
|
-
|
|
15419
|
-
const
|
|
15420
|
-
|
|
15421
|
-
|
|
15422
|
-
|
|
15423
|
-
|
|
15424
|
-
|
|
15425
|
-
|
|
15426
|
-
|
|
15427
|
-
|
|
15428
|
-
|
|
15429
|
-
|
|
15430
|
-
|
|
15431
|
-
|
|
15432
|
-
|
|
15433
|
-
|
|
15434
|
-
|
|
15435
|
-
|
|
15436
|
-
|
|
15437
|
-
|
|
15438
|
-
|
|
15439
|
-
|
|
15440
|
-
|
|
15441
|
-
|
|
15442
|
-
|
|
15443
|
-
|
|
15444
|
-
|
|
15445
|
-
|
|
15446
|
-
|
|
15447
|
-
|
|
15448
|
-
|
|
15449
|
-
|
|
15332
|
+
return;
|
|
15333
|
+
}
|
|
15334
|
+
if (sub === "rename") {
|
|
15335
|
+
const ref = args[1];
|
|
15336
|
+
const title = args.slice(2).join(" ").trim();
|
|
15337
|
+
if (!ref || !title) {
|
|
15338
|
+
ctx.send({ type: "error", message: "Usage: /branch rename <id|title> <new title>" });
|
|
15339
|
+
return;
|
|
15340
|
+
}
|
|
15341
|
+
const id = resolve14(ref);
|
|
15342
|
+
if (!id) return;
|
|
15343
|
+
const ok = session.renameBranch(id, title);
|
|
15344
|
+
if (ok) {
|
|
15345
|
+
await ctx.sessions.save();
|
|
15346
|
+
ctx.send({ type: "info", message: `\u2713 Renamed branch "${id}" \u2192 "${title}"` });
|
|
15347
|
+
ctx.sendStatus();
|
|
15348
|
+
} else {
|
|
15349
|
+
ctx.send({ type: "error", message: `Branch "${id}" not found.` });
|
|
15350
|
+
}
|
|
15351
|
+
return;
|
|
15352
|
+
}
|
|
15353
|
+
if (sub === "diff") {
|
|
15354
|
+
const ref = args[1];
|
|
15355
|
+
if (!ref) {
|
|
15356
|
+
ctx.send({ type: "error", message: "Usage: /branch diff <id|title>" });
|
|
15357
|
+
return;
|
|
15358
|
+
}
|
|
15359
|
+
const id = resolve14(ref);
|
|
15360
|
+
if (!id) return;
|
|
15361
|
+
const d = session.diffBranches(id);
|
|
15362
|
+
if (!d) {
|
|
15363
|
+
ctx.send({ type: "error", message: `Branch "${id}" not found.` });
|
|
15364
|
+
return;
|
|
15365
|
+
}
|
|
15366
|
+
const preview = (m) => {
|
|
15367
|
+
const text = typeof m.content === "string" ? m.content : "[complex content]";
|
|
15368
|
+
return text.replace(/\s+/g, " ").trim().slice(0, 70);
|
|
15369
|
+
};
|
|
15370
|
+
const lines = [
|
|
15371
|
+
`\u{1F500} diff ${id} (source) \u2194 ${session.activeBranchId} (active)`,
|
|
15372
|
+
`Common prefix: ${d.commonPrefix} message(s)`
|
|
15373
|
+
];
|
|
15374
|
+
if (d.sourceOnly.length === 0 && d.targetOnly.length === 0) {
|
|
15375
|
+
lines.push("Branches are identical.");
|
|
15376
|
+
} else {
|
|
15377
|
+
if (d.sourceOnly.length > 0) {
|
|
15378
|
+
lines.push("", `Only in ${id} (${d.sourceOnly.length}):`);
|
|
15379
|
+
d.sourceOnly.forEach(
|
|
15380
|
+
(m, i) => lines.push(` [${d.commonPrefix + i}] ${m.role}: ${preview(m)}`)
|
|
15381
|
+
);
|
|
15382
|
+
}
|
|
15383
|
+
if (d.targetOnly.length > 0) {
|
|
15384
|
+
lines.push("", `Only in ${session.activeBranchId} (active) (${d.targetOnly.length}):`);
|
|
15385
|
+
d.targetOnly.forEach(
|
|
15386
|
+
(m, i) => lines.push(` [${d.commonPrefix + i}] ${m.role}: ${preview(m)}`)
|
|
15387
|
+
);
|
|
15450
15388
|
}
|
|
15389
|
+
lines.push("", `Use /branch cherry-pick ${id} <index> to copy a message into the active branch.`);
|
|
15451
15390
|
}
|
|
15452
|
-
|
|
15453
|
-
|
|
15454
|
-
ctx.send({ type: "info", message: diffLines2.join("\n") });
|
|
15391
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
15392
|
+
return;
|
|
15455
15393
|
}
|
|
15456
|
-
|
|
15457
|
-
|
|
15458
|
-
|
|
15459
|
-
|
|
15460
|
-
|
|
15461
|
-
|
|
15462
|
-
if (existsSync28(targetPath) && !force) {
|
|
15463
|
-
ctx.send({ type: "info", message: `AICLI.md already exists at ${targetPath}
|
|
15464
|
-
Use /init --force to overwrite.` });
|
|
15394
|
+
if (sub === "cherry-pick") {
|
|
15395
|
+
const ref = args[1];
|
|
15396
|
+
const idxStr = args[2];
|
|
15397
|
+
if (!ref || !idxStr) {
|
|
15398
|
+
ctx.send({ type: "error", message: "Usage: /branch cherry-pick <source-id|title> <msg-index>" });
|
|
15399
|
+
return;
|
|
15465
15400
|
}
|
|
15466
|
-
|
|
15467
|
-
|
|
15468
|
-
|
|
15469
|
-
|
|
15470
|
-
|
|
15471
|
-
|
|
15472
|
-
ctx.send({ type: "info", message: `\u2713 Generated: ${targetPath} (${content.length} chars)
|
|
15473
|
-
Use /context reload to load it.` });
|
|
15474
|
-
} catch (err) {
|
|
15475
|
-
ctx.send({ type: "error", message: `Failed to generate AICLI.md: ${err.message}` });
|
|
15401
|
+
const id = resolve14(ref);
|
|
15402
|
+
if (!id) return;
|
|
15403
|
+
const idx = parseInt(idxStr, 10);
|
|
15404
|
+
if (Number.isNaN(idx)) {
|
|
15405
|
+
ctx.send({ type: "error", message: `Invalid message index: "${idxStr}"` });
|
|
15406
|
+
return;
|
|
15476
15407
|
}
|
|
15408
|
+
const picked = session.cherryPickMessage(id, idx);
|
|
15409
|
+
if (!picked) {
|
|
15410
|
+
ctx.send({ type: "error", message: `Cherry-pick failed \u2014 index ${idx} out of range for branch "${id}".` });
|
|
15411
|
+
return;
|
|
15412
|
+
}
|
|
15413
|
+
await ctx.sessions.save();
|
|
15414
|
+
const pickPreview = typeof picked.content === "string" ? picked.content.replace(/\s+/g, " ").trim().slice(0, 60) : "[complex content]";
|
|
15415
|
+
ctx.send({ type: "info", message: `\u2713 Cherry-picked ${picked.role} message from "${id}"[${idx}] \u2192 active branch (${session.messages.length} msgs): ${pickPreview}` });
|
|
15416
|
+
ctx.sendSessionMessages();
|
|
15417
|
+
ctx.sendStatus();
|
|
15418
|
+
return;
|
|
15477
15419
|
}
|
|
15420
|
+
ctx.send({ type: "error", message: `Unknown subcommand: ${sub}. Use list/new/switch/delete/rename/diff/cherry-pick.` });
|
|
15478
15421
|
}
|
|
15479
|
-
async function
|
|
15480
|
-
|
|
15481
|
-
|
|
15482
|
-
|
|
15483
|
-
|
|
15422
|
+
async function handleFork(args, ctx) {
|
|
15423
|
+
const session = ctx.sessions.current;
|
|
15424
|
+
if (!session) {
|
|
15425
|
+
ctx.send({ type: "info", message: "No active session to fork." });
|
|
15426
|
+
return;
|
|
15427
|
+
}
|
|
15428
|
+
const sub = args.join(" ").trim();
|
|
15429
|
+
let messageCount = session.messages.length;
|
|
15430
|
+
let fromLabel = "current position";
|
|
15431
|
+
if (sub) {
|
|
15432
|
+
const cp = session.checkpoints.find((c) => c.name === sub);
|
|
15433
|
+
if (!cp) {
|
|
15434
|
+
const available = session.checkpoints.map((c) => c.name);
|
|
15435
|
+
ctx.send({ type: "error", message: available.length > 0 ? `Checkpoint "${sub}" not found. Available: ${available.join(", ")}` : `Checkpoint "${sub}" not found. No checkpoints saved.` });
|
|
15436
|
+
return;
|
|
15484
15437
|
}
|
|
15485
|
-
|
|
15486
|
-
|
|
15487
|
-
|
|
15488
|
-
|
|
15489
|
-
|
|
15438
|
+
messageCount = cp.messageIndex;
|
|
15439
|
+
fromLabel = `checkpoint "${cp.name}" (${cp.messageIndex} messages)`;
|
|
15440
|
+
}
|
|
15441
|
+
try {
|
|
15442
|
+
const originalId = session.id.slice(0, 8);
|
|
15443
|
+
const forked = await ctx.sessions.forkSession(messageCount);
|
|
15444
|
+
ctx.send({ type: "info", message: `\u{1F500} Session Forked
|
|
15445
|
+
Original: ${originalId}
|
|
15446
|
+
Forked: ${forked.id.slice(0, 8)} "${forked.title ?? "(untitled)"}"
|
|
15447
|
+
From: ${fromLabel}
|
|
15448
|
+
Messages: ${forked.messages.length} copied` });
|
|
15449
|
+
ctx.sendSessionMessages();
|
|
15450
|
+
ctx.sendStatus();
|
|
15451
|
+
ctx.sendSessionList();
|
|
15452
|
+
} catch (err) {
|
|
15453
|
+
ctx.send({ type: "error", message: `Fork failed: ${err.message}` });
|
|
15490
15454
|
}
|
|
15491
15455
|
}
|
|
15456
|
+
async function handleRewind(args, ctx) {
|
|
15457
|
+
const session = ctx.sessions.current;
|
|
15458
|
+
if (!session || session.messages.length === 0) {
|
|
15459
|
+
ctx.send({ type: "info", message: "No messages to rewind." });
|
|
15460
|
+
return;
|
|
15461
|
+
}
|
|
15462
|
+
const rewindSub = args[0];
|
|
15463
|
+
if (rewindSub === "list" || !rewindSub) {
|
|
15464
|
+
const lines = [`Conversation messages (${session.messages.length} total):
|
|
15465
|
+
`];
|
|
15466
|
+
const cpIndices = (await import("./file-checkpoint-CGH6OJVI.js")).fileCheckpoints.getMessageIndices();
|
|
15467
|
+
for (let i = 0; i < session.messages.length; i++) {
|
|
15468
|
+
const m = session.messages[i];
|
|
15469
|
+
const text = getContentText(m.content).replace(/\n/g, " ").slice(0, 60);
|
|
15470
|
+
const pin = cpIndices.includes(i) ? " \u{1F4CC}" : "";
|
|
15471
|
+
lines.push(` [${i + 1}] ${m.role.padEnd(10)} ${text}${pin}`);
|
|
15472
|
+
}
|
|
15473
|
+
lines.push("", "Usage: /rewind <n> \u2014 rewind to message N", "\u{1F4CC} = file checkpoint");
|
|
15474
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
15475
|
+
return;
|
|
15476
|
+
}
|
|
15477
|
+
const rewindN = parseInt(rewindSub, 10);
|
|
15478
|
+
if (isNaN(rewindN) || rewindN < 1 || rewindN > session.messages.length) {
|
|
15479
|
+
ctx.send({ type: "error", message: `Invalid message number: ${rewindSub}. Range: 1-${session.messages.length}` });
|
|
15480
|
+
return;
|
|
15481
|
+
}
|
|
15482
|
+
const { fileCheckpoints: fc } = await import("./file-checkpoint-CGH6OJVI.js");
|
|
15483
|
+
const rewindRemoved = session.messages.length - rewindN;
|
|
15484
|
+
const rewindResult = fc.restoreToMessageIndex(rewindN);
|
|
15485
|
+
session.messages = session.messages.slice(0, rewindN);
|
|
15486
|
+
session.checkpoints = session.checkpoints.filter((c) => c.messageIndex <= rewindN);
|
|
15487
|
+
session.updated = /* @__PURE__ */ new Date();
|
|
15488
|
+
ctx.sessions.save();
|
|
15489
|
+
const rewindLines = [`\u2713 Rewound to message ${rewindN}`, ` Messages removed: ${rewindRemoved}`];
|
|
15490
|
+
if (rewindResult.restored > 0 || rewindResult.deleted > 0) {
|
|
15491
|
+
rewindLines.push(` Files restored: ${rewindResult.restored}, deleted: ${rewindResult.deleted}`);
|
|
15492
|
+
for (const f of rewindResult.files) rewindLines.push(` ${f}`);
|
|
15493
|
+
} else {
|
|
15494
|
+
rewindLines.push(" No file changes to revert.");
|
|
15495
|
+
}
|
|
15496
|
+
ctx.send({ type: "info", message: rewindLines.join("\n") });
|
|
15497
|
+
ctx.sendSessionMessages();
|
|
15498
|
+
ctx.sendStatus();
|
|
15499
|
+
}
|
|
15492
15500
|
|
|
15493
|
-
// src/web/commands/
|
|
15494
|
-
|
|
15495
|
-
|
|
15496
|
-
|
|
15497
|
-
|
|
15498
|
-
|
|
15499
|
-
|
|
15500
|
-
|
|
15501
|
-
|
|
15502
|
-
|
|
15503
|
-
|
|
15504
|
-
|
|
15505
|
-
|
|
15506
|
-
|
|
15507
|
-
|
|
15508
|
-
}
|
|
15501
|
+
// src/web/commands/tooling-commands.ts
|
|
15502
|
+
async function handleTools(_args, ctx) {
|
|
15503
|
+
ctx.sendToolsList();
|
|
15504
|
+
}
|
|
15505
|
+
async function handlePlugins(_args, ctx) {
|
|
15506
|
+
const configDir = ctx.config.getConfigDir();
|
|
15507
|
+
const pluginsDir = join24(configDir, PLUGINS_DIR_NAME);
|
|
15508
|
+
const pluginTools = ctx.toolRegistry.listPluginTools();
|
|
15509
|
+
const pluginPackages = listInstalledPlugins(configDir);
|
|
15510
|
+
const lines = [`\u{1F50C} **Plugins:**`, `Dir: ${pluginsDir}`, ""];
|
|
15511
|
+
if (pluginTools.length === 0 && pluginPackages.length === 0) {
|
|
15512
|
+
lines.push("No plugins loaded.");
|
|
15513
|
+
lines.push("Use /plugin install <path>, /plugin trust <name>, then /plugin enable <name>.");
|
|
15514
|
+
} else {
|
|
15515
|
+
if (pluginPackages.length > 0) {
|
|
15516
|
+
lines.push(`Package plugins (${pluginPackages.length}):`);
|
|
15517
|
+
for (const p of pluginPackages) lines.push(` \u2022 ${p.name}@${p.manifest.version} \u2014 ${p.enabled ? "enabled" : "disabled"} / ${p.trusted ? "trusted" : "untrusted"}`);
|
|
15518
|
+
lines.push("");
|
|
15509
15519
|
}
|
|
15510
|
-
if (
|
|
15511
|
-
|
|
15512
|
-
|
|
15513
|
-
} else {
|
|
15514
|
-
ctx.activeSystemPrompt = text;
|
|
15515
|
-
ctx.send({ type: "info", message: `\u2713 System prompt set (${text.length} chars).` });
|
|
15520
|
+
if (pluginTools.length > 0) {
|
|
15521
|
+
lines.push(`Legacy JS plugin tools (${pluginTools.length}):`);
|
|
15522
|
+
for (const t of pluginTools) lines.push(` \u2022 ${t.definition.name} \u2014 ${t.definition.description}`);
|
|
15516
15523
|
}
|
|
15517
15524
|
}
|
|
15525
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
15518
15526
|
}
|
|
15519
|
-
async function
|
|
15520
|
-
|
|
15521
|
-
|
|
15522
|
-
|
|
15523
|
-
|
|
15524
|
-
|
|
15525
|
-
|
|
15526
|
-
|
|
15527
|
-
|
|
15528
|
-
|
|
15529
|
-
|
|
15530
|
-
for (const layer of result.layers) {
|
|
15531
|
-
layers.push(` \u2713 ${labels[layer.level] ?? layer.level}: ${layer.displayPath} (${layer.charCount} chars${layer.truncated ? ", truncated" : ""})`);
|
|
15532
|
-
}
|
|
15533
|
-
layers.push(` Total: ${result.totalChars} chars (${result.layers.length} layer${result.layers.length > 1 ? "s" : ""})`);
|
|
15534
|
-
} else {
|
|
15535
|
-
layers.push(" No context files loaded.");
|
|
15536
|
-
layers.push(" Search order per layer: AICLI.override.md \u2192 AGENTS.override.md \u2192 AICLI.md \u2192 CLAUDE.md \u2192 AGENTS.md");
|
|
15527
|
+
async function handlePlugin(args, ctx) {
|
|
15528
|
+
const configDir = ctx.config.getConfigDir();
|
|
15529
|
+
const sub = args[0] ?? "list";
|
|
15530
|
+
try {
|
|
15531
|
+
if (sub === "list") {
|
|
15532
|
+
const plugins = listInstalledPlugins(configDir);
|
|
15533
|
+
const lines = [`\u{1F50C} **Package Plugins**`, `Dir: ${pluginRoot(configDir)}`, ""];
|
|
15534
|
+
if (plugins.length === 0) lines.push("No package plugins installed.");
|
|
15535
|
+
for (const p of plugins) lines.push(` \u2022 ${p.name}@${p.manifest.version} \u2014 ${p.enabled ? "enabled" : "disabled"} / ${p.trusted ? "trusted" : "untrusted"}`);
|
|
15536
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
15537
|
+
return;
|
|
15537
15538
|
}
|
|
15538
|
-
if (
|
|
15539
|
-
|
|
15540
|
-
|
|
15541
|
-
|
|
15542
|
-
|
|
15543
|
-
|
|
15539
|
+
if (sub === "install") {
|
|
15540
|
+
const input = args[1];
|
|
15541
|
+
if (!input) throw new Error("Usage: /plugin install <path>");
|
|
15542
|
+
const plugin = installPlugin(configDir, input);
|
|
15543
|
+
ctx.send({ type: "info", message: `Installed plugin ${plugin.name}@${plugin.manifest.version}. Run /plugin trust ${plugin.name}, then /plugin enable ${plugin.name}.` });
|
|
15544
|
+
return;
|
|
15544
15545
|
}
|
|
15545
|
-
|
|
15546
|
-
|
|
15547
|
-
|
|
15548
|
-
|
|
15546
|
+
if (sub === "trust") {
|
|
15547
|
+
const name = args[1];
|
|
15548
|
+
if (!name) throw new Error("Usage: /plugin trust <name>");
|
|
15549
|
+
const state2 = trustPlugin(configDir, name);
|
|
15550
|
+
ctx.send({ type: "info", message: `Trusted plugin ${state2.name}.` });
|
|
15551
|
+
return;
|
|
15552
|
+
}
|
|
15553
|
+
if (sub === "enable" || sub === "disable") {
|
|
15554
|
+
const name = args[1];
|
|
15555
|
+
if (!name) throw new Error(`Usage: /plugin ${sub} <name>`);
|
|
15556
|
+
const state2 = setPluginEnabled(configDir, name, sub === "enable");
|
|
15557
|
+
ctx.send({ type: "info", message: `${sub === "enable" ? "Enabled" : "Disabled"} plugin ${state2.name}. Restart Web UI for changes to take effect.` });
|
|
15558
|
+
return;
|
|
15559
|
+
}
|
|
15560
|
+
if (sub === "inspect") {
|
|
15561
|
+
const name = args[1];
|
|
15562
|
+
if (!name) throw new Error("Usage: /plugin inspect <name>");
|
|
15563
|
+
ctx.send({ type: "info", message: describePlugin(getInstalledPlugin(configDir, name)).join("\n") });
|
|
15564
|
+
return;
|
|
15565
|
+
}
|
|
15566
|
+
throw new Error("Usage: /plugin [list|install|enable|disable|inspect|trust]");
|
|
15567
|
+
} catch (err) {
|
|
15568
|
+
ctx.send({ type: "error", message: err instanceof Error ? err.message : String(err) });
|
|
15549
15569
|
}
|
|
15550
15570
|
}
|
|
15551
|
-
async function
|
|
15552
|
-
{
|
|
15553
|
-
|
|
15554
|
-
|
|
15555
|
-
|
|
15556
|
-
|
|
15557
|
-
|
|
15558
|
-
|
|
15559
|
-
|
|
15560
|
-
|
|
15561
|
-
|
|
15562
|
-
|
|
15563
|
-
|
|
15564
|
-
|
|
15565
|
-
|
|
15566
|
-
|
|
15567
|
-
|
|
15568
|
-
|
|
15569
|
-
|
|
15570
|
-
|
|
15571
|
-
|
|
15572
|
-
|
|
15573
|
-
|
|
15574
|
-
|
|
15575
|
-
|
|
15576
|
-
|
|
15577
|
-
|
|
15578
|
-
|
|
15579
|
-
|
|
15580
|
-
|
|
15581
|
-
|
|
15582
|
-
|
|
15583
|
-
|
|
15584
|
-
|
|
15585
|
-
|
|
15586
|
-
|
|
15587
|
-
|
|
15588
|
-
|
|
15589
|
-
|
|
15590
|
-
|
|
15591
|
-
|
|
15592
|
-
}
|
|
15593
|
-
if (idx.symbolCount === 0) {
|
|
15594
|
-
ctx.send({
|
|
15595
|
-
type: "error",
|
|
15596
|
-
message: "No code symbols to embed. Semantic search needs TS/TSX/JS/JSX/Python source files \u2014 this project appears to have none. Not downloading the embedding model."
|
|
15597
|
-
});
|
|
15598
|
-
}
|
|
15599
|
-
ctx.send({
|
|
15600
|
-
type: "info",
|
|
15601
|
-
message: `Building semantic index for ${idx.symbolCount} symbols\u2026 (first run downloads ~117 MB model)`
|
|
15602
|
-
});
|
|
15603
|
-
try {
|
|
15604
|
-
const { rebuildSemanticIndex } = await import("./semantic-FF6DDJI6.js");
|
|
15605
|
-
const stats = await rebuildSemanticIndex(root);
|
|
15606
|
-
const first = stats.modelFirstLoadMs ? ` (model load+first batch ${stats.modelFirstLoadMs}ms)` : "";
|
|
15607
|
-
ctx.send({
|
|
15608
|
-
type: "info",
|
|
15609
|
-
message: `\u2713 Embedded ${stats.symbolsEmbedded} symbols in ${stats.durationMs}ms${first}`
|
|
15610
|
-
});
|
|
15611
|
-
} catch (err) {
|
|
15612
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
15613
|
-
ctx.send({ type: "error", message: `Semantic rebuild failed: ${msg}` });
|
|
15614
|
-
}
|
|
15615
|
-
} else if (sub === "semantic-clear") {
|
|
15616
|
-
clearVectorStore(root);
|
|
15617
|
-
ctx.send({ type: "info", message: "\u2713 Cleared semantic index (symbol index preserved)" });
|
|
15571
|
+
async function handleMcp(_args, ctx) {
|
|
15572
|
+
if (!ctx.mcpManager) {
|
|
15573
|
+
ctx.send({ type: "info", message: "MCP not configured. Add mcpServers to config.json." });
|
|
15574
|
+
return;
|
|
15575
|
+
}
|
|
15576
|
+
const sub = _args[0];
|
|
15577
|
+
if (sub === "reconnect") {
|
|
15578
|
+
ctx.send({ type: "info", message: "\u{1F504} Reconnecting MCP servers..." });
|
|
15579
|
+
try {
|
|
15580
|
+
await ctx.mcpManager.reconnectAll();
|
|
15581
|
+
ctx.send({ type: "info", message: "\u2713 MCP reconnect complete." });
|
|
15582
|
+
} catch (err) {
|
|
15583
|
+
ctx.send({ type: "error", message: `Reconnect failed: ${err.message}` });
|
|
15584
|
+
}
|
|
15585
|
+
ctx.sendToolsList();
|
|
15586
|
+
return;
|
|
15587
|
+
}
|
|
15588
|
+
const statuses = ctx.mcpManager.getStatus();
|
|
15589
|
+
if (statuses.length === 0) {
|
|
15590
|
+
ctx.send({ type: "info", message: "No MCP servers configured." });
|
|
15591
|
+
return;
|
|
15592
|
+
}
|
|
15593
|
+
const lines = [`\u{1F50C} **MCP Servers (${statuses.length}):**`, ""];
|
|
15594
|
+
for (const s of statuses) {
|
|
15595
|
+
const state2 = s.connected ? `\u2713 connected \xB7 ${s.serverName} \xB7 ${s.toolCount} tools` : `\u2717 disconnected${s.error ? ` \xB7 ${s.error}` : ""}`;
|
|
15596
|
+
lines.push(` ${s.serverId}: ${state2}`);
|
|
15597
|
+
}
|
|
15598
|
+
lines.push("");
|
|
15599
|
+
lines.push("Use /mcp reconnect to reconnect.");
|
|
15600
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
15601
|
+
}
|
|
15602
|
+
async function handleSkill(args, ctx) {
|
|
15603
|
+
const sub = args[0];
|
|
15604
|
+
if (!ctx.skillManager) {
|
|
15605
|
+
ctx.send({ type: "error", message: "Skill system not available." });
|
|
15606
|
+
return;
|
|
15607
|
+
}
|
|
15608
|
+
if (!sub || sub === "list") {
|
|
15609
|
+
const skills = ctx.skillManager.listSkills();
|
|
15610
|
+
const active = ctx.skillManager.getActive();
|
|
15611
|
+
if (skills.length === 0) {
|
|
15612
|
+
ctx.send({ type: "info", message: "No skills available. Add .md files to ~/.aicli/skills/" });
|
|
15618
15613
|
} else {
|
|
15619
|
-
|
|
15620
|
-
|
|
15621
|
-
|
|
15614
|
+
const lines = skills.map((s) => {
|
|
15615
|
+
const isActive = active?.meta.name === s.meta.name;
|
|
15616
|
+
const marker = isActive ? " \u2705 (active)" : "";
|
|
15617
|
+
return ` ${s.meta.name}${marker} \u2014 ${s.meta.description || "(no description)"}`;
|
|
15622
15618
|
});
|
|
15619
|
+
ctx.send({ type: "info", message: `\u{1F4DA} Skills:
|
|
15620
|
+
${lines.join("\n")}` });
|
|
15623
15621
|
}
|
|
15622
|
+
return;
|
|
15623
|
+
}
|
|
15624
|
+
if (sub === "off") {
|
|
15625
|
+
ctx.skillManager.deactivate();
|
|
15626
|
+
ctx.send({ type: "info", message: "\u{1F50C} Skill deactivated." });
|
|
15627
|
+
ctx.sendToolsList();
|
|
15628
|
+
ctx.sendStatus();
|
|
15629
|
+
return;
|
|
15630
|
+
}
|
|
15631
|
+
if (sub === "reload") {
|
|
15632
|
+
ctx.skillManager.loadSkills(getActivePluginAssets(ctx.config.getConfigDir()).skillDirs);
|
|
15633
|
+
ctx.send({ type: "info", message: `\u{1F504} Reloaded ${ctx.skillManager.listSkills().length} skill(s).` });
|
|
15634
|
+
ctx.sendToolsList();
|
|
15635
|
+
return;
|
|
15636
|
+
}
|
|
15637
|
+
const activated = ctx.skillManager.activate(sub);
|
|
15638
|
+
if (activated) {
|
|
15639
|
+
ctx.send({ type: "info", message: `\u2705 Skill activated: ${activated.meta.name}
|
|
15640
|
+
${activated.meta.description || ""}` });
|
|
15641
|
+
ctx.sendToolsList();
|
|
15642
|
+
ctx.sendStatus();
|
|
15643
|
+
} else {
|
|
15644
|
+
const available = ctx.skillManager.listSkills().map((s) => s.meta.name).join(", ");
|
|
15645
|
+
ctx.send({ type: "error", message: `Skill "${sub}" not found. Available: ${available}` });
|
|
15624
15646
|
}
|
|
15625
15647
|
}
|
|
15626
|
-
async function
|
|
15627
|
-
|
|
15628
|
-
|
|
15629
|
-
|
|
15630
|
-
|
|
15631
|
-
|
|
15632
|
-
|
|
15648
|
+
async function handleCommands(_args, ctx) {
|
|
15649
|
+
const configDir = ctx.config.getConfigDir();
|
|
15650
|
+
const commandsDir = join24(configDir, CUSTOM_COMMANDS_DIR_NAME);
|
|
15651
|
+
if (!existsSync29(commandsDir)) {
|
|
15652
|
+
ctx.send({ type: "info", message: `No custom commands directory.
|
|
15653
|
+
Create: ${commandsDir}/ with .md files.` });
|
|
15654
|
+
return;
|
|
15655
|
+
}
|
|
15656
|
+
try {
|
|
15657
|
+
const files = readdirSync13(commandsDir).filter((f) => f.endsWith(".md"));
|
|
15658
|
+
if (files.length === 0) {
|
|
15659
|
+
ctx.send({ type: "info", message: `No custom commands found in ${commandsDir}
|
|
15660
|
+
Add .md files to create commands.` });
|
|
15633
15661
|
} else {
|
|
15634
|
-
|
|
15662
|
+
const lines = [`\u{1F4CB} **Custom Commands (${files.length}):**`, `Dir: ${commandsDir}`, ""];
|
|
15663
|
+
for (const f of files) {
|
|
15664
|
+
const name = f.replace(/\.md$/, "");
|
|
15665
|
+
lines.push(` /${name}`);
|
|
15666
|
+
}
|
|
15667
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
15635
15668
|
}
|
|
15669
|
+
} catch {
|
|
15670
|
+
ctx.send({ type: "info", message: `Cannot read: ${commandsDir}` });
|
|
15636
15671
|
}
|
|
15637
15672
|
}
|
|
15638
|
-
async function handleAddDir(args, ctx) {
|
|
15639
|
-
{
|
|
15640
|
-
const sub = args[0]?.trim();
|
|
15641
|
-
if (!sub) {
|
|
15642
|
-
ctx.send({ type: "info", message: ctx.addedDirs.size > 0 ? `\u{1F4C1} Added directories:
|
|
15643
|
-
${[...ctx.addedDirs].map((d) => ` \u2022 ${d}`).join("\n")}
|
|
15644
15673
|
|
|
15645
|
-
|
|
15646
|
-
|
|
15647
|
-
|
|
15648
|
-
|
|
15649
|
-
|
|
15650
|
-
|
|
15651
|
-
const dirPath = resolve12(sub);
|
|
15652
|
-
if (!existsSync29(dirPath)) {
|
|
15653
|
-
ctx.send({ type: "error", message: `Directory not found: ${dirPath}` });
|
|
15654
|
-
}
|
|
15655
|
-
if (!statSync12(dirPath).isDirectory()) {
|
|
15656
|
-
ctx.send({ type: "error", message: `Not a directory: ${dirPath}` });
|
|
15657
|
-
}
|
|
15658
|
-
ctx.addedDirs.add(dirPath);
|
|
15659
|
-
ctx.send({ type: "info", message: `\u2713 Added directory: ${dirPath}
|
|
15660
|
-
It will be included in AI context for subsequent messages.` });
|
|
15674
|
+
// src/web/commands/review-commands.ts
|
|
15675
|
+
async function handleReview(args, ctx) {
|
|
15676
|
+
const gitCtx = getGitContext();
|
|
15677
|
+
if (!gitCtx) {
|
|
15678
|
+
ctx.send({ type: "error", message: "Not a git repository." });
|
|
15679
|
+
return;
|
|
15661
15680
|
}
|
|
15662
|
-
|
|
15663
|
-
|
|
15664
|
-
|
|
15665
|
-
import { existsSync as existsSync31, statSync as statSync14 } from "fs";
|
|
15666
|
-
import { join as join26 } from "path";
|
|
15667
|
-
import { arch as arch2, platform as platform6, release as release2 } from "os";
|
|
15668
|
-
|
|
15669
|
-
// src/diagnostics/crash-log.ts
|
|
15670
|
-
import {
|
|
15671
|
-
existsSync as existsSync30,
|
|
15672
|
-
mkdirSync as mkdirSync16,
|
|
15673
|
-
readdirSync as readdirSync15,
|
|
15674
|
-
readFileSync as readFileSync24,
|
|
15675
|
-
statSync as statSync13,
|
|
15676
|
-
unlinkSync as unlinkSync6,
|
|
15677
|
-
writeFileSync as writeFileSync5
|
|
15678
|
-
} from "fs";
|
|
15679
|
-
import { join as join25 } from "path";
|
|
15680
|
-
import { homedir as homedir8, platform as platform5, release, arch } from "os";
|
|
15681
|
-
var LOGS_DIR_NAME = "logs";
|
|
15682
|
-
var CRASH_LOG_PREFIX = "crash-";
|
|
15683
|
-
function getLogsDir(configDir) {
|
|
15684
|
-
const base = configDir ?? join25(homedir8(), CONFIG_DIR_NAME);
|
|
15685
|
-
return join25(base, LOGS_DIR_NAME);
|
|
15686
|
-
}
|
|
15687
|
-
function truncate(s, n) {
|
|
15688
|
-
return s.length <= n ? s : s.slice(0, n) + "\u2026";
|
|
15689
|
-
}
|
|
15690
|
-
function listRecentCrashes(limit = 10, configDir) {
|
|
15691
|
-
const dir = getLogsDir(configDir);
|
|
15692
|
-
if (!existsSync30(dir)) return [];
|
|
15693
|
-
const files = [];
|
|
15694
|
-
let names;
|
|
15681
|
+
const staged = args.includes("--staged");
|
|
15682
|
+
const detailed = args.includes("--detailed");
|
|
15683
|
+
let diff;
|
|
15695
15684
|
try {
|
|
15696
|
-
|
|
15685
|
+
diff = readGitDiff({ staged });
|
|
15697
15686
|
} catch {
|
|
15698
|
-
|
|
15687
|
+
ctx.send({ type: "error", message: "Failed to run git diff." });
|
|
15688
|
+
return;
|
|
15699
15689
|
}
|
|
15700
|
-
|
|
15701
|
-
|
|
15702
|
-
|
|
15703
|
-
|
|
15704
|
-
|
|
15705
|
-
|
|
15706
|
-
|
|
15707
|
-
|
|
15708
|
-
|
|
15709
|
-
|
|
15710
|
-
|
|
15711
|
-
|
|
15712
|
-
|
|
15713
|
-
|
|
15714
|
-
|
|
15715
|
-
|
|
15716
|
-
|
|
15717
|
-
|
|
15718
|
-
|
|
15690
|
+
if (!diff) {
|
|
15691
|
+
ctx.send({ type: "info", message: "No changes to review." + (staged ? "" : " Try --staged for staged changes.") });
|
|
15692
|
+
return;
|
|
15693
|
+
}
|
|
15694
|
+
const MAX_DIFF = 8e3;
|
|
15695
|
+
let truncated = false;
|
|
15696
|
+
if (diff.length > MAX_DIFF) {
|
|
15697
|
+
const head = diff.slice(0, Math.floor(MAX_DIFF * 0.7));
|
|
15698
|
+
const tail = diff.slice(diff.length - Math.floor(MAX_DIFF * 0.2));
|
|
15699
|
+
diff = head + "\n\n... [diff truncated, " + diff.length + " chars total] ...\n\n" + tail;
|
|
15700
|
+
truncated = true;
|
|
15701
|
+
}
|
|
15702
|
+
const reviewPrompt = buildReviewPrompt(diff, formatGitContextForPrompt(gitCtx), detailed);
|
|
15703
|
+
ctx.send({ type: "info", message: "\u{1F50D} Analyzing changes..." });
|
|
15704
|
+
try {
|
|
15705
|
+
const review = await ctx.chatOnce(reviewPrompt, { temperature: 0.3, maxTokens: 8192 });
|
|
15706
|
+
const msg = truncated ? review + "\n\n\u26A0 Diff was truncated. Consider reviewing smaller changesets." : review;
|
|
15707
|
+
ctx.send({ type: "info", message: msg });
|
|
15708
|
+
} catch (err) {
|
|
15709
|
+
ctx.send({ type: "error", message: `Review failed: ${err.message}` });
|
|
15719
15710
|
}
|
|
15720
|
-
files.sort((a, b) => a.timestamp < b.timestamp ? 1 : -1);
|
|
15721
|
-
return files.slice(0, limit);
|
|
15722
15711
|
}
|
|
15723
|
-
function
|
|
15724
|
-
const
|
|
15725
|
-
if (!
|
|
15726
|
-
|
|
15727
|
-
|
|
15728
|
-
|
|
15712
|
+
async function handleSecurityReview(args, ctx) {
|
|
15713
|
+
const gitCtx = getGitContext();
|
|
15714
|
+
if (!gitCtx) {
|
|
15715
|
+
ctx.send({ type: "error", message: "Not a git repository." });
|
|
15716
|
+
return;
|
|
15717
|
+
}
|
|
15718
|
+
const secStaged = args.includes("--staged");
|
|
15719
|
+
let secDiff;
|
|
15729
15720
|
try {
|
|
15730
|
-
|
|
15721
|
+
secDiff = readGitDiff({ staged: secStaged });
|
|
15731
15722
|
} catch {
|
|
15732
|
-
|
|
15723
|
+
ctx.send({ type: "error", message: "Failed to run git diff." });
|
|
15724
|
+
return;
|
|
15733
15725
|
}
|
|
15734
|
-
|
|
15735
|
-
|
|
15736
|
-
|
|
15737
|
-
|
|
15738
|
-
|
|
15726
|
+
if (!secDiff) {
|
|
15727
|
+
ctx.send({ type: "info", message: "No changes to review." + (secStaged ? "" : " Try --staged for staged changes.") });
|
|
15728
|
+
return;
|
|
15729
|
+
}
|
|
15730
|
+
const SEC_MAX_DIFF = 8e3;
|
|
15731
|
+
let secTruncated = false;
|
|
15732
|
+
if (secDiff.length > SEC_MAX_DIFF) {
|
|
15733
|
+
const head = secDiff.slice(0, Math.floor(SEC_MAX_DIFF * 0.7));
|
|
15734
|
+
const tail = secDiff.slice(secDiff.length - Math.floor(SEC_MAX_DIFF * 0.2));
|
|
15735
|
+
secDiff = head + "\n\n... [diff truncated, " + secDiff.length + " chars total] ...\n\n" + tail;
|
|
15736
|
+
secTruncated = true;
|
|
15737
|
+
}
|
|
15738
|
+
const secPrompt = buildSecurityReviewPrompt(secDiff, formatGitContextForPrompt(gitCtx));
|
|
15739
|
+
ctx.send({ type: "info", message: "\u{1F512} Scanning for security vulnerabilities..." });
|
|
15740
|
+
try {
|
|
15741
|
+
const secReview = await ctx.chatOnce(secPrompt, { temperature: 0.2, maxTokens: 8192 });
|
|
15742
|
+
const secMsg = secTruncated ? secReview + "\n\n\u26A0 Diff was truncated. Consider reviewing smaller changesets." : secReview;
|
|
15743
|
+
ctx.send({ type: "info", message: secMsg });
|
|
15744
|
+
} catch (err) {
|
|
15745
|
+
ctx.send({ type: "error", message: `Security review failed: ${err.message}` });
|
|
15739
15746
|
}
|
|
15740
|
-
entries.sort((a, b) => b.bytes - a.bytes);
|
|
15741
|
-
return { totalBytes: total, entries };
|
|
15742
15747
|
}
|
|
15743
|
-
function
|
|
15748
|
+
async function handleTest(args, ctx) {
|
|
15749
|
+
ctx.send({ type: "info", message: "\u{1F9EA} Running tests..." });
|
|
15744
15750
|
try {
|
|
15745
|
-
const
|
|
15746
|
-
|
|
15747
|
-
|
|
15748
|
-
|
|
15749
|
-
|
|
15750
|
-
|
|
15751
|
-
}
|
|
15752
|
-
|
|
15753
|
-
|
|
15754
|
-
|
|
15751
|
+
const { executeTests } = await import("./run-tests-4PNOUHC3.js");
|
|
15752
|
+
const argStr = args.join(" ").trim();
|
|
15753
|
+
let testArgs = {};
|
|
15754
|
+
if (argStr) {
|
|
15755
|
+
const isCommand = argStr.includes(" ") || /^(mvn|gradle|npm|pytest|cargo|go)\b/.test(argStr);
|
|
15756
|
+
testArgs = isCommand ? { command: argStr } : { filter: argStr };
|
|
15757
|
+
}
|
|
15758
|
+
const report = await executeTests(testArgs);
|
|
15759
|
+
ctx.send({ type: "info", message: report });
|
|
15760
|
+
} catch (err) {
|
|
15761
|
+
ctx.send({ type: "error", message: `Test failed: ${err.message}` });
|
|
15755
15762
|
}
|
|
15756
15763
|
}
|
|
15757
|
-
|
|
15758
|
-
|
|
15759
|
-
|
|
15760
|
-
|
|
15761
|
-
|
|
15762
|
-
|
|
15763
|
-
|
|
15764
|
-
|
|
15765
|
-
|
|
15764
|
+
async function handleDiff(args, ctx) {
|
|
15765
|
+
const history = undoStack.getHistory();
|
|
15766
|
+
if (history.length === 0) {
|
|
15767
|
+
ctx.send({ type: "info", message: "No file modifications in this session." });
|
|
15768
|
+
return;
|
|
15769
|
+
}
|
|
15770
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
15771
|
+
for (const entry of history) {
|
|
15772
|
+
if (!fileMap.has(entry.filePath)) {
|
|
15773
|
+
fileMap.set(entry.filePath, { earliest: entry.previousContent });
|
|
15766
15774
|
}
|
|
15767
15775
|
}
|
|
15768
|
-
|
|
15769
|
-
|
|
15770
|
-
|
|
15771
|
-
const
|
|
15772
|
-
const
|
|
15773
|
-
|
|
15774
|
-
|
|
15775
|
-
|
|
15776
|
-
|
|
15777
|
-
|
|
15778
|
-
|
|
15779
|
-
|
|
15780
|
-
|
|
15781
|
-
|
|
15782
|
-
|
|
15783
|
-
|
|
15784
|
-
|
|
15785
|
-
|
|
15786
|
-
|
|
15787
|
-
|
|
15788
|
-
|
|
15789
|
-
|
|
15790
|
-
|
|
15791
|
-
|
|
15792
|
-
|
|
15793
|
-
|
|
15794
|
-
|
|
15795
|
-
|
|
15796
|
-
|
|
15797
|
-
|
|
15798
|
-
|
|
15799
|
-
|
|
15800
|
-
|
|
15801
|
-
|
|
15802
|
-
files: [
|
|
15803
|
-
checkFile("config.json", join26(configDir, "config.json")),
|
|
15804
|
-
checkFile("memory.md", join26(configDir, MEMORY_FILE_NAME)),
|
|
15805
|
-
checkFile("memory.jsonl", join26(configDir, "memory.jsonl")),
|
|
15806
|
-
checkFile("dev-state.md", join26(configDir, DEV_STATE_FILE_NAME)),
|
|
15807
|
-
checkFile("hooks-trust.json", join26(configDir, "hooks-trust.json")),
|
|
15808
|
-
checkFile("plugin-state.json", join26(configDir, "plugins", "plugin-state.json"))
|
|
15809
|
-
],
|
|
15810
|
-
context: {
|
|
15811
|
-
enabled: contextSetting !== false,
|
|
15812
|
-
setting: contextSetting,
|
|
15813
|
-
candidates: context.candidates,
|
|
15814
|
-
maxBytes: context.maxBytes,
|
|
15815
|
-
layers: context.layers.map((l) => ({ level: l.level, fileName: l.fileName, displayPath: l.displayPath, bytes: l.byteCount, chars: l.charCount, truncated: l.truncated })),
|
|
15816
|
-
skipped: context.skipped.map((s) => ({ level: s.level, fileName: s.fileName, displayPath: s.displayPath, reason: s.reason, message: s.message }))
|
|
15817
|
-
},
|
|
15818
|
-
permissions: {
|
|
15819
|
-
defaultProfile: config.get("defaultPermissionProfile"),
|
|
15820
|
-
allowedProfiles: config.get("allowedPermissionProfiles"),
|
|
15821
|
-
customProfileCount: Object.keys(config.get("permissionProfiles")).length,
|
|
15822
|
-
legacyDefaultAction: config.get("defaultPermission"),
|
|
15823
|
-
ruleCount: config.get("permissionRules").length
|
|
15824
|
-
},
|
|
15825
|
-
networkPolicy: {
|
|
15826
|
-
enabled: network.enabled,
|
|
15827
|
-
defaultAction: network.defaultAction,
|
|
15828
|
-
allowDomains: network.allowDomains.length,
|
|
15829
|
-
denyDomains: network.denyDomains.length,
|
|
15830
|
-
allowPorts: network.allowPorts.length,
|
|
15831
|
-
denyPorts: network.denyPorts.length,
|
|
15832
|
-
allowPrivateNetwork: network.allowPrivateNetwork,
|
|
15833
|
-
toolOverrides: Object.fromEntries(Object.entries(network.tools).filter(([, v]) => v !== void 0))
|
|
15834
|
-
},
|
|
15835
|
-
hooks: {
|
|
15836
|
-
enabled: hooksConfig?.enabled !== false,
|
|
15837
|
-
total: hooks.length,
|
|
15838
|
-
project: hooks.filter((h) => h.source === "project").length,
|
|
15839
|
-
pendingTrust: pendingHooks.length,
|
|
15840
|
-
disabled: hooks.filter((h) => h.disabled).length
|
|
15841
|
-
},
|
|
15842
|
-
plugins: {
|
|
15843
|
-
root: join26(configDir, "plugins"),
|
|
15844
|
-
installed: plugins.length,
|
|
15845
|
-
active: activeAssets.plugins.length,
|
|
15846
|
-
invalid: plugins.filter((p) => !p.valid).length,
|
|
15847
|
-
untrusted: plugins.filter((p) => p.valid && !p.trusted).length,
|
|
15848
|
-
disabled: plugins.filter((p) => p.valid && !p.enabled).length,
|
|
15849
|
-
mcpServers: Object.keys(activeAssets.mcpServers).length,
|
|
15850
|
-
skillDirs: activeAssets.skillDirs.length,
|
|
15851
|
-
commandDirs: activeAssets.commandDirs.length,
|
|
15852
|
-
agentDirs: activeAssets.agentDirs.length,
|
|
15853
|
-
entries: plugins.map((p) => ({ name: p.name, version: p.manifest.version, enabled: p.enabled, trusted: p.trusted, valid: p.valid, error: p.error }))
|
|
15854
|
-
},
|
|
15855
|
-
mcp: {
|
|
15856
|
-
enabled: config.get("mcpEnabled"),
|
|
15857
|
-
configured: Object.keys(mcpServers).length,
|
|
15858
|
-
projectConfigExists: existsSync31(join26(projectRoot, MCP_PROJECT_CONFIG_NAME)),
|
|
15859
|
-
statuses: options.mcpStatuses ?? []
|
|
15860
|
-
},
|
|
15861
|
-
recentCrashes: listRecentCrashes(5, configDir),
|
|
15862
|
-
diskUsage: getConfigDirUsage(configDir),
|
|
15863
|
-
toolStats: {
|
|
15864
|
-
totalCalls: allStats.reduce((a, b) => a + b.calls, 0),
|
|
15865
|
-
totalFailures: allStats.reduce((a, b) => a + b.failures, 0),
|
|
15866
|
-
topUsed: getTopUsedTools(5),
|
|
15867
|
-
topFailing: getTopFailingTools(5)
|
|
15776
|
+
const statsOnly = args[0]?.toLowerCase() === "--stats";
|
|
15777
|
+
let newFiles = 0;
|
|
15778
|
+
let modifiedFiles = 0;
|
|
15779
|
+
const diffLines2 = [];
|
|
15780
|
+
for (const [filePath, { earliest }] of fileMap) {
|
|
15781
|
+
const currentContent = existsSync29(filePath) ? (() => {
|
|
15782
|
+
try {
|
|
15783
|
+
return readFileSync22(filePath, "utf-8");
|
|
15784
|
+
} catch {
|
|
15785
|
+
return null;
|
|
15786
|
+
}
|
|
15787
|
+
})() : null;
|
|
15788
|
+
const isNew = earliest === null;
|
|
15789
|
+
if (isNew) newFiles++;
|
|
15790
|
+
else modifiedFiles++;
|
|
15791
|
+
if (statsOnly) continue;
|
|
15792
|
+
if (isNew) {
|
|
15793
|
+
diffLines2.push(`
|
|
15794
|
+
\u{1F4C4} [NEW] ${filePath}`);
|
|
15795
|
+
if (currentContent !== null) {
|
|
15796
|
+
diffLines2.push(` ${currentContent.split("\n").length} lines`);
|
|
15797
|
+
} else {
|
|
15798
|
+
diffLines2.push(" (file was deleted after creation)");
|
|
15799
|
+
}
|
|
15800
|
+
} else {
|
|
15801
|
+
diffLines2.push(`
|
|
15802
|
+
\u{1F4DD} [MODIFIED] ${filePath}`);
|
|
15803
|
+
const oldText = earliest ?? "";
|
|
15804
|
+
const newText = currentContent ?? "";
|
|
15805
|
+
if (oldText === newText) {
|
|
15806
|
+
diffLines2.push(" (no net change)");
|
|
15807
|
+
} else {
|
|
15808
|
+
diffLines2.push(renderDiff(oldText, newText, { filePath, maxLines: 80 }));
|
|
15809
|
+
}
|
|
15868
15810
|
}
|
|
15869
|
-
}
|
|
15811
|
+
}
|
|
15812
|
+
diffLines2.push(`
|
|
15813
|
+
**Summary:** ${fileMap.size} file(s) \u2014 ${newFiles} new, ${modifiedFiles} modified`);
|
|
15814
|
+
ctx.send({ type: "info", message: diffLines2.join("\n") });
|
|
15815
|
+
}
|
|
15816
|
+
async function handleInit(args, ctx) {
|
|
15817
|
+
const cwd = process.cwd();
|
|
15818
|
+
const targetPath = join24(cwd, "AICLI.md");
|
|
15819
|
+
const force = args.includes("--force");
|
|
15820
|
+
if (existsSync29(targetPath) && !force) {
|
|
15821
|
+
ctx.send({ type: "info", message: `AICLI.md already exists at ${targetPath}
|
|
15822
|
+
Use /init --force to overwrite.` });
|
|
15823
|
+
return;
|
|
15824
|
+
}
|
|
15825
|
+
ctx.send({ type: "info", message: "\u{1F4DD} Scanning project structure..." });
|
|
15826
|
+
try {
|
|
15827
|
+
const projectInfo = scanProject(cwd);
|
|
15828
|
+
const prompt = buildInitPrompt(projectInfo, cwd);
|
|
15829
|
+
const content = await ctx.chatOnce(prompt, { temperature: 0.3, maxTokens: 4096 });
|
|
15830
|
+
writeFileSync3(targetPath, content, "utf-8");
|
|
15831
|
+
ctx.send({ type: "info", message: `\u2713 Generated: ${targetPath} (${content.length} chars)
|
|
15832
|
+
Use /context reload to load it.` });
|
|
15833
|
+
} catch (err) {
|
|
15834
|
+
ctx.send({ type: "error", message: `Failed to generate AICLI.md: ${err.message}` });
|
|
15835
|
+
}
|
|
15870
15836
|
}
|
|
15837
|
+
async function handleScaffold(args, ctx) {
|
|
15838
|
+
const description = args.join(" ").trim();
|
|
15839
|
+
if (!description) {
|
|
15840
|
+
ctx.send({ type: "error", message: "Usage: /scaffold <project description>" });
|
|
15841
|
+
return;
|
|
15842
|
+
}
|
|
15843
|
+
ctx.send({ type: "info", message: "\u{1F3D7}\uFE0F Generating project scaffold..." });
|
|
15844
|
+
const scaffoldPrompt = `Please scaffold a project based on this description: ${description}
|
|
15871
15845
|
|
|
15872
|
-
|
|
15873
|
-
|
|
15874
|
-
if (n < 1024) return `${n} B`;
|
|
15875
|
-
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
15876
|
-
if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
|
15877
|
-
return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
|
15846
|
+
Create the necessary files and directory structure. Use write_file and bash tools to set up the project.`;
|
|
15847
|
+
await ctx.handleChat(scaffoldPrompt);
|
|
15878
15848
|
}
|
|
15879
|
-
|
|
15880
|
-
|
|
15881
|
-
|
|
15882
|
-
const
|
|
15883
|
-
|
|
15884
|
-
|
|
15885
|
-
|
|
15886
|
-
|
|
15887
|
-
|
|
15888
|
-
|
|
15889
|
-
|
|
15890
|
-
|
|
15891
|
-
|
|
15892
|
-
|
|
15893
|
-
|
|
15894
|
-
else if (nc?.status === "skipped") versionLine += ` ${D(`(npm check skipped: ${nc.reason ?? "unknown"})`)}`;
|
|
15895
|
-
out.push(versionLine);
|
|
15896
|
-
out.push(` node: ${report.node}`);
|
|
15897
|
-
out.push(` platform: ${report.platform}`);
|
|
15898
|
-
out.push(` cwd: ${report.cwd}`);
|
|
15899
|
-
out.push(` project: ${report.projectRoot}`);
|
|
15900
|
-
out.push(` config: ${report.configDir}`);
|
|
15901
|
-
out.push("");
|
|
15902
|
-
out.push(B("API Keys:"));
|
|
15903
|
-
for (const p of report.providers) {
|
|
15904
|
-
out.push(` ${p.configured ? G("\u2713") : D("\u25CB")} ${p.id.padEnd(14)} ${p.configured ? G("configured") : D("not configured")}`);
|
|
15849
|
+
|
|
15850
|
+
// src/web/commands/memory-commands.ts
|
|
15851
|
+
async function handleSystem(args, ctx) {
|
|
15852
|
+
const text = args.join(" ").trim();
|
|
15853
|
+
if (!text) {
|
|
15854
|
+
const current = ctx.activeSystemPrompt;
|
|
15855
|
+
if (current) {
|
|
15856
|
+
const preview = current.length > 500 ? current.slice(0, 500) + "..." : current;
|
|
15857
|
+
ctx.send({ type: "info", message: `\u{1F4CB} Current system prompt (${current.length} chars):
|
|
15858
|
+
|
|
15859
|
+
${preview}` });
|
|
15860
|
+
} else {
|
|
15861
|
+
ctx.send({ type: "info", message: "No custom system prompt set. Using default.\nUsage: /system <prompt>" });
|
|
15862
|
+
}
|
|
15863
|
+
return;
|
|
15905
15864
|
}
|
|
15906
|
-
|
|
15907
|
-
|
|
15908
|
-
|
|
15909
|
-
|
|
15910
|
-
|
|
15911
|
-
|
|
15865
|
+
if (text === "clear" || text === "reset") {
|
|
15866
|
+
ctx.activeSystemPrompt = ctx.loadContextFiles();
|
|
15867
|
+
ctx.send({ type: "info", message: "\u2713 System prompt reset to default (context files)." });
|
|
15868
|
+
} else {
|
|
15869
|
+
ctx.activeSystemPrompt = text;
|
|
15870
|
+
ctx.send({ type: "info", message: `\u2713 System prompt set (${text.length} chars).` });
|
|
15912
15871
|
}
|
|
15913
|
-
|
|
15914
|
-
|
|
15915
|
-
|
|
15916
|
-
|
|
15872
|
+
}
|
|
15873
|
+
async function handleContext(_args, ctx) {
|
|
15874
|
+
const sub = _args[0];
|
|
15875
|
+
if (sub === "reload") {
|
|
15876
|
+
ctx.activeSystemPrompt = ctx.loadContextFiles();
|
|
15877
|
+
ctx.send({ type: "info", message: ctx.activeSystemPrompt ? `\u2713 Context reloaded (${ctx.activeSystemPrompt.length} chars).` : "\u2713 No context files found." });
|
|
15878
|
+
return;
|
|
15879
|
+
}
|
|
15880
|
+
const result = ctx.contextLoadResult;
|
|
15881
|
+
const layers = ["\u{1F4DA} **Context Layers:**", ""];
|
|
15882
|
+
if (result && result.layers.length > 0) {
|
|
15883
|
+
const labels = { global: "Global ", project: "Project", plugin: "Plugin ", local: "Local " };
|
|
15884
|
+
for (const layer of result.layers) {
|
|
15885
|
+
layers.push(` \u2713 ${labels[layer.level] ?? layer.level}: ${layer.displayPath} (${layer.charCount} chars${layer.truncated ? ", truncated" : ""})`);
|
|
15917
15886
|
}
|
|
15887
|
+
layers.push(` Total: ${result.totalChars} chars (${result.layers.length} layer${result.layers.length > 1 ? "s" : ""})`);
|
|
15888
|
+
} else {
|
|
15889
|
+
layers.push(" No context files loaded.");
|
|
15890
|
+
layers.push(" Search order per layer: AICLI.override.md \u2192 AGENTS.override.md \u2192 AICLI.md \u2192 CLAUDE.md \u2192 AGENTS.md");
|
|
15918
15891
|
}
|
|
15919
|
-
|
|
15920
|
-
|
|
15921
|
-
|
|
15922
|
-
|
|
15923
|
-
|
|
15924
|
-
const overrides = Object.entries(np.toolOverrides).map(([k, v]) => `${k}=${v}`).join(", ");
|
|
15925
|
-
if (overrides) out.push(` tool overrides: ${overrides}`);
|
|
15926
|
-
out.push("");
|
|
15927
|
-
out.push(B("Hooks / Plugins:"));
|
|
15928
|
-
out.push(` hooks: ${report.hooks.enabled ? G("enabled") : D("disabled")} \xB7 total ${report.hooks.total} \xB7 project ${report.hooks.project} \xB7 pending trust ${report.hooks.pendingTrust} \xB7 disabled ${report.hooks.disabled}`);
|
|
15929
|
-
out.push(` plugins: installed ${report.plugins.installed} \xB7 active ${report.plugins.active} \xB7 disabled ${report.plugins.disabled} \xB7 untrusted ${report.plugins.untrusted} \xB7 invalid ${report.plugins.invalid}`);
|
|
15930
|
-
out.push(` plugin assets: ${report.plugins.skillDirs} skill dir(s), ${report.plugins.commandDirs} command dir(s), ${report.plugins.agentDirs} agent dir(s), ${report.plugins.mcpServers} MCP server(s)`);
|
|
15931
|
-
if (report.plugins.entries.length > 0) {
|
|
15932
|
-
for (const p of report.plugins.entries.slice(0, 5)) {
|
|
15933
|
-
const state2 = p.valid ? `${p.enabled ? "enabled" : "disabled"} \xB7 ${p.trusted ? "trusted" : "untrusted"}` : `invalid \xB7 ${p.error ?? ""}`;
|
|
15934
|
-
out.push(` ${p.valid ? G("\u2713") : R("\u2717")} ${p.name}@${p.version} ${D(state2)}`);
|
|
15892
|
+
if (result && result.skipped.length > 0) {
|
|
15893
|
+
layers.push("");
|
|
15894
|
+
layers.push("Skipped:");
|
|
15895
|
+
for (const skipped of result.skipped.slice(0, 5)) {
|
|
15896
|
+
layers.push(` - ${skipped.displayPath}: ${skipped.reason}${skipped.message ? ` (${skipped.message})` : ""}`);
|
|
15935
15897
|
}
|
|
15936
15898
|
}
|
|
15937
|
-
|
|
15938
|
-
|
|
15939
|
-
|
|
15940
|
-
|
|
15941
|
-
|
|
15942
|
-
|
|
15943
|
-
|
|
15899
|
+
layers.push("");
|
|
15900
|
+
layers.push(`Total prompt length: ${ctx.activeSystemPrompt?.length ?? 0} chars`);
|
|
15901
|
+
layers.push("Use /context reload to refresh.");
|
|
15902
|
+
ctx.send({ type: "info", message: layers.join("\n") });
|
|
15903
|
+
}
|
|
15904
|
+
async function handleIndex(args, ctx) {
|
|
15905
|
+
const sub = (args[0] ?? "status").toLowerCase();
|
|
15906
|
+
const root = process.cwd();
|
|
15907
|
+
const { loadIndex: loadIndex2, clearIndex } = await import("./store-VO37H6LS.js");
|
|
15908
|
+
const { indexProject: indexProject2 } = await import("./indexer-4WWS3VIL.js");
|
|
15909
|
+
const { loadVectorStore, clearVectorStore } = await import("./vector-store-JBAE6PS4.js");
|
|
15910
|
+
if (sub === "status") {
|
|
15911
|
+
const idx = loadIndex2(root);
|
|
15912
|
+
const vec = loadVectorStore(root);
|
|
15913
|
+
if (!idx) {
|
|
15914
|
+
ctx.send({ type: "info", message: `No symbol index for ${root}. Run /index rebuild.` });
|
|
15915
|
+
return;
|
|
15916
|
+
}
|
|
15917
|
+
const semantic = vec ? `${vec.count} vectors \xD7 ${vec.dim}-dim` : "not built \u2014 run /index semantic-rebuild to enable search_code";
|
|
15918
|
+
ctx.send({
|
|
15919
|
+
type: "info",
|
|
15920
|
+
message: [
|
|
15921
|
+
"\u{1F50E} Symbol index:",
|
|
15922
|
+
` Root: ${idx.root}`,
|
|
15923
|
+
` Generated: ${idx.generated}`,
|
|
15924
|
+
` Files: ${idx.fileCount}`,
|
|
15925
|
+
` Symbols: ${idx.symbolCount}`,
|
|
15926
|
+
` Semantic: ${semantic}`
|
|
15927
|
+
].join("\n")
|
|
15928
|
+
});
|
|
15929
|
+
} else if (sub === "rebuild") {
|
|
15930
|
+
ctx.send({ type: "info", message: `Indexing ${root}\u2026` });
|
|
15931
|
+
const { stats } = await indexProject2(root, { force: true });
|
|
15932
|
+
ctx.send({
|
|
15933
|
+
type: "info",
|
|
15934
|
+
message: `\u2713 Indexed ${stats.filesParsed} files (${stats.symbols} symbols) in ${stats.durationMs}ms`
|
|
15935
|
+
});
|
|
15936
|
+
} else if (sub === "clear") {
|
|
15937
|
+
clearIndex(root);
|
|
15938
|
+
clearVectorStore(root);
|
|
15939
|
+
ctx.send({ type: "info", message: `\u2713 Cleared symbol + semantic index for ${root}` });
|
|
15940
|
+
} else if (sub === "semantic-rebuild") {
|
|
15941
|
+
const idx = loadIndex2(root);
|
|
15942
|
+
if (!idx) {
|
|
15943
|
+
ctx.send({ type: "error", message: "No symbol index yet. Run /index rebuild first." });
|
|
15944
|
+
return;
|
|
15945
|
+
}
|
|
15946
|
+
if (idx.symbolCount === 0) {
|
|
15947
|
+
ctx.send({
|
|
15948
|
+
type: "error",
|
|
15949
|
+
message: "No code symbols to embed. Semantic search needs TS/TSX/JS/JSX/Python source files \u2014 this project appears to have none. Not downloading the embedding model."
|
|
15950
|
+
});
|
|
15951
|
+
return;
|
|
15952
|
+
}
|
|
15953
|
+
ctx.send({
|
|
15954
|
+
type: "info",
|
|
15955
|
+
message: `Building semantic index for ${idx.symbolCount} symbols\u2026 (first run downloads ~117 MB model)`
|
|
15956
|
+
});
|
|
15957
|
+
try {
|
|
15958
|
+
const { rebuildSemanticIndex } = await import("./semantic-FF6DDJI6.js");
|
|
15959
|
+
const stats = await rebuildSemanticIndex(root);
|
|
15960
|
+
const first = stats.modelFirstLoadMs ? ` (model load+first batch ${stats.modelFirstLoadMs}ms)` : "";
|
|
15961
|
+
ctx.send({
|
|
15962
|
+
type: "info",
|
|
15963
|
+
message: `\u2713 Embedded ${stats.symbolsEmbedded} symbols in ${stats.durationMs}ms${first}`
|
|
15964
|
+
});
|
|
15965
|
+
} catch (err) {
|
|
15966
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
15967
|
+
ctx.send({ type: "error", message: `Semantic rebuild failed: ${msg}` });
|
|
15968
|
+
}
|
|
15969
|
+
} else if (sub === "semantic-clear") {
|
|
15970
|
+
clearVectorStore(root);
|
|
15971
|
+
ctx.send({ type: "info", message: "\u2713 Cleared semantic index (symbol index preserved)" });
|
|
15972
|
+
} else {
|
|
15973
|
+
ctx.send({
|
|
15974
|
+
type: "error",
|
|
15975
|
+
message: `Unknown subcommand: ${sub}. Use status/rebuild/clear/semantic-rebuild/semantic-clear.`
|
|
15976
|
+
});
|
|
15944
15977
|
}
|
|
15945
|
-
|
|
15946
|
-
|
|
15947
|
-
|
|
15948
|
-
|
|
15949
|
-
|
|
15978
|
+
}
|
|
15979
|
+
async function handleMemory(args, ctx) {
|
|
15980
|
+
const sub = args[0];
|
|
15981
|
+
if (sub === "add" && args.length > 1) {
|
|
15982
|
+
ctx.memoryAdd(args.slice(1).join(" "));
|
|
15983
|
+
} else if (sub === "clear") {
|
|
15984
|
+
ctx.memoryClear();
|
|
15985
|
+
} else {
|
|
15986
|
+
ctx.memoryShow();
|
|
15950
15987
|
}
|
|
15951
|
-
|
|
15952
|
-
|
|
15953
|
-
|
|
15954
|
-
|
|
15955
|
-
|
|
15956
|
-
|
|
15957
|
-
|
|
15988
|
+
}
|
|
15989
|
+
async function handleAddDir(args, ctx) {
|
|
15990
|
+
const sub = args[0]?.trim();
|
|
15991
|
+
if (!sub) {
|
|
15992
|
+
ctx.send({ type: "info", message: ctx.addedDirs.size > 0 ? `\u{1F4C1} Added directories:
|
|
15993
|
+
${[...ctx.addedDirs].map((d) => ` \u2022 ${d}`).join("\n")}
|
|
15994
|
+
|
|
15995
|
+
Use /add-dir remove to clear.` : "No directories added.\nUsage: /add-dir <path> | /add-dir remove" });
|
|
15996
|
+
return;
|
|
15958
15997
|
}
|
|
15959
|
-
|
|
15960
|
-
|
|
15961
|
-
|
|
15962
|
-
|
|
15963
|
-
if (ts.topUsed.length > 0) {
|
|
15964
|
-
out.push(` ${D("top used:")}`);
|
|
15965
|
-
for (const t of ts.topUsed) out.push(` ${t.name.padEnd(20)} ${String(t.calls).padStart(6)} calls \xB7 avg ${t.calls ? (t.totalDurationMs / t.calls).toFixed(0) : "0"}ms`);
|
|
15998
|
+
if (sub === "remove" || sub === "clear") {
|
|
15999
|
+
ctx.addedDirs.clear();
|
|
16000
|
+
ctx.send({ type: "info", message: "\u2713 All added directories removed from context." });
|
|
16001
|
+
return;
|
|
15966
16002
|
}
|
|
15967
|
-
|
|
15968
|
-
|
|
15969
|
-
|
|
16003
|
+
const dirPath = resolve10(sub);
|
|
16004
|
+
if (!existsSync29(dirPath)) {
|
|
16005
|
+
ctx.send({ type: "error", message: `Directory not found: ${dirPath}` });
|
|
16006
|
+
return;
|
|
15970
16007
|
}
|
|
15971
|
-
|
|
15972
|
-
|
|
15973
|
-
|
|
15974
|
-
index: "symbol index -> /index clear",
|
|
15975
|
-
history: "conversation history",
|
|
15976
|
-
logs: "crash logs",
|
|
15977
|
-
"memory-index": "chat memory index -> /memory index-clear",
|
|
15978
|
-
plugins: "installed package plugins"
|
|
15979
|
-
};
|
|
15980
|
-
out.push(B("Disk Usage (~/.aicli):"));
|
|
15981
|
-
out.push(` total: ${formatBytes(report.diskUsage.totalBytes)}`);
|
|
15982
|
-
for (const e of report.diskUsage.entries.slice(0, 10)) {
|
|
15983
|
-
const note = dirAnnotations[e.name];
|
|
15984
|
-
out.push(` ${e.name.padEnd(18)} ${formatBytes(e.bytes)}${note ? ` ${D(`\xB7 ${note}`)}` : ""}`);
|
|
16008
|
+
if (!statSync12(dirPath).isDirectory()) {
|
|
16009
|
+
ctx.send({ type: "error", message: `Not a directory: ${dirPath}` });
|
|
16010
|
+
return;
|
|
15985
16011
|
}
|
|
15986
|
-
|
|
15987
|
-
|
|
15988
|
-
|
|
15989
|
-
return out.join("\n");
|
|
16012
|
+
ctx.addedDirs.add(dirPath);
|
|
16013
|
+
ctx.send({ type: "info", message: `\u2713 Added directory: ${dirPath}
|
|
16014
|
+
It will be included in AI context for subsequent messages.` });
|
|
15990
16015
|
}
|
|
15991
16016
|
|
|
15992
16017
|
// src/web/commands/security-commands.ts
|
|
15993
16018
|
async function handleConfig(args, ctx) {
|
|
15994
|
-
|
|
15995
|
-
|
|
15996
|
-
|
|
15997
|
-
ctx.send({ type: "info", message: `\u2699\uFE0F Configuration:
|
|
16019
|
+
const sub = args[0];
|
|
16020
|
+
if (sub === "show" || !sub) {
|
|
16021
|
+
ctx.send({ type: "info", message: `\u2699\uFE0F Configuration:
|
|
15998
16022
|
\`\`\`json
|
|
15999
16023
|
${ctx.config.toFormattedJSON()}
|
|
16000
16024
|
\`\`\`` });
|
|
16001
|
-
|
|
16002
|
-
|
|
16003
|
-
|
|
16004
|
-
|
|
16005
|
-
|
|
16006
|
-
|
|
16007
|
-
|
|
16008
|
-
|
|
16009
|
-
|
|
16010
|
-
|
|
16011
|
-
}
|
|
16012
|
-
} else {
|
|
16013
|
-
ctx.send({ type: "info", message: "Usage: /config [show] | /config get <key> | /config set <key> <value>" });
|
|
16025
|
+
} else if (sub === "get" && args[1]) {
|
|
16026
|
+
const val = ctx.config.getByPath(args[1]);
|
|
16027
|
+
const display = typeof val === "object" ? JSON.stringify(val, null, 2) : String(val ?? "undefined");
|
|
16028
|
+
ctx.send({ type: "info", message: `${args[1]} = ${display}` });
|
|
16029
|
+
} else if (sub === "set" && args[1] && args[2] !== void 0) {
|
|
16030
|
+
try {
|
|
16031
|
+
ctx.config.setByPath(args[1], args.slice(2).join(" "));
|
|
16032
|
+
ctx.send({ type: "info", message: `\u2713 ${args[1]} = ${args.slice(2).join(" ")}` });
|
|
16033
|
+
} catch (err) {
|
|
16034
|
+
ctx.send({ type: "error", message: `Config error: ${err.message}` });
|
|
16014
16035
|
}
|
|
16036
|
+
} else {
|
|
16037
|
+
ctx.send({ type: "info", message: "Usage: /config [show] | /config get <key> | /config set <key> <value>" });
|
|
16015
16038
|
}
|
|
16016
16039
|
}
|
|
16017
|
-
async function handleDoctor(
|
|
16018
|
-
{
|
|
16019
|
-
|
|
16020
|
-
|
|
16021
|
-
|
|
16022
|
-
|
|
16023
|
-
|
|
16024
|
-
|
|
16025
|
-
|
|
16026
|
-
|
|
16027
|
-
|
|
16028
|
-
|
|
16029
|
-
|
|
16030
|
-
|
|
16031
|
-
|
|
16032
|
-
|
|
16033
|
-
|
|
16034
|
-
|
|
16035
|
-
|
|
16036
|
-
|
|
16037
|
-
|
|
16038
|
-
|
|
16039
|
-
|
|
16040
|
-
|
|
16041
|
-
|
|
16042
|
-
|
|
16043
|
-
|
|
16044
|
-
|
|
16045
|
-
|
|
16046
|
-
|
|
16047
|
-
|
|
16048
|
-
|
|
16049
|
-
|
|
16050
|
-
|
|
16051
|
-
|
|
16052
|
-
|
|
16053
|
-
|
|
16054
|
-
"Report at: https://github.com/jinzhengdong/ai-cli/issues"
|
|
16055
|
-
];
|
|
16056
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
16057
|
-
}
|
|
16040
|
+
async function handleDoctor(_args, ctx) {
|
|
16041
|
+
const report = buildDoctorReport({
|
|
16042
|
+
config: ctx.config,
|
|
16043
|
+
providers: ctx.providers,
|
|
16044
|
+
cwd: process.cwd(),
|
|
16045
|
+
mcpStatuses: ctx.mcpManager?.getStatus() ?? []
|
|
16046
|
+
});
|
|
16047
|
+
ctx.send({ type: "info", message: formatDoctorReport(report, false) });
|
|
16048
|
+
}
|
|
16049
|
+
async function handleBug(_args, ctx) {
|
|
16050
|
+
const session = ctx.sessions.current;
|
|
16051
|
+
const lines = [
|
|
16052
|
+
"\u{1F41B} **Bug Report Template**",
|
|
16053
|
+
"",
|
|
16054
|
+
"**Environment:**",
|
|
16055
|
+
` ai-cli version: ${VERSION}`,
|
|
16056
|
+
` Node.js: ${process.version}`,
|
|
16057
|
+
` OS: ${process.platform} ${process.arch}`,
|
|
16058
|
+
` Provider: ${ctx.currentProvider}`,
|
|
16059
|
+
` Model: ${ctx.currentModel}`,
|
|
16060
|
+
` Session: ${session?.id.slice(0, 8) ?? "none"} (${session?.messages.length ?? 0} messages)`,
|
|
16061
|
+
"",
|
|
16062
|
+
"**Description:**",
|
|
16063
|
+
"(Describe the bug)",
|
|
16064
|
+
"",
|
|
16065
|
+
"**Steps to reproduce:**",
|
|
16066
|
+
"1. ...",
|
|
16067
|
+
"",
|
|
16068
|
+
"**Expected behavior:**",
|
|
16069
|
+
"...",
|
|
16070
|
+
"",
|
|
16071
|
+
"**Actual behavior:**",
|
|
16072
|
+
"...",
|
|
16073
|
+
"",
|
|
16074
|
+
"Report at: https://github.com/jinzhengdong/ai-cli/issues"
|
|
16075
|
+
];
|
|
16076
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
16058
16077
|
}
|
|
16059
16078
|
|
|
16060
16079
|
// src/web/commands/agent-commands.ts
|
|
16061
16080
|
async function handleAuto(args, ctx) {
|
|
16062
|
-
|
|
16063
|
-
|
|
16064
|
-
|
|
16065
|
-
|
|
16066
|
-
|
|
16067
|
-
|
|
16068
|
-
|
|
16069
|
-
|
|
16070
|
-
|
|
16071
|
-
ctx.send({ type: "info", message: `Auto Mode: ${ctx.toolExecutor.sessionAutoMode ? "on" : "off"}
|
|
16081
|
+
const sub = args[0]?.toLowerCase() ?? "status";
|
|
16082
|
+
if (sub === "on") {
|
|
16083
|
+
ctx.toolExecutor.sessionAutoMode = true;
|
|
16084
|
+
ctx.send({ type: "info", message: "\u26A1 Auto Mode ON \u2014 low-risk actions may auto-run; high-risk actions still confirm or deny. Auto Mode is session-scoped." });
|
|
16085
|
+
} else if (sub === "off") {
|
|
16086
|
+
ctx.toolExecutor.sessionAutoMode = false;
|
|
16087
|
+
ctx.send({ type: "info", message: "Auto Mode disabled." });
|
|
16088
|
+
} else if (sub === "status") {
|
|
16089
|
+
ctx.send({ type: "info", message: `Auto Mode: ${ctx.toolExecutor.sessionAutoMode ? "on" : "off"}
|
|
16072
16090
|
Scope: current session only
|
|
16073
16091
|
Denied: /permissions recently-denied` });
|
|
16074
|
-
|
|
16075
|
-
|
|
16076
|
-
}
|
|
16077
|
-
ctx.sendStatus();
|
|
16092
|
+
} else {
|
|
16093
|
+
ctx.send({ type: "error", message: "Usage: /auto [on|off|status]" });
|
|
16078
16094
|
}
|
|
16095
|
+
ctx.sendStatus();
|
|
16079
16096
|
}
|
|
16080
16097
|
async function handlePermissions(args, ctx) {
|
|
16081
|
-
|
|
16082
|
-
|
|
16083
|
-
|
|
16084
|
-
|
|
16085
|
-
|
|
16086
|
-
ctx.send({ type: "info", message: "No recently denied Auto Mode actions." });
|
|
16087
|
-
} else {
|
|
16088
|
-
const lines = [`Recently denied Auto Mode actions (${rows.length})`, ""];
|
|
16089
|
-
for (const r of rows.slice(0, 20)) {
|
|
16090
|
-
lines.push(`- ${r.timestamp} ${r.tool} ${r.ruleId}`);
|
|
16091
|
-
lines.push(` ${r.reason}`);
|
|
16092
|
-
lines.push(` ${r.argsPreview}`);
|
|
16093
|
-
}
|
|
16094
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
16095
|
-
}
|
|
16096
|
-
} else if (sub === "clear-denied") {
|
|
16097
|
-
clearRecentlyDeniedAutoActions();
|
|
16098
|
-
ctx.send({ type: "info", message: "Cleared recently denied Auto Mode actions." });
|
|
16098
|
+
const sub = args[0]?.toLowerCase() ?? "recently-denied";
|
|
16099
|
+
if (sub === "recently-denied") {
|
|
16100
|
+
const rows = getRecentlyDeniedAutoActions();
|
|
16101
|
+
if (rows.length === 0) {
|
|
16102
|
+
ctx.send({ type: "info", message: "No recently denied Auto Mode actions." });
|
|
16099
16103
|
} else {
|
|
16100
|
-
|
|
16104
|
+
const lines = [`Recently denied Auto Mode actions (${rows.length})`, ""];
|
|
16105
|
+
for (const r of rows.slice(0, 20)) {
|
|
16106
|
+
lines.push(`- ${r.timestamp} ${r.tool} ${r.ruleId}`);
|
|
16107
|
+
lines.push(` ${r.reason}`);
|
|
16108
|
+
lines.push(` ${r.argsPreview}`);
|
|
16109
|
+
}
|
|
16110
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
16101
16111
|
}
|
|
16112
|
+
} else if (sub === "clear-denied") {
|
|
16113
|
+
clearRecentlyDeniedAutoActions();
|
|
16114
|
+
ctx.send({ type: "info", message: "Cleared recently denied Auto Mode actions." });
|
|
16115
|
+
} else {
|
|
16116
|
+
ctx.send({ type: "error", message: "Usage: /permissions recently-denied|clear-denied" });
|
|
16102
16117
|
}
|
|
16103
16118
|
}
|
|
16104
16119
|
async function handleYolo(args, ctx) {
|
|
16105
|
-
|
|
16106
|
-
|
|
16107
|
-
|
|
16108
|
-
|
|
16109
|
-
|
|
16110
|
-
|
|
16111
|
-
|
|
16112
|
-
ctx.send({ type: "info", message: "\u26A1 YOLO mode ON \u2014 write tools auto-approved; destructive tools still require confirmation.\nUse /yolo off to re-enable write confirmations." });
|
|
16113
|
-
}
|
|
16120
|
+
const sub = args[0]?.toLowerCase();
|
|
16121
|
+
if (sub === "off") {
|
|
16122
|
+
ctx.toolExecutor.sessionAutoApprove = false;
|
|
16123
|
+
ctx.send({ type: "info", message: "\u{1F512} Auto-approve disabled \u2014 confirmations restored for this session." });
|
|
16124
|
+
} else {
|
|
16125
|
+
ctx.toolExecutor.sessionAutoApprove = true;
|
|
16126
|
+
ctx.send({ type: "info", message: "\u26A1 YOLO mode ON \u2014 write tools auto-approved; destructive tools still require confirmation.\nUse /yolo off to re-enable write confirmations." });
|
|
16114
16127
|
}
|
|
16115
16128
|
}
|
|
16116
16129
|
async function handleAgent(args, ctx) {
|
|
16117
|
-
|
|
16118
|
-
|
|
16119
|
-
|
|
16120
|
-
|
|
16121
|
-
|
|
16122
|
-
|
|
16123
|
-
|
|
16124
|
-
|
|
16125
|
-
|
|
16126
|
-
|
|
16127
|
-
|
|
16128
|
-
|
|
16129
|
-
|
|
16130
|
+
const sub = args[0]?.toLowerCase() ?? "summary";
|
|
16131
|
+
if (sub === "list") {
|
|
16132
|
+
const agents = listAgentConfigs(ctx.config.getConfigDir());
|
|
16133
|
+
const lines = [`Agents (${agents.length})`, ""];
|
|
16134
|
+
for (const agent of agents) {
|
|
16135
|
+
const active = agent.name === getPreferredAgentName() ? " *" : " ";
|
|
16136
|
+
const model = [agent.provider, agent.model].filter(Boolean).join("/") || "inherit";
|
|
16137
|
+
lines.push(`${active} ${agent.name} [${agent.source ?? "builtin"}] \u2014 ${agent.description ?? ""}`);
|
|
16138
|
+
lines.push(` model: ${model} \xB7 permission: ${agent.permissionProfile ?? "workspace-write"} \xB7 maxRounds: ${agent.maxToolRounds ?? "inherit"}`);
|
|
16139
|
+
}
|
|
16140
|
+
lines.push("", "Config dirs: ~/.aicli/agents/ and .aicli/agents/");
|
|
16141
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
16142
|
+
return;
|
|
16143
|
+
}
|
|
16144
|
+
if (sub === "switch") {
|
|
16145
|
+
const name = args[1];
|
|
16146
|
+
if (!name) {
|
|
16147
|
+
ctx.send({ type: "error", message: "Usage: /agent switch <name>" });
|
|
16148
|
+
return;
|
|
16130
16149
|
}
|
|
16131
|
-
|
|
16132
|
-
|
|
16133
|
-
|
|
16134
|
-
|
|
16135
|
-
}
|
|
16136
|
-
const agent = resolveAgentConfig(name, ctx.config.getConfigDir());
|
|
16137
|
-
if (!agent) {
|
|
16138
|
-
const available = listAgentConfigs(ctx.config.getConfigDir()).map((a) => a.name).join(", ");
|
|
16139
|
-
ctx.send({ type: "error", message: `Unknown agent: ${name}
|
|
16150
|
+
const agent = resolveAgentConfig(name, ctx.config.getConfigDir());
|
|
16151
|
+
if (!agent) {
|
|
16152
|
+
const available = listAgentConfigs(ctx.config.getConfigDir()).map((a) => a.name).join(", ");
|
|
16153
|
+
ctx.send({ type: "error", message: `Unknown agent: ${name}
|
|
16140
16154
|
Available: ${available}` });
|
|
16141
|
-
|
|
16142
|
-
setPreferredAgentName(agent.name);
|
|
16143
|
-
ctx.send({ type: "info", message: `Preferred spawn_agent role: ${agent.name}` });
|
|
16144
|
-
ctx.sendStatus();
|
|
16155
|
+
return;
|
|
16145
16156
|
}
|
|
16146
|
-
|
|
16147
|
-
|
|
16148
|
-
|
|
16149
|
-
|
|
16150
|
-
|
|
16157
|
+
setPreferredAgentName(agent.name);
|
|
16158
|
+
ctx.send({ type: "info", message: `Preferred spawn_agent role: ${agent.name}` });
|
|
16159
|
+
ctx.sendStatus();
|
|
16160
|
+
return;
|
|
16161
|
+
}
|
|
16162
|
+
if (sub === "stop") {
|
|
16163
|
+
const target = args[1] ?? "all";
|
|
16164
|
+
const count = requestAgentStop(target);
|
|
16165
|
+
ctx.send({ type: "info", message: count > 0 ? `Stop requested for ${count} running agent(s).` : "No matching running agents." });
|
|
16166
|
+
ctx.sendStatus();
|
|
16167
|
+
return;
|
|
16168
|
+
}
|
|
16169
|
+
if (sub === "summary") {
|
|
16170
|
+
const runs2 = listAgentRuns();
|
|
16171
|
+
if (runs2.length === 0) {
|
|
16172
|
+
ctx.send({ type: "info", message: "No sub-agent runs yet." });
|
|
16173
|
+
return;
|
|
16151
16174
|
}
|
|
16152
|
-
|
|
16153
|
-
|
|
16154
|
-
|
|
16155
|
-
|
|
16156
|
-
}
|
|
16157
|
-
|
|
16158
|
-
|
|
16159
|
-
lines.push(`${run.id} ${run.status} ${run.agentName} ${run.startedAt}`);
|
|
16160
|
-
lines.push(` task: ${run.task.slice(0, 120)}${run.task.length > 120 ? "..." : ""}`);
|
|
16161
|
-
if (run.toolNames.length > 0) lines.push(` tools: ${run.toolNames.join(", ")}`);
|
|
16162
|
-
if (run.summary) lines.push(` summary: ${run.summary.slice(0, 180)}${run.summary.length > 180 ? "..." : ""}`);
|
|
16163
|
-
if (run.error) lines.push(` error: ${run.error}`);
|
|
16164
|
-
}
|
|
16165
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
16175
|
+
const lines = [`Recent agent runs (${runs2.length})`, ""];
|
|
16176
|
+
for (const run of runs2.slice(0, 20)) {
|
|
16177
|
+
lines.push(`${run.id} ${run.status} ${run.agentName} ${run.startedAt}`);
|
|
16178
|
+
lines.push(` task: ${run.task.slice(0, 120)}${run.task.length > 120 ? "..." : ""}`);
|
|
16179
|
+
if (run.toolNames.length > 0) lines.push(` tools: ${run.toolNames.join(", ")}`);
|
|
16180
|
+
if (run.summary) lines.push(` summary: ${run.summary.slice(0, 180)}${run.summary.length > 180 ? "..." : ""}`);
|
|
16181
|
+
if (run.error) lines.push(` error: ${run.error}`);
|
|
16166
16182
|
}
|
|
16167
|
-
ctx.send({ type: "
|
|
16183
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
16184
|
+
return;
|
|
16168
16185
|
}
|
|
16186
|
+
ctx.send({ type: "error", message: "Usage: /agent list|switch <name>|stop <id|all>|summary" });
|
|
16169
16187
|
}
|
|
16170
16188
|
|
|
16171
16189
|
// src/web/commands/misc-commands.ts
|
|
16172
|
-
async function handleHelp(
|
|
16190
|
+
async function handleHelp(_args, ctx) {
|
|
16173
16191
|
ctx.send({
|
|
16174
16192
|
type: "info",
|
|
16175
16193
|
message: [
|
|
@@ -16227,90 +16245,87 @@ async function handleHelp(args, ctx) {
|
|
|
16227
16245
|
].join("\n")
|
|
16228
16246
|
});
|
|
16229
16247
|
}
|
|
16230
|
-
async function handleAbout(
|
|
16231
|
-
|
|
16232
|
-
|
|
16233
|
-
|
|
16234
|
-
|
|
16235
|
-
|
|
16236
|
-
|
|
16237
|
-
|
|
16238
|
-
|
|
16239
|
-
|
|
16240
|
-
|
|
16241
|
-
|
|
16242
|
-
|
|
16243
|
-
|
|
16244
|
-
|
|
16245
|
-
|
|
16246
|
-
|
|
16247
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
16248
|
-
}
|
|
16248
|
+
async function handleAbout(_args, ctx) {
|
|
16249
|
+
const providerList = ctx.providers.listAll();
|
|
16250
|
+
const toolCount = ctx.toolRegistry.getDefinitions().length;
|
|
16251
|
+
const mcpTools = ctx.mcpManager?.getTotalToolCount() ?? 0;
|
|
16252
|
+
const lines = [
|
|
16253
|
+
`\u{1F916} **ai-cli** v${VERSION}`,
|
|
16254
|
+
"",
|
|
16255
|
+
`${DESCRIPTION}`,
|
|
16256
|
+
`Author: ${AUTHOR} <${AUTHOR_EMAIL}>`,
|
|
16257
|
+
"",
|
|
16258
|
+
`**Providers:** ${providerList.length} (${providerList.map((p) => p.id).join(", ")})`,
|
|
16259
|
+
`**Tools:** ${toolCount + mcpTools} (${toolCount} built-in${mcpTools > 0 ? ` + ${mcpTools} MCP` : ""})`,
|
|
16260
|
+
`**REPL Commands:** 37`,
|
|
16261
|
+
"",
|
|
16262
|
+
`GitHub: https://github.com/jinzhengdong/ai-cli`
|
|
16263
|
+
];
|
|
16264
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
16249
16265
|
}
|
|
16250
16266
|
async function handleSearch(args, ctx) {
|
|
16251
|
-
|
|
16252
|
-
|
|
16253
|
-
|
|
16254
|
-
|
|
16255
|
-
|
|
16256
|
-
|
|
16257
|
-
|
|
16258
|
-
|
|
16259
|
-
|
|
16260
|
-
|
|
16261
|
-
|
|
16262
|
-
|
|
16263
|
-
|
|
16264
|
-
|
|
16265
|
-
|
|
16266
|
-
|
|
16267
|
-
|
|
16268
|
-
|
|
16269
|
-
}
|
|
16270
|
-
lines.push("");
|
|
16267
|
+
const query = args.join(" ").trim();
|
|
16268
|
+
if (!query) {
|
|
16269
|
+
ctx.send({ type: "error", message: "Usage: /search <keyword>" });
|
|
16270
|
+
return;
|
|
16271
|
+
}
|
|
16272
|
+
const results = ctx.sessions.searchMessages(query);
|
|
16273
|
+
if (results.length === 0) {
|
|
16274
|
+
ctx.send({ type: "info", message: `No sessions found containing "${query}".` });
|
|
16275
|
+
return;
|
|
16276
|
+
}
|
|
16277
|
+
const lines = [`\u{1F50D} Found ${results.length} session(s) containing "${query}"`, ""];
|
|
16278
|
+
for (const r of results) {
|
|
16279
|
+
const { sessionMeta, matches } = r;
|
|
16280
|
+
const dateStr = sessionMeta.updated instanceof Date ? sessionMeta.updated.toLocaleDateString() : String(sessionMeta.updated);
|
|
16281
|
+
lines.push(`**${sessionMeta.id.slice(0, 8)}** [${dateStr}] ${sessionMeta.provider} / ${sessionMeta.model}`);
|
|
16282
|
+
if (sessionMeta.title) lines.push(` ${sessionMeta.title}`);
|
|
16283
|
+
for (const m of matches) {
|
|
16284
|
+
const icon = m.role === "user" ? "\u{1F464}" : "\u{1F916}";
|
|
16285
|
+
lines.push(` ${icon} ${m.snippet}`);
|
|
16271
16286
|
}
|
|
16272
|
-
lines.push("
|
|
16273
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
16287
|
+
lines.push("");
|
|
16274
16288
|
}
|
|
16289
|
+
lines.push("Use /session load <id> to resume a session.");
|
|
16290
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
16275
16291
|
}
|
|
16276
16292
|
async function handleUndo(args, ctx) {
|
|
16277
|
-
|
|
16278
|
-
|
|
16279
|
-
|
|
16280
|
-
|
|
16281
|
-
|
|
16282
|
-
|
|
16283
|
-
}
|
|
16284
|
-
const lines = [`\u{1F4CB} Undo Stack (${history.length} entries, newest last):`, ""];
|
|
16285
|
-
history.forEach((entry, i) => {
|
|
16286
|
-
const timeStr = entry.timestamp.toLocaleTimeString();
|
|
16287
|
-
const typeTag = entry.isDirectory ? "[dir]" : entry.previousContent === null ? "[new]" : "[mod]";
|
|
16288
|
-
lines.push(` ${String(i + 1).padStart(3)} ${typeTag} ${entry.description} ${timeStr}`);
|
|
16289
|
-
});
|
|
16290
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
16291
|
-
}
|
|
16292
|
-
const n = sub ? parseInt(sub, 10) : 1;
|
|
16293
|
-
if (isNaN(n) || n < 1) {
|
|
16294
|
-
ctx.send({ type: "info", message: "Usage: /undo [list | <n>]" });
|
|
16295
|
-
}
|
|
16296
|
-
if (undoStack.depth === 0) {
|
|
16297
|
-
ctx.send({ type: "info", message: "Nothing to undo." });
|
|
16298
|
-
}
|
|
16299
|
-
const count = Math.min(n, undoStack.depth);
|
|
16300
|
-
const undoResults = [];
|
|
16301
|
-
for (let i = 0; i < count; i++) {
|
|
16302
|
-
const top = undoStack.peek();
|
|
16303
|
-
if (!top) {
|
|
16304
|
-
}
|
|
16305
|
-
const undoResult = undoStack.undo();
|
|
16306
|
-
if (undoResult) undoResults.push(undoResult.result);
|
|
16293
|
+
const sub = args.join(" ").trim();
|
|
16294
|
+
if (sub === "list") {
|
|
16295
|
+
const history = undoStack.getHistory();
|
|
16296
|
+
if (history.length === 0) {
|
|
16297
|
+
ctx.send({ type: "info", message: "Undo stack is empty." });
|
|
16298
|
+
return;
|
|
16307
16299
|
}
|
|
16308
|
-
|
|
16309
|
-
|
|
16310
|
-
|
|
16311
|
-
|
|
16300
|
+
const lines = [`\u{1F4CB} Undo Stack (${history.length} entries, newest last):`, ""];
|
|
16301
|
+
history.forEach((entry, i) => {
|
|
16302
|
+
const timeStr = entry.timestamp.toLocaleTimeString();
|
|
16303
|
+
const typeTag = entry.isDirectory ? "[dir]" : entry.previousContent === null ? "[new]" : "[mod]";
|
|
16304
|
+
lines.push(` ${String(i + 1).padStart(3)} ${typeTag} ${entry.description} ${timeStr}`);
|
|
16305
|
+
});
|
|
16306
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
16307
|
+
return;
|
|
16308
|
+
}
|
|
16309
|
+
const n = sub ? parseInt(sub, 10) : 1;
|
|
16310
|
+
if (isNaN(n) || n < 1) {
|
|
16311
|
+
ctx.send({ type: "info", message: "Usage: /undo [list | <n>]" });
|
|
16312
|
+
return;
|
|
16313
|
+
}
|
|
16314
|
+
if (undoStack.depth === 0) {
|
|
16315
|
+
ctx.send({ type: "info", message: "Nothing to undo." });
|
|
16316
|
+
return;
|
|
16317
|
+
}
|
|
16318
|
+
const count = Math.min(n, undoStack.depth);
|
|
16319
|
+
const undoResults = [];
|
|
16320
|
+
for (let i = 0; i < count; i++) {
|
|
16321
|
+
const undoResult = undoStack.undo();
|
|
16322
|
+
if (undoResult) undoResults.push(undoResult.result);
|
|
16323
|
+
}
|
|
16324
|
+
if (undoResults.length === 0) {
|
|
16325
|
+
ctx.send({ type: "info", message: "Nothing to undo." });
|
|
16326
|
+
} else {
|
|
16327
|
+
ctx.send({ type: "info", message: `\u2713 ${undoResults.length} operation(s) undone:
|
|
16312
16328
|
${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
16313
|
-
}
|
|
16314
16329
|
}
|
|
16315
16330
|
}
|
|
16316
16331
|
|
|
@@ -16359,20 +16374,6 @@ var webCommandHandlers = {
|
|
|
16359
16374
|
search: handleSearch,
|
|
16360
16375
|
undo: handleUndo
|
|
16361
16376
|
};
|
|
16362
|
-
function lastAssistantText(messages) {
|
|
16363
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
16364
|
-
const m = messages[i];
|
|
16365
|
-
if (!m || m.role !== "assistant") continue;
|
|
16366
|
-
const c = m.content;
|
|
16367
|
-
if (typeof c === "string") return c;
|
|
16368
|
-
if (Array.isArray(c)) {
|
|
16369
|
-
const text = c.filter((p) => p && p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
|
|
16370
|
-
if (text) return text;
|
|
16371
|
-
}
|
|
16372
|
-
return void 0;
|
|
16373
|
-
}
|
|
16374
|
-
return void 0;
|
|
16375
|
-
}
|
|
16376
16377
|
var SessionHandler = class {
|
|
16377
16378
|
ws;
|
|
16378
16379
|
config;
|
|
@@ -16551,10 +16552,10 @@ var SessionHandler = class {
|
|
|
16551
16552
|
return;
|
|
16552
16553
|
}
|
|
16553
16554
|
case "ask_user_response": {
|
|
16554
|
-
const
|
|
16555
|
-
if (
|
|
16555
|
+
const resolve14 = this.pendingAskUser.get(msg.requestId);
|
|
16556
|
+
if (resolve14) {
|
|
16556
16557
|
this.pendingAskUser.delete(msg.requestId);
|
|
16557
|
-
|
|
16558
|
+
resolve14(msg.answer);
|
|
16558
16559
|
}
|
|
16559
16560
|
return;
|
|
16560
16561
|
}
|
|
@@ -16567,10 +16568,10 @@ var SessionHandler = class {
|
|
|
16567
16568
|
case "memory_manage":
|
|
16568
16569
|
return this.handleMemoryManage(msg.action, msg.id, msg.expiresAt);
|
|
16569
16570
|
case "auto_pause_response": {
|
|
16570
|
-
const
|
|
16571
|
-
if (
|
|
16571
|
+
const resolve14 = this.pendingAutoPause.get(msg.requestId);
|
|
16572
|
+
if (resolve14) {
|
|
16572
16573
|
this.pendingAutoPause.delete(msg.requestId);
|
|
16573
|
-
|
|
16574
|
+
resolve14({ action: msg.action, message: msg.message });
|
|
16574
16575
|
}
|
|
16575
16576
|
return;
|
|
16576
16577
|
}
|
|
@@ -16585,10 +16586,10 @@ var SessionHandler = class {
|
|
|
16585
16586
|
this.hubOrchestrator?.abort();
|
|
16586
16587
|
return;
|
|
16587
16588
|
case "hub_steer": {
|
|
16588
|
-
const
|
|
16589
|
-
if (
|
|
16589
|
+
const resolve14 = this.pendingHubReview.get(msg.requestId);
|
|
16590
|
+
if (resolve14) {
|
|
16590
16591
|
this.pendingHubReview.delete(msg.requestId);
|
|
16591
|
-
|
|
16592
|
+
resolve14({ action: msg.action, message: msg.message });
|
|
16592
16593
|
}
|
|
16593
16594
|
return;
|
|
16594
16595
|
}
|
|
@@ -16601,9 +16602,9 @@ var SessionHandler = class {
|
|
|
16601
16602
|
runLifecycleHooks(mergePluginHooks(this.config.get("hooks") ?? void 0, this.config.getConfigDir()), "Stop", { sessionId: this.sessions.current?.id }, { configDir: this.config.getConfigDir() });
|
|
16602
16603
|
this.toolExecutor.cancelAll();
|
|
16603
16604
|
if (this.abortController) this.abortController.abort();
|
|
16604
|
-
for (const
|
|
16605
|
+
for (const resolve14 of this.pendingAskUser.values()) resolve14(null);
|
|
16605
16606
|
this.pendingAskUser.clear();
|
|
16606
|
-
for (const
|
|
16607
|
+
for (const resolve14 of this.pendingAutoPause.values()) resolve14({ action: "stop" });
|
|
16607
16608
|
this.pendingAutoPause.clear();
|
|
16608
16609
|
this.saveIfNeeded();
|
|
16609
16610
|
}
|
|
@@ -16737,9 +16738,9 @@ var SessionHandler = class {
|
|
|
16737
16738
|
this.hubOrchestrator = orchestrator;
|
|
16738
16739
|
orchestrator.onEvent = (event) => this.send({ type: "hub_event", event });
|
|
16739
16740
|
if (config.humanSteer) {
|
|
16740
|
-
orchestrator.onRoundReview = ({ round, maxRounds }) => new Promise((
|
|
16741
|
+
orchestrator.onRoundReview = ({ round, maxRounds }) => new Promise((resolve14) => {
|
|
16741
16742
|
const requestId = `hubrev_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
16742
|
-
this.pendingHubReview.set(requestId,
|
|
16743
|
+
this.pendingHubReview.set(requestId, resolve14);
|
|
16743
16744
|
this.send({ type: "hub_review", requestId, round, maxRounds });
|
|
16744
16745
|
});
|
|
16745
16746
|
}
|
|
@@ -17072,7 +17073,7 @@ Try: /compact to reduce context, /clear to reset, or switch to a larger-context
|
|
|
17072
17073
|
this.send({ type: "response_done", content, usage });
|
|
17073
17074
|
if (pendingTeeSave && isCleanDocumentBody(content)) {
|
|
17074
17075
|
try {
|
|
17075
|
-
|
|
17076
|
+
mkdirSync16(dirname10(pendingTeeSave), { recursive: true });
|
|
17076
17077
|
const bodyToSave = stripOuterCodeFence(content);
|
|
17077
17078
|
atomicWriteFileSync(pendingTeeSave, bodyToSave);
|
|
17078
17079
|
undoStack.push(pendingTeeSave, `save_last_response (deferred): ${pendingTeeSave}`);
|
|
@@ -17097,8 +17098,8 @@ Try: /compact to reduce context, /clear to reset, or switch to a larger-context
|
|
|
17097
17098
|
onMcpToolUsed: (name) => this.usedMcpToolNames.add(name),
|
|
17098
17099
|
requestAutoPause: async ({ effectiveRound, maxToolRounds: totalRounds, toolSummary }) => {
|
|
17099
17100
|
const requestId = `pause_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
17100
|
-
const pauseResp = await new Promise((
|
|
17101
|
-
this.pendingAutoPause.set(requestId,
|
|
17101
|
+
const pauseResp = await new Promise((resolve14) => {
|
|
17102
|
+
this.pendingAutoPause.set(requestId, resolve14);
|
|
17102
17103
|
this.send({
|
|
17103
17104
|
type: "auto_pause_request",
|
|
17104
17105
|
requestId,
|
|
@@ -17248,8 +17249,8 @@ ${summaryContent}`,
|
|
|
17248
17249
|
let isError = false;
|
|
17249
17250
|
let summary;
|
|
17250
17251
|
try {
|
|
17251
|
-
|
|
17252
|
-
fileStream =
|
|
17252
|
+
mkdirSync16(dirname10(saveToFile), { recursive: true });
|
|
17253
|
+
fileStream = createWriteStream3(saveToFile);
|
|
17253
17254
|
const teeSystemPrompt = stripToolCallReminder(systemPrompt ?? "") + CONTENT_ONLY_STREAM_REMINDER;
|
|
17254
17255
|
const teeExtraMessages = extraMessages.length > 0 ? [...extraMessages, { role: "user", content: TEE_FINAL_USER_NUDGE }] : [{ role: "user", content: TEE_FINAL_USER_NUDGE }];
|
|
17255
17256
|
const chatRequest = {
|
|
@@ -17277,8 +17278,8 @@ ${summaryContent}`,
|
|
|
17277
17278
|
}
|
|
17278
17279
|
if (chunk.done) break;
|
|
17279
17280
|
}
|
|
17280
|
-
await new Promise((
|
|
17281
|
-
fileStream.end((err) => err ? reject(err) :
|
|
17281
|
+
await new Promise((resolve14, reject) => {
|
|
17282
|
+
fileStream.end((err) => err ? reject(err) : resolve14());
|
|
17282
17283
|
});
|
|
17283
17284
|
const verdict = evaluateTeeContent(fullContent, saveToFile, priorContent);
|
|
17284
17285
|
if (verdict.kind === "reject") {
|
|
@@ -17304,7 +17305,7 @@ ${summaryContent}`,
|
|
|
17304
17305
|
} catch (err) {
|
|
17305
17306
|
if (fileStream) {
|
|
17306
17307
|
try {
|
|
17307
|
-
await new Promise((
|
|
17308
|
+
await new Promise((resolve14) => fileStream.end(() => resolve14()));
|
|
17308
17309
|
} catch {
|
|
17309
17310
|
}
|
|
17310
17311
|
}
|
|
@@ -17541,7 +17542,7 @@ ${entry.content}`;
|
|
|
17541
17542
|
const configDir = this.config.getConfigDir();
|
|
17542
17543
|
try {
|
|
17543
17544
|
atomicWriteFileSync(memoryStorePath(configDir), "");
|
|
17544
|
-
atomicWriteFileSync(
|
|
17545
|
+
atomicWriteFileSync(join26(configDir, MEMORY_FILE_NAME), "");
|
|
17545
17546
|
this.send({ type: "info", message: "\u{1F5D1}\uFE0F Persistent memory cleared." });
|
|
17546
17547
|
this.sendMemoryEntries();
|
|
17547
17548
|
} catch (err) {
|
|
@@ -17823,8 +17824,8 @@ async function setupProxy(configProxy) {
|
|
|
17823
17824
|
}
|
|
17824
17825
|
|
|
17825
17826
|
// src/web/auth.ts
|
|
17826
|
-
import { existsSync as
|
|
17827
|
-
import { join as
|
|
17827
|
+
import { existsSync as existsSync32, readFileSync as readFileSync25, writeFileSync as writeFileSync6, mkdirSync as mkdirSync17, readdirSync as readdirSync16, copyFileSync } from "fs";
|
|
17828
|
+
import { join as join27 } from "path";
|
|
17828
17829
|
import { createHmac, randomBytes, timingSafeEqual, pbkdf2Sync } from "crypto";
|
|
17829
17830
|
var USERS_FILE = "users.json";
|
|
17830
17831
|
var TOKEN_EXPIRY_HOURS = 24;
|
|
@@ -17839,7 +17840,7 @@ var AuthManager = class {
|
|
|
17839
17840
|
db;
|
|
17840
17841
|
constructor(baseDir) {
|
|
17841
17842
|
this.baseDir = baseDir;
|
|
17842
|
-
this.usersFile =
|
|
17843
|
+
this.usersFile = join27(baseDir, USERS_FILE);
|
|
17843
17844
|
this.db = this.loadOrCreate();
|
|
17844
17845
|
}
|
|
17845
17846
|
// ── Public API ─────────────────────────────────────────────────
|
|
@@ -17868,9 +17869,9 @@ var AuthManager = class {
|
|
|
17868
17869
|
}
|
|
17869
17870
|
const salt = randomBytes(16).toString("hex");
|
|
17870
17871
|
const passwordHash = this.hashPassword(password, salt);
|
|
17871
|
-
const dataDir =
|
|
17872
|
-
const fullDataDir =
|
|
17873
|
-
|
|
17872
|
+
const dataDir = join27(USERS_DIR, username);
|
|
17873
|
+
const fullDataDir = join27(this.baseDir, dataDir);
|
|
17874
|
+
mkdirSync17(join27(fullDataDir, "history"), { recursive: true });
|
|
17874
17875
|
const user = {
|
|
17875
17876
|
username,
|
|
17876
17877
|
passwordHash,
|
|
@@ -17985,7 +17986,7 @@ var AuthManager = class {
|
|
|
17985
17986
|
getUserDataDir(username) {
|
|
17986
17987
|
const user = this.db.users.find((u) => u.username === username);
|
|
17987
17988
|
if (!user) throw new Error(`User not found: ${username}`);
|
|
17988
|
-
return
|
|
17989
|
+
return join27(this.baseDir, user.dataDir);
|
|
17989
17990
|
}
|
|
17990
17991
|
/** List all usernames */
|
|
17991
17992
|
listUsers() {
|
|
@@ -18021,30 +18022,30 @@ var AuthManager = class {
|
|
|
18021
18022
|
const err = this.register(username, password);
|
|
18022
18023
|
if (err) return err;
|
|
18023
18024
|
const userDir = this.getUserDataDir(username);
|
|
18024
|
-
const globalConfig =
|
|
18025
|
-
if (
|
|
18025
|
+
const globalConfig = join27(this.baseDir, "config.json");
|
|
18026
|
+
if (existsSync32(globalConfig)) {
|
|
18026
18027
|
try {
|
|
18027
|
-
const content =
|
|
18028
|
-
|
|
18028
|
+
const content = readFileSync25(globalConfig, "utf-8");
|
|
18029
|
+
writeFileSync6(join27(userDir, "config.json"), content, "utf-8");
|
|
18029
18030
|
} catch {
|
|
18030
18031
|
}
|
|
18031
18032
|
}
|
|
18032
|
-
const globalMemory =
|
|
18033
|
-
if (
|
|
18033
|
+
const globalMemory = join27(this.baseDir, "memory.md");
|
|
18034
|
+
if (existsSync32(globalMemory)) {
|
|
18034
18035
|
try {
|
|
18035
|
-
const content =
|
|
18036
|
-
|
|
18036
|
+
const content = readFileSync25(globalMemory, "utf-8");
|
|
18037
|
+
writeFileSync6(join27(userDir, "memory.md"), content, "utf-8");
|
|
18037
18038
|
} catch {
|
|
18038
18039
|
}
|
|
18039
18040
|
}
|
|
18040
|
-
const globalHistory =
|
|
18041
|
-
if (
|
|
18041
|
+
const globalHistory = join27(this.baseDir, "history");
|
|
18042
|
+
if (existsSync32(globalHistory)) {
|
|
18042
18043
|
try {
|
|
18043
|
-
const files =
|
|
18044
|
-
const userHistory =
|
|
18044
|
+
const files = readdirSync16(globalHistory).filter((f) => f.endsWith(".json"));
|
|
18045
|
+
const userHistory = join27(userDir, "history");
|
|
18045
18046
|
for (const f of files) {
|
|
18046
18047
|
try {
|
|
18047
|
-
copyFileSync(
|
|
18048
|
+
copyFileSync(join27(globalHistory, f), join27(userHistory, f));
|
|
18048
18049
|
} catch {
|
|
18049
18050
|
}
|
|
18050
18051
|
}
|
|
@@ -18055,9 +18056,9 @@ var AuthManager = class {
|
|
|
18055
18056
|
}
|
|
18056
18057
|
// ── Private methods ────────────────────────────────────────────
|
|
18057
18058
|
loadOrCreate() {
|
|
18058
|
-
if (
|
|
18059
|
+
if (existsSync32(this.usersFile)) {
|
|
18059
18060
|
try {
|
|
18060
|
-
return JSON.parse(
|
|
18061
|
+
return JSON.parse(readFileSync25(this.usersFile, "utf-8"));
|
|
18061
18062
|
} catch {
|
|
18062
18063
|
}
|
|
18063
18064
|
}
|
|
@@ -18073,7 +18074,7 @@ var AuthManager = class {
|
|
|
18073
18074
|
this.saveDB(this.db);
|
|
18074
18075
|
}
|
|
18075
18076
|
saveDB(db) {
|
|
18076
|
-
|
|
18077
|
+
mkdirSync17(this.baseDir, { recursive: true });
|
|
18077
18078
|
atomicWriteFileSync(this.usersFile, JSON.stringify(db, null, 2));
|
|
18078
18079
|
}
|
|
18079
18080
|
/** Legacy hash — kept only for migrating old users (v0.2.x) */
|
|
@@ -18105,11 +18106,11 @@ function getModuleDir() {
|
|
|
18105
18106
|
if (typeof import.meta?.url === "string") {
|
|
18106
18107
|
const url = new URL(import.meta.url);
|
|
18107
18108
|
const filePath = url.pathname.replace(/^\/([A-Z]:)/i, "$1");
|
|
18108
|
-
return
|
|
18109
|
+
return dirname11(filePath);
|
|
18109
18110
|
}
|
|
18110
18111
|
} catch {
|
|
18111
18112
|
}
|
|
18112
|
-
return
|
|
18113
|
+
return resolve13(".");
|
|
18113
18114
|
}
|
|
18114
18115
|
async function startWebServer(options = {}) {
|
|
18115
18116
|
const port = options.port ?? 3e3;
|
|
@@ -18177,8 +18178,8 @@ async function startWebServer(options = {}) {
|
|
|
18177
18178
|
}
|
|
18178
18179
|
}
|
|
18179
18180
|
let skillManager = null;
|
|
18180
|
-
const skillsDir =
|
|
18181
|
-
if (
|
|
18181
|
+
const skillsDir = join28(config.getConfigDir(), SKILLS_DIR_NAME);
|
|
18182
|
+
if (existsSync33(skillsDir)) {
|
|
18182
18183
|
skillManager = new SkillManager(skillsDir, config.get("ui").skillSizeWarn);
|
|
18183
18184
|
skillManager.loadSkills(pluginAssets.skillDirs);
|
|
18184
18185
|
const count = skillManager.listSkills().length;
|
|
@@ -18249,18 +18250,18 @@ async function startWebServer(options = {}) {
|
|
|
18249
18250
|
next();
|
|
18250
18251
|
};
|
|
18251
18252
|
const moduleDir = getModuleDir();
|
|
18252
|
-
let clientDir =
|
|
18253
|
-
if (!
|
|
18254
|
-
clientDir =
|
|
18253
|
+
let clientDir = join28(moduleDir, "web", "client");
|
|
18254
|
+
if (!existsSync33(clientDir)) {
|
|
18255
|
+
clientDir = join28(moduleDir, "client");
|
|
18255
18256
|
}
|
|
18256
|
-
if (!
|
|
18257
|
-
clientDir =
|
|
18257
|
+
if (!existsSync33(clientDir)) {
|
|
18258
|
+
clientDir = join28(moduleDir, "..", "..", "src", "web", "client");
|
|
18258
18259
|
}
|
|
18259
|
-
if (!
|
|
18260
|
-
clientDir =
|
|
18260
|
+
if (!existsSync33(clientDir)) {
|
|
18261
|
+
clientDir = join28(process.cwd(), "src", "web", "client");
|
|
18261
18262
|
}
|
|
18262
18263
|
console.log(` Static files: ${clientDir}`);
|
|
18263
|
-
app.use("/vendor", express.static(
|
|
18264
|
+
app.use("/vendor", express.static(join28(clientDir, "vendor"), { maxAge: "1y", immutable: true }));
|
|
18264
18265
|
app.use(express.static(clientDir));
|
|
18265
18266
|
app.get("/api/status", (_req, res) => {
|
|
18266
18267
|
res.json({
|
|
@@ -18327,10 +18328,10 @@ async function startWebServer(options = {}) {
|
|
|
18327
18328
|
app.get("/api/files", requireAuth, (req, res) => {
|
|
18328
18329
|
const cwd = process.cwd();
|
|
18329
18330
|
const prefix = req.query.prefix || "";
|
|
18330
|
-
const targetDir =
|
|
18331
|
+
const targetDir = join28(cwd, prefix);
|
|
18331
18332
|
try {
|
|
18332
|
-
const canonicalTarget = realpathSync(
|
|
18333
|
-
const canonicalCwd = realpathSync(
|
|
18333
|
+
const canonicalTarget = realpathSync(resolve13(targetDir));
|
|
18334
|
+
const canonicalCwd = realpathSync(resolve13(cwd));
|
|
18334
18335
|
if (!canonicalTarget.startsWith(canonicalCwd + sep3) && canonicalTarget !== canonicalCwd) {
|
|
18335
18336
|
res.json({ files: [] });
|
|
18336
18337
|
return;
|
|
@@ -18341,10 +18342,10 @@ async function startWebServer(options = {}) {
|
|
|
18341
18342
|
}
|
|
18342
18343
|
try {
|
|
18343
18344
|
const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "dist-cjs", "release", "__pycache__", ".next", ".nuxt", "coverage", ".cache"]);
|
|
18344
|
-
const entries =
|
|
18345
|
+
const entries = readdirSync17(targetDir, { withFileTypes: true });
|
|
18345
18346
|
const files = entries.filter((e) => !SKIP.has(e.name) && !e.name.startsWith(".")).slice(0, 50).map((e) => ({
|
|
18346
18347
|
name: e.name,
|
|
18347
|
-
path: relative4(cwd,
|
|
18348
|
+
path: relative4(cwd, join28(targetDir, e.name)).replace(/\\/g, "/"),
|
|
18348
18349
|
isDir: e.isDirectory()
|
|
18349
18350
|
}));
|
|
18350
18351
|
res.json({ files });
|
|
@@ -18384,8 +18385,8 @@ async function startWebServer(options = {}) {
|
|
|
18384
18385
|
try {
|
|
18385
18386
|
const authUser = req._authUser;
|
|
18386
18387
|
const histDir = authUser ? getUserShared(authUser).config.getHistoryDir() : config.getHistoryDir();
|
|
18387
|
-
const filePath =
|
|
18388
|
-
if (!
|
|
18388
|
+
const filePath = join28(histDir, `${id}.json`);
|
|
18389
|
+
if (!existsSync33(filePath)) {
|
|
18389
18390
|
res.status(404).json({ error: "Session not found" });
|
|
18390
18391
|
return;
|
|
18391
18392
|
}
|
|
@@ -18400,7 +18401,7 @@ async function startWebServer(options = {}) {
|
|
|
18400
18401
|
res.status(404).json({ error: "Session not found" });
|
|
18401
18402
|
return;
|
|
18402
18403
|
}
|
|
18403
|
-
const data = JSON.parse(
|
|
18404
|
+
const data = JSON.parse(readFileSync26(filePath, "utf-8"));
|
|
18404
18405
|
res.json({ session: data });
|
|
18405
18406
|
} catch (err) {
|
|
18406
18407
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
@@ -18413,10 +18414,10 @@ async function startWebServer(options = {}) {
|
|
|
18413
18414
|
return;
|
|
18414
18415
|
}
|
|
18415
18416
|
const cwd = process.cwd();
|
|
18416
|
-
const fullPath =
|
|
18417
|
+
const fullPath = resolve13(join28(cwd, filePath));
|
|
18417
18418
|
try {
|
|
18418
18419
|
const canonicalFull = realpathSync(fullPath);
|
|
18419
|
-
const canonicalCwd = realpathSync(
|
|
18420
|
+
const canonicalCwd = realpathSync(resolve13(cwd));
|
|
18420
18421
|
if (!canonicalFull.startsWith(canonicalCwd + sep3) && canonicalFull !== canonicalCwd) {
|
|
18421
18422
|
res.json({ error: "Access denied" });
|
|
18422
18423
|
return;
|
|
@@ -18426,12 +18427,12 @@ async function startWebServer(options = {}) {
|
|
|
18426
18427
|
return;
|
|
18427
18428
|
}
|
|
18428
18429
|
try {
|
|
18429
|
-
const stat =
|
|
18430
|
+
const stat = statSync15(fullPath);
|
|
18430
18431
|
if (stat.size > 512 * 1024) {
|
|
18431
18432
|
res.json({ error: `File too large (${(stat.size / 1024).toFixed(0)} KB, max 512 KB)` });
|
|
18432
18433
|
return;
|
|
18433
18434
|
}
|
|
18434
|
-
const content =
|
|
18435
|
+
const content = readFileSync26(fullPath, "utf-8");
|
|
18435
18436
|
res.json({ content, size: stat.size });
|
|
18436
18437
|
} catch {
|
|
18437
18438
|
res.json({ error: "Cannot read file" });
|
|
@@ -18635,7 +18636,7 @@ async function startWebServer(options = {}) {
|
|
|
18635
18636
|
});
|
|
18636
18637
|
const MAX_PORT_ATTEMPTS = 10;
|
|
18637
18638
|
let actualPort = port;
|
|
18638
|
-
const result = await new Promise((
|
|
18639
|
+
const result = await new Promise((resolve14, reject) => {
|
|
18639
18640
|
const tryListen = (attempt) => {
|
|
18640
18641
|
server.once("error", (err) => {
|
|
18641
18642
|
if (err.code === "EADDRINUSE" && attempt < MAX_PORT_ATTEMPTS) {
|
|
@@ -18666,7 +18667,7 @@ async function startWebServer(options = {}) {
|
|
|
18666
18667
|
}
|
|
18667
18668
|
console.log(` Press Ctrl+C to stop
|
|
18668
18669
|
`);
|
|
18669
|
-
|
|
18670
|
+
resolve14({ port: actualPort, host, url });
|
|
18670
18671
|
});
|
|
18671
18672
|
};
|
|
18672
18673
|
tryListen(1);
|
|
@@ -18686,17 +18687,17 @@ function resolveProjectMcpPath() {
|
|
|
18686
18687
|
const cwd = process.cwd();
|
|
18687
18688
|
const gitRoot = getGitRoot(cwd);
|
|
18688
18689
|
const projectRoot = gitRoot ?? cwd;
|
|
18689
|
-
const configPath =
|
|
18690
|
-
return
|
|
18690
|
+
const configPath = join28(projectRoot, MCP_PROJECT_CONFIG_NAME);
|
|
18691
|
+
return existsSync33(configPath) ? configPath : null;
|
|
18691
18692
|
}
|
|
18692
18693
|
function loadProjectMcpConfig() {
|
|
18693
18694
|
const cwd = process.cwd();
|
|
18694
18695
|
const gitRoot = getGitRoot(cwd);
|
|
18695
18696
|
const projectRoot = gitRoot ?? cwd;
|
|
18696
|
-
const configPath =
|
|
18697
|
-
if (!
|
|
18697
|
+
const configPath = join28(projectRoot, MCP_PROJECT_CONFIG_NAME);
|
|
18698
|
+
if (!existsSync33(configPath)) return null;
|
|
18698
18699
|
try {
|
|
18699
|
-
const raw = JSON.parse(
|
|
18700
|
+
const raw = JSON.parse(readFileSync26(configPath, "utf-8"));
|
|
18700
18701
|
return raw.mcpServers ?? raw;
|
|
18701
18702
|
} catch {
|
|
18702
18703
|
return null;
|