jinzd-ai-cli 0.4.222 → 0.4.224
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-EQER63CQ.js} +2 -2
- package/dist/{chat-index-FDSGKWQV.js → chat-index-O56HDGFI.js} +2 -2
- package/dist/{chat-index-PS274XM7.js → chat-index-WSI7ULRQ.js} +1 -1
- package/dist/{chunk-OJL3PY36.js → chunk-2X2DOHA6.js} +3 -2
- package/dist/{chunk-VWTUYDZG.js → chunk-34RV4ZKA.js} +1 -1
- package/dist/{chunk-UWUTLIOH.js → chunk-35OPVVEK.js} +1 -1
- package/dist/{chunk-NZ4X6GUC.js → chunk-3ARPAVNV.js} +2 -2
- package/dist/{chunk-FBK7NSIK.js → chunk-4CZZJQXJ.js} +2 -2
- package/dist/{chunk-5EE4GRGG.js → chunk-4RQPCOEE.js} +281 -41
- package/dist/{chunk-VBRCWH55.js → chunk-6FEZAWH5.js} +3 -3
- package/dist/{chunk-SISFAT6W.js → chunk-A2AVDSXJ.js} +1 -1
- package/dist/{chunk-SLSWPBK3.js → chunk-B5TYJO7V.js} +4 -1
- package/dist/{chunk-QT5FA2ZY.js → chunk-DEHVRQWW.js} +5 -5
- package/dist/{chunk-AV5OPK7Q.js → chunk-HLXQXV23.js} +1 -1
- package/dist/{chunk-TNW22OUY.js → chunk-JDMFVRFG.js} +3 -3
- package/dist/{chunk-S6L5R6SS.js → chunk-JU7OI4BN.js} +1 -1
- package/dist/{chunk-VHY6NVMQ.js → chunk-LF3OUJHK.js} +4 -1
- package/dist/{chunk-TB4W4Y4T.js → chunk-NVUCDUXE.js} +1 -1
- package/dist/{chunk-NNWWMGYK.js → chunk-QJUAQKH2.js} +1 -1
- package/dist/{chunk-5GTWZ4QF.js → chunk-QYQCCIBT.js} +1 -1
- package/dist/{chunk-Q3ZUDA6S.js → chunk-VT6NRDD2.js} +1 -1
- package/dist/{ci-IB7NWHZN.js → ci-UAQFOAMS.js} +4 -4
- package/dist/{ci-format-YLGZN3D4.js → ci-format-MNUHYUTO.js} +2 -2
- package/dist/{constants-BOMLZEXC.js → constants-PGDTYD2R.js} +1 -1
- package/dist/{doctor-cli-2ND4LONW.js → doctor-cli-F6A3GOVY.js} +4 -4
- package/dist/electron-server.js +2017 -1786
- package/dist/{hub-6WADFJNC.js → hub-EDTZKS7E.js} +6 -6
- package/dist/{hub-server-LJ2JSKZ2.js → hub-server-XX36DX7O.js} +2 -2
- package/dist/index.js +26 -22
- package/dist/{persist-GTBEKVFL.js → persist-XUXIPYUU.js} +3 -3
- package/dist/{pr-F7WXUH27.js → pr-YC7DMVC5.js} +4 -4
- package/dist/{run-tests-5NEMM6EF.js → run-tests-AUEKHJCC.js} +2 -2
- package/dist/{run-tests-NIY24YDP.js → run-tests-S36TE2W2.js} +1 -1
- package/dist/{server-ACFX2J66.js → server-347YIBHS.js} +6 -6
- package/dist/{server-QGZYDDZW.js → server-LQFHSFN4.js} +1278 -1271
- package/dist/{task-orchestrator-4GOBFXPN.js → task-orchestrator-FIR5MCND.js} +17 -9
- package/dist/{usage-JXT2YVA6.js → usage-3MXPFFBU.js} +2 -2
- package/dist/web/client/app.js +3 -2
- package/dist/web/client/util.js +42 -0
- 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-HLXQXV23.js";
|
|
42
42
|
import {
|
|
43
43
|
hasSemanticIndex,
|
|
44
44
|
semanticSearch
|
|
@@ -54,7 +54,7 @@ import {
|
|
|
54
54
|
redactString,
|
|
55
55
|
scanString,
|
|
56
56
|
searchChatMemory
|
|
57
|
-
} from "./chunk-
|
|
57
|
+
} from "./chunk-LF3OUJHK.js";
|
|
58
58
|
import "./chunk-JV5N65KN.js";
|
|
59
59
|
import {
|
|
60
60
|
atomicWriteFileSync
|
|
@@ -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
|
|
@@ -4857,9 +4857,9 @@ var SessionManager = class {
|
|
|
4857
4857
|
};
|
|
4858
4858
|
|
|
4859
4859
|
// src/tools/builtin/bash.ts
|
|
4860
|
-
import { spawn } from "child_process";
|
|
4860
|
+
import { spawn as spawn2 } from "child_process";
|
|
4861
4861
|
import { existsSync as existsSync5, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
4862
|
-
import { platform as
|
|
4862
|
+
import { platform as platform3 } from "os";
|
|
4863
4863
|
import { resolve } from "path";
|
|
4864
4864
|
|
|
4865
4865
|
// src/tools/undo-stack.ts
|
|
@@ -5020,9 +5020,192 @@ function decodeWindowsBuffer(buf) {
|
|
|
5020
5020
|
}
|
|
5021
5021
|
}
|
|
5022
5022
|
|
|
5023
|
-
// src/tools/
|
|
5023
|
+
// src/tools/shell-pool.ts
|
|
5024
|
+
import { spawn } from "child_process";
|
|
5025
|
+
import { platform as platform2 } from "os";
|
|
5024
5026
|
var IS_WINDOWS2 = platform2() === "win32";
|
|
5025
5027
|
var SHELL = IS_WINDOWS2 ? "powershell.exe" : process.env["SHELL"] ?? "/bin/bash";
|
|
5028
|
+
var pool = /* @__PURE__ */ new Map();
|
|
5029
|
+
var IDLE_EVICT_MS = 5 * 6e4;
|
|
5030
|
+
var evictionTimer = null;
|
|
5031
|
+
function spawnShell(cwd) {
|
|
5032
|
+
const shellArgs = IS_WINDOWS2 ? ["-NoProfile", "-NonInteractive", "-Command", "-"] : ["-s"];
|
|
5033
|
+
const proc = spawn(SHELL, shellArgs, {
|
|
5034
|
+
cwd,
|
|
5035
|
+
windowsHide: true,
|
|
5036
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
5037
|
+
});
|
|
5038
|
+
proc.on("exit", () => {
|
|
5039
|
+
for (const [key, entry] of pool) {
|
|
5040
|
+
if (entry.proc === proc) {
|
|
5041
|
+
pool.delete(key);
|
|
5042
|
+
break;
|
|
5043
|
+
}
|
|
5044
|
+
}
|
|
5045
|
+
});
|
|
5046
|
+
return proc;
|
|
5047
|
+
}
|
|
5048
|
+
function startEvictionTimer() {
|
|
5049
|
+
if (evictionTimer) return;
|
|
5050
|
+
evictionTimer = setInterval(() => {
|
|
5051
|
+
const now = Date.now();
|
|
5052
|
+
for (const [key, entry] of pool) {
|
|
5053
|
+
if (now - entry.lastUsed > IDLE_EVICT_MS) {
|
|
5054
|
+
try {
|
|
5055
|
+
entry.proc.kill();
|
|
5056
|
+
} catch {
|
|
5057
|
+
}
|
|
5058
|
+
pool.delete(key);
|
|
5059
|
+
}
|
|
5060
|
+
}
|
|
5061
|
+
}, 6e4).unref();
|
|
5062
|
+
}
|
|
5063
|
+
async function execInPool(sessionKey, command, timeout, cwd, abortSignal) {
|
|
5064
|
+
let entry = pool.get(sessionKey);
|
|
5065
|
+
if (!entry || entry.proc.exitCode !== null || entry.cwd !== cwd) {
|
|
5066
|
+
if (entry) {
|
|
5067
|
+
try {
|
|
5068
|
+
entry.proc.kill();
|
|
5069
|
+
} catch {
|
|
5070
|
+
}
|
|
5071
|
+
}
|
|
5072
|
+
const proc = spawnShell(cwd);
|
|
5073
|
+
entry = { proc, cwd, lastUsed: Date.now() };
|
|
5074
|
+
pool.set(sessionKey, entry);
|
|
5075
|
+
}
|
|
5076
|
+
entry.lastUsed = Date.now();
|
|
5077
|
+
startEvictionTimer();
|
|
5078
|
+
const id = `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
5079
|
+
const markerStart = `AICLI_MARKER_START:${id}`;
|
|
5080
|
+
const markerEnd = `AICLI_MARKER_END:${id}`;
|
|
5081
|
+
const wrappedCmd = IS_WINDOWS2 ? `echo "${markerStart}"; ${command}; echo "${markerEnd}:0"` : `echo "${markerStart}"; ${command}; echo "${markerEnd}:$?"`;
|
|
5082
|
+
return new Promise((resolve14) => {
|
|
5083
|
+
let stdout = "";
|
|
5084
|
+
let stderr = "";
|
|
5085
|
+
let timedOut = false;
|
|
5086
|
+
let aborted = false;
|
|
5087
|
+
let resolved = false;
|
|
5088
|
+
const markerStartPattern = `AICLI_MARKER_START:${id}`;
|
|
5089
|
+
const markerEndPattern = `AICLI_MARKER_END:${id}`;
|
|
5090
|
+
const onData = (chunk) => {
|
|
5091
|
+
stdout += chunk.toString("utf-8");
|
|
5092
|
+
if (!resolved && stdout.includes(markerEndPattern)) {
|
|
5093
|
+
const endLine = stdout.split("\n").find((l) => l.includes(markerEndPattern));
|
|
5094
|
+
const exitCode = endLine ? parseEndMarker(endLine, markerEndPattern) : null;
|
|
5095
|
+
stdout = stripMarkerOutput(stdout, markerStartPattern, markerEndPattern);
|
|
5096
|
+
resolved = true;
|
|
5097
|
+
cleanup();
|
|
5098
|
+
resolve14({ stdout, stderr, exitCode, timedOut, aborted });
|
|
5099
|
+
}
|
|
5100
|
+
};
|
|
5101
|
+
const onErr = (chunk) => {
|
|
5102
|
+
stderr += chunk.toString("utf-8");
|
|
5103
|
+
};
|
|
5104
|
+
const proc = entry.proc;
|
|
5105
|
+
proc.stdout?.on("data", onData);
|
|
5106
|
+
proc.stderr?.on("data", onErr);
|
|
5107
|
+
const timeoutTimer = setTimeout(() => {
|
|
5108
|
+
if (resolved) return;
|
|
5109
|
+
timedOut = true;
|
|
5110
|
+
resolved = true;
|
|
5111
|
+
stdout = stripMarkerOutput(stdout, markerStartPattern, markerEndPattern);
|
|
5112
|
+
cleanup();
|
|
5113
|
+
resolve14({ stdout, stderr, exitCode: null, timedOut, aborted });
|
|
5114
|
+
try {
|
|
5115
|
+
proc.kill();
|
|
5116
|
+
} catch {
|
|
5117
|
+
}
|
|
5118
|
+
pool.delete(sessionKey);
|
|
5119
|
+
}, timeout);
|
|
5120
|
+
const onAbort = () => {
|
|
5121
|
+
if (resolved) return;
|
|
5122
|
+
aborted = true;
|
|
5123
|
+
resolved = true;
|
|
5124
|
+
cleanup();
|
|
5125
|
+
stdout = stripMarkerOutput(stdout, markerStartPattern, markerEndPattern);
|
|
5126
|
+
resolve14({ stdout, stderr, exitCode: null, timedOut, aborted });
|
|
5127
|
+
try {
|
|
5128
|
+
proc.kill();
|
|
5129
|
+
} catch {
|
|
5130
|
+
}
|
|
5131
|
+
pool.delete(sessionKey);
|
|
5132
|
+
};
|
|
5133
|
+
if (abortSignal?.aborted) {
|
|
5134
|
+
onAbort();
|
|
5135
|
+
} else {
|
|
5136
|
+
abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
5137
|
+
}
|
|
5138
|
+
const onError = (err) => {
|
|
5139
|
+
if (resolved) return;
|
|
5140
|
+
resolved = true;
|
|
5141
|
+
stderr += `
|
|
5142
|
+
[pool error] ${err.message}`;
|
|
5143
|
+
cleanup();
|
|
5144
|
+
resolve14({ stdout, stderr, exitCode: null, timedOut, aborted });
|
|
5145
|
+
pool.delete(sessionKey);
|
|
5146
|
+
};
|
|
5147
|
+
proc.on("error", onError);
|
|
5148
|
+
function cleanup() {
|
|
5149
|
+
clearTimeout(timeoutTimer);
|
|
5150
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
5151
|
+
proc.stdout?.removeListener("data", onData);
|
|
5152
|
+
proc.stderr?.removeListener("data", onErr);
|
|
5153
|
+
proc.removeListener("error", onError);
|
|
5154
|
+
}
|
|
5155
|
+
try {
|
|
5156
|
+
proc.stdin?.write(wrappedCmd + "\n");
|
|
5157
|
+
} catch (err) {
|
|
5158
|
+
if (!resolved) {
|
|
5159
|
+
cleanup();
|
|
5160
|
+
pool.delete(sessionKey);
|
|
5161
|
+
}
|
|
5162
|
+
}
|
|
5163
|
+
});
|
|
5164
|
+
}
|
|
5165
|
+
function parseEndMarker(line, markerEndPattern) {
|
|
5166
|
+
const idx = line.lastIndexOf(":");
|
|
5167
|
+
if (idx < 0) return null;
|
|
5168
|
+
const code = line.slice(idx + 1).trim();
|
|
5169
|
+
return /^\d+$/.test(code) ? Number(code) : null;
|
|
5170
|
+
}
|
|
5171
|
+
function stripMarkerOutput(stdout, startMarker, endMarker) {
|
|
5172
|
+
const lines = stdout.split("\n");
|
|
5173
|
+
let capturing = false;
|
|
5174
|
+
const out = [];
|
|
5175
|
+
for (const line of lines) {
|
|
5176
|
+
if (line.includes(startMarker)) {
|
|
5177
|
+
capturing = true;
|
|
5178
|
+
continue;
|
|
5179
|
+
}
|
|
5180
|
+
if (line.includes(endMarker)) {
|
|
5181
|
+
capturing = false;
|
|
5182
|
+
continue;
|
|
5183
|
+
}
|
|
5184
|
+
if (capturing) {
|
|
5185
|
+
out.push(line);
|
|
5186
|
+
}
|
|
5187
|
+
}
|
|
5188
|
+
if (!stdout.includes(endMarker) && out.length === 0) {
|
|
5189
|
+
const startIdx = stdout.indexOf(startMarker);
|
|
5190
|
+
if (startIdx >= 0) {
|
|
5191
|
+
return stdout.slice(startIdx + startMarker.length).trimStart();
|
|
5192
|
+
}
|
|
5193
|
+
}
|
|
5194
|
+
return out.join("\n").trim();
|
|
5195
|
+
}
|
|
5196
|
+
function cleanupPoolSession(sessionKey) {
|
|
5197
|
+
const entry = pool.get(sessionKey);
|
|
5198
|
+
if (!entry) return;
|
|
5199
|
+
try {
|
|
5200
|
+
entry.proc.kill();
|
|
5201
|
+
} catch {
|
|
5202
|
+
}
|
|
5203
|
+
pool.delete(sessionKey);
|
|
5204
|
+
}
|
|
5205
|
+
|
|
5206
|
+
// src/tools/builtin/bash.ts
|
|
5207
|
+
var IS_WINDOWS3 = platform3() === "win32";
|
|
5208
|
+
var SHELL2 = IS_WINDOWS3 ? "powershell.exe" : process.env["SHELL"] ?? "/bin/bash";
|
|
5026
5209
|
var cwdBySession = /* @__PURE__ */ new Map();
|
|
5027
5210
|
function getCwd() {
|
|
5028
5211
|
const key = getCurrentSessionKey();
|
|
@@ -5036,17 +5219,21 @@ function getCwd() {
|
|
|
5036
5219
|
function setCwd(next) {
|
|
5037
5220
|
cwdBySession.set(getCurrentSessionKey(), next);
|
|
5038
5221
|
}
|
|
5222
|
+
function clearBashSession(sessionKey) {
|
|
5223
|
+
cwdBySession.delete(sessionKey);
|
|
5224
|
+
cleanupPoolSession(sessionKey);
|
|
5225
|
+
}
|
|
5039
5226
|
var bashTool = {
|
|
5040
5227
|
definition: {
|
|
5041
5228
|
name: "bash",
|
|
5042
|
-
description:
|
|
5229
|
+
description: IS_WINDOWS3 ? `Execute commands in PowerShell. Supports mkdir, ls, cat, python, etc.
|
|
5043
5230
|
Important rules:
|
|
5044
5231
|
1. Each bash call runs in an independent subprocess; cd commands do not persist. To run in a specific directory, use the cwd parameter, or combine commands: e.g. "cd mydir; ls" or "mkdir mydir; cd mydir; New-Item file.txt".
|
|
5045
5232
|
2. If a command fails (returns an error or non-zero exit code), stop immediately, report the error to the user, and do not retry the same or similar commands.
|
|
5046
5233
|
3. Multiple commands can be combined with semicolons in a single call to reduce rounds.
|
|
5047
5234
|
4. To delete directories, use Remove-Item -Recurse (the system will automatically optimize to a more reliable method).
|
|
5048
5235
|
5. IMPORTANT: On Windows, "curl" is an alias for Invoke-WebRequest and does NOT support curl flags like -s, -X, -H. Use Invoke-RestMethod instead for HTTP requests. Example: Invoke-RestMethod -Uri "http://localhost:3000/api/health" -Method Get
|
|
5049
|
-
6. NEVER run long-running / never-exiting commands through bash \u2014 they block until timeout. This includes starting an Android emulator (emulator -avd X), streaming logs (adb logcat), and dev servers (node server.js, npm run dev, metro / react-native start). Use the 'task_create' tool to run these in the background, then poll status with short bash calls (e.g. adb shell getprop sys.boot_completed).` : `Execute commands in ${
|
|
5236
|
+
6. NEVER run long-running / never-exiting commands through bash \u2014 they block until timeout. This includes starting an Android emulator (emulator -avd X), streaming logs (adb logcat), and dev servers (node server.js, npm run dev, metro / react-native start). Use the 'task_create' tool to run these in the background, then poll status with short bash calls (e.g. adb shell getprop sys.boot_completed).` : `Execute commands in ${SHELL2}.
|
|
5050
5237
|
Important rules:
|
|
5051
5238
|
1. Each bash call runs in an independent subprocess; cd commands do not persist. To run in a specific directory, use the cwd parameter, or combine commands: e.g. "cd mydir && ls" or "mkdir -p mydir && touch mydir/file.txt".
|
|
5052
5239
|
2. If a command fails (returns an error or non-zero exit code), stop immediately, report the error to the user, and do not retry the same or similar commands.
|
|
@@ -5055,7 +5242,7 @@ Important rules:
|
|
|
5055
5242
|
parameters: {
|
|
5056
5243
|
command: {
|
|
5057
5244
|
type: "string",
|
|
5058
|
-
description:
|
|
5245
|
+
description: IS_WINDOWS3 ? `PowerShell command to execute. Combine multiple commands with semicolons, e.g.: "mkdir mydir; Set-Content mydir/file.txt 'content'"` : `${SHELL2} command to execute. Combine multiple commands with &&, e.g.: "mkdir -p mydir && echo 'content' > mydir/file.txt"`,
|
|
5059
5246
|
required: true
|
|
5060
5247
|
},
|
|
5061
5248
|
cwd: {
|
|
@@ -5107,7 +5294,7 @@ Important rules:
|
|
|
5107
5294
|
setCwd(resolved);
|
|
5108
5295
|
}
|
|
5109
5296
|
let actualCommand;
|
|
5110
|
-
if (
|
|
5297
|
+
if (IS_WINDOWS3) {
|
|
5111
5298
|
const fixedCommand = fixWindowsDeleteCommand(command);
|
|
5112
5299
|
actualCommand = WIN_UTF8_PREAMBLE + fixedCommand;
|
|
5113
5300
|
} else {
|
|
@@ -5119,6 +5306,44 @@ Important rules:
|
|
|
5119
5306
|
for (const t of parsedTargets) {
|
|
5120
5307
|
parsedTargetsBefore.set(t, existsSync5(t));
|
|
5121
5308
|
}
|
|
5309
|
+
let poolResult = null;
|
|
5310
|
+
try {
|
|
5311
|
+
poolResult = await execInPool(
|
|
5312
|
+
getCurrentSessionKey(),
|
|
5313
|
+
actualCommand,
|
|
5314
|
+
timeout,
|
|
5315
|
+
effectiveCwd,
|
|
5316
|
+
currentAbortSignal()
|
|
5317
|
+
);
|
|
5318
|
+
} catch {
|
|
5319
|
+
poolResult = null;
|
|
5320
|
+
}
|
|
5321
|
+
if (poolResult) {
|
|
5322
|
+
const { stdout, stderr, exitCode, timedOut, aborted } = poolResult;
|
|
5323
|
+
const status = exitCode;
|
|
5324
|
+
if (aborted) {
|
|
5325
|
+
pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, effectiveCwd);
|
|
5326
|
+
throw new ToolError(
|
|
5327
|
+
"bash",
|
|
5328
|
+
`Command interrupted by user (Ctrl+C).
|
|
5329
|
+
|
|
5330
|
+
[The user pressed Ctrl+C while this command was running. Stop the current task, summarize what was done so far, and wait for the user's next instruction. Do NOT retry this command or continue with further tool calls unless explicitly asked.]`
|
|
5331
|
+
);
|
|
5332
|
+
}
|
|
5333
|
+
if (status !== 0 || timedOut) {
|
|
5334
|
+
throw {
|
|
5335
|
+
status,
|
|
5336
|
+
signal: null,
|
|
5337
|
+
code: timedOut ? "ETIMEDOUT" : void 0,
|
|
5338
|
+
stdout: Buffer.from(stdout, "utf-8"),
|
|
5339
|
+
stderr: Buffer.from(stderr, "utf-8"),
|
|
5340
|
+
message: timedOut ? "ETIMEDOUT" : `exit ${status}`
|
|
5341
|
+
};
|
|
5342
|
+
}
|
|
5343
|
+
pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, effectiveCwd);
|
|
5344
|
+
updateCwdFromCommand(command, effectiveCwd);
|
|
5345
|
+
return stdout || "(command completed with no output)";
|
|
5346
|
+
}
|
|
5122
5347
|
try {
|
|
5123
5348
|
const { stdout, stderr, status, signal, timedOut, aborted } = await runShell(
|
|
5124
5349
|
actualCommand,
|
|
@@ -5153,7 +5378,7 @@ Important rules:
|
|
|
5153
5378
|
}
|
|
5154
5379
|
updateCwdFromCommand(command, effectiveCwd);
|
|
5155
5380
|
pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, effectiveCwd);
|
|
5156
|
-
const result = Buffer.isBuffer(stdout) ?
|
|
5381
|
+
const result = Buffer.isBuffer(stdout) ? IS_WINDOWS3 ? decodeWindowsBuffer(stdout) : stdout.toString("utf-8") : String(stdout ?? "");
|
|
5157
5382
|
return result || "(command completed with no output)";
|
|
5158
5383
|
} catch (err) {
|
|
5159
5384
|
pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, effectiveCwd);
|
|
@@ -5178,8 +5403,8 @@ How to recover (pick ONE \u2014 do NOT retry the same command):
|
|
|
5178
5403
|
);
|
|
5179
5404
|
}
|
|
5180
5405
|
if ("status" in execErr && execErr.status !== void 0) {
|
|
5181
|
-
const stderr =
|
|
5182
|
-
const stdout =
|
|
5406
|
+
const stderr = IS_WINDOWS3 && Buffer.isBuffer(execErr.stderr) ? decodeWindowsBuffer(execErr.stderr).trim() : execErr.stderr?.toString().trim() ?? "";
|
|
5407
|
+
const stdout = IS_WINDOWS3 && Buffer.isBuffer(execErr.stdout) ? decodeWindowsBuffer(execErr.stdout).trim() : execErr.stdout?.toString().trim() ?? "";
|
|
5183
5408
|
const combined = [stdout, stderr].filter(Boolean).join("\n");
|
|
5184
5409
|
const hint = buildErrorHint(command, combined);
|
|
5185
5410
|
throw new ToolError(
|
|
@@ -5215,12 +5440,12 @@ function fixWindowsDeleteCommand(command) {
|
|
|
5215
5440
|
}
|
|
5216
5441
|
function buildErrorHint(command, stderr) {
|
|
5217
5442
|
const hints = [];
|
|
5218
|
-
if (
|
|
5443
|
+
if (IS_WINDOWS3 && /\|\|/.test(command) && /(\|\||is not a valid argument|不是此版本中的有效|ParserError|Unexpected token)/i.test(stderr)) {
|
|
5219
5444
|
hints.push(
|
|
5220
5445
|
`Hint: PowerShell parses '||' as a pipeline operator (PS 5.x) or logical-or (PS 7+), it CANNOT be passed inline as SQL string concatenation. Workaround: write the SQL to a local .sql file with write_file, then 'scp' it to the remote host and run 'psql -f /tmp/x.sql'. Do NOT try to escape || or wrap the command differently \u2014 it will not work.`
|
|
5221
5446
|
);
|
|
5222
5447
|
}
|
|
5223
|
-
if (
|
|
5448
|
+
if (IS_WINDOWS3 && /\bpython3\b/.test(command)) {
|
|
5224
5449
|
const trivialStderr = stderr.trim().length === 0 || /^[·\s]*exit 1\b/i.test(stderr.trim());
|
|
5225
5450
|
if (/(not recognized|is not recognized|无法将.*识别)/i.test(stderr) || trivialStderr) {
|
|
5226
5451
|
hints.push(
|
|
@@ -5228,27 +5453,27 @@ function buildErrorHint(command, stderr) {
|
|
|
5228
5453
|
);
|
|
5229
5454
|
}
|
|
5230
5455
|
}
|
|
5231
|
-
if (
|
|
5456
|
+
if (IS_WINDOWS3 && /^\s*ssh\b/.test(command) && /\\"/.test(command) && /(syntax error|ERROR:|ParserError|unexpected|parser|意外的|语法|不是此版本|所在位置\s+行)/i.test(stderr)) {
|
|
5232
5457
|
hints.push(
|
|
5233
5458
|
`Hint: SSH + nested quotes ("...\\"...\\"") is fragile across PS \u2192 ssh \u2192 remote-shell \u2192 psql layers. Use the file-based flow: (1) write_file locally to a .sql file, (2) 'scp x.sql root@host:/tmp/', (3) 'ssh root@host "sudo -u postgres psql -d <db> -f /tmp/x.sql"'. Or for remote-only SQL: 'ssh host "cat > /tmp/x.sql << \\'SQLEOF\\'\\n...\\nSQLEOF"' then run psql -f. Avoid inline psql -c with embedded quotes.`
|
|
5234
5459
|
);
|
|
5235
5460
|
}
|
|
5236
|
-
if (
|
|
5461
|
+
if (IS_WINDOWS3 && /\bmkdir\b/.test(command) && /\s-p\b/.test(command) && /(已存在|already exists|ItemExistsUnauthorizedAccessError|exists)/i.test(stderr)) {
|
|
5237
5462
|
hints.push(
|
|
5238
5463
|
`Hint: PowerShell's mkdir does not support the '-p' flag. When the directory already exists it exits with code 1. Use instead: New-Item -ItemType Directory -Force -Path <dir> ('-Force' silently succeeds even if the directory already exists, mimicking 'mkdir -p').`
|
|
5239
5464
|
);
|
|
5240
5465
|
}
|
|
5241
|
-
if (
|
|
5466
|
+
if (IS_WINDOWS3 && /Remove-Item/i.test(command) && /cannot find path|no such file|exit 1/i.test(stderr || command)) {
|
|
5242
5467
|
hints.push(
|
|
5243
5468
|
`Hint: On Windows, "Remove-Item ... -ErrorAction SilentlyContinue" still makes the powershell process exit 1 when no files match (even though the error is silenced). Use Test-Path as a guard: @('tmp_a.sql','tmp_b.sql','tmp_c.csv') | Where-Object { Test-Path $_ } | Remove-Item -Force \u2014 Test-Path returns a bool without raising, so missing paths are filtered out and exit code is 0.`
|
|
5244
5469
|
);
|
|
5245
5470
|
}
|
|
5246
|
-
if (
|
|
5471
|
+
if (IS_WINDOWS3 && /-(?:EA|ErrorAction)\s+(?:SilentlyContinue|Ignore)\b/i.test(command) && /\b(Get-ChildItem|Get-Item|Get-Content|gci|gi|gc|dir|ls)\b/i.test(command) && !/Remove-Item/i.test(command) && !/Test-Path/i.test(command) && /cannot find path|no such file|does not exist|exit 1|不存在|找不到/i.test(stderr || command)) {
|
|
5247
5472
|
hints.push(
|
|
5248
5473
|
`Hint: On Windows, "-ErrorAction SilentlyContinue" silences the error MESSAGE but does NOT change the exit code \u2014 the process still exits 1 when the path doesn't exist (because powershell.exe -NonInteractive flips the exit code on any non-terminating error, silenced or not). If you're just probing for existence, use Test-Path instead \u2014 it returns $true/$false without raising: \`if (Test-Path ./mydir) { Get-ChildItem ./mydir }\` or unconditionally use \`New-Item -ItemType Directory -Force -Path ./mydir\` which is idempotent. Don't retry the same -ErrorAction SilentlyContinue probe expecting a different result.`
|
|
5249
5474
|
);
|
|
5250
5475
|
}
|
|
5251
|
-
if (
|
|
5476
|
+
if (IS_WINDOWS3 && /\bWrite-(?:Content|File)\b/i.test(command) && /(not recognized|无法将.*识别|不是.*cmdlet|CommandNotFoundException)/i.test(stderr)) {
|
|
5252
5477
|
hints.push(
|
|
5253
5478
|
`Hint: 'Write-Content' / 'Write-File' are NOT PowerShell cmdlets (common LLM hallucination). To write a file, prefer the built-in 'write_file' tool \u2014 it handles encoding/escaping cleanly and supports diff preview. If you must stay in bash: use 'Set-Content -Path <file> -Value <string> -Encoding utf8' (overwrites) or 'Add-Content' (appends) or 'Out-File -FilePath <file> -Encoding utf8'.`
|
|
5254
5479
|
);
|
|
@@ -5360,8 +5585,8 @@ function pushBashUndoEntries(beforeSnapshot, parsedTargetsBefore, cwd) {
|
|
|
5360
5585
|
}
|
|
5361
5586
|
function runShell(command, opts) {
|
|
5362
5587
|
return new Promise((resolvePromise) => {
|
|
5363
|
-
const shellArgs =
|
|
5364
|
-
const child =
|
|
5588
|
+
const shellArgs = IS_WINDOWS3 ? ["-NoProfile", "-NonInteractive", "-Command", command] : ["-c", command];
|
|
5589
|
+
const child = spawn2(SHELL2, shellArgs, {
|
|
5365
5590
|
cwd: opts.cwd,
|
|
5366
5591
|
env: opts.env,
|
|
5367
5592
|
windowsHide: true,
|
|
@@ -5423,9 +5648,9 @@ function runShell(command, opts) {
|
|
|
5423
5648
|
}
|
|
5424
5649
|
function killChild(child) {
|
|
5425
5650
|
if (child.killed || child.exitCode !== null) return;
|
|
5426
|
-
if (
|
|
5651
|
+
if (IS_WINDOWS3 && child.pid) {
|
|
5427
5652
|
try {
|
|
5428
|
-
|
|
5653
|
+
spawn2("taskkill", ["/PID", String(child.pid), "/T", "/F"], {
|
|
5429
5654
|
windowsHide: true,
|
|
5430
5655
|
stdio: "ignore"
|
|
5431
5656
|
});
|
|
@@ -7257,7 +7482,7 @@ var ToolExecutor = class {
|
|
|
7257
7482
|
rl.resume();
|
|
7258
7483
|
process.stdout.write(prompt);
|
|
7259
7484
|
this.confirming = true;
|
|
7260
|
-
return new Promise((
|
|
7485
|
+
return new Promise((resolve14) => {
|
|
7261
7486
|
let completed = false;
|
|
7262
7487
|
const cleanup = (result) => {
|
|
7263
7488
|
if (completed) return;
|
|
@@ -7267,7 +7492,7 @@ var ToolExecutor = class {
|
|
|
7267
7492
|
rl.pause();
|
|
7268
7493
|
rlAny.output = savedOutput;
|
|
7269
7494
|
this.confirming = false;
|
|
7270
|
-
|
|
7495
|
+
resolve14(result);
|
|
7271
7496
|
};
|
|
7272
7497
|
const onLine = (line) => {
|
|
7273
7498
|
const trimmed = line.trim();
|
|
@@ -7441,7 +7666,7 @@ var ToolExecutor = class {
|
|
|
7441
7666
|
rl.resume();
|
|
7442
7667
|
process.stdout.write(color("Proceed? [y/N] (type y + Enter to confirm) "));
|
|
7443
7668
|
this.confirming = true;
|
|
7444
|
-
return new Promise((
|
|
7669
|
+
return new Promise((resolve14) => {
|
|
7445
7670
|
let completed = false;
|
|
7446
7671
|
const cleanup = (answer) => {
|
|
7447
7672
|
if (completed) return;
|
|
@@ -7451,7 +7676,7 @@ var ToolExecutor = class {
|
|
|
7451
7676
|
rl.pause();
|
|
7452
7677
|
rlAny.output = savedOutput;
|
|
7453
7678
|
this.confirming = false;
|
|
7454
|
-
|
|
7679
|
+
resolve14(answer === "y");
|
|
7455
7680
|
};
|
|
7456
7681
|
const onLine = (line) => {
|
|
7457
7682
|
const trimmed = line.trim();
|
|
@@ -8637,9 +8862,9 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
|
|
|
8637
8862
|
}
|
|
8638
8863
|
|
|
8639
8864
|
// src/tools/builtin/run-interactive.ts
|
|
8640
|
-
import { spawn as
|
|
8641
|
-
import { platform as
|
|
8642
|
-
var
|
|
8865
|
+
import { spawn as spawn3 } from "child_process";
|
|
8866
|
+
import { platform as platform4 } from "os";
|
|
8867
|
+
var IS_WINDOWS4 = platform4() === "win32";
|
|
8643
8868
|
var runInteractiveTool = {
|
|
8644
8869
|
definition: {
|
|
8645
8870
|
name: "run_interactive",
|
|
@@ -8700,8 +8925,8 @@ var runInteractiveTool = {
|
|
|
8700
8925
|
PYTHONDONTWRITEBYTECODE: "1"
|
|
8701
8926
|
};
|
|
8702
8927
|
const prefixWarnings = [argsTypeWarning, stdinTypeWarning].filter(Boolean).join("");
|
|
8703
|
-
return new Promise((
|
|
8704
|
-
const child =
|
|
8928
|
+
return new Promise((resolve14) => {
|
|
8929
|
+
const child = spawn3(executable, cmdArgs.map(String), {
|
|
8705
8930
|
cwd: process.cwd(),
|
|
8706
8931
|
env,
|
|
8707
8932
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -8719,7 +8944,7 @@ var runInteractiveTool = {
|
|
|
8719
8944
|
let lineIdx = 0;
|
|
8720
8945
|
const writeNextLine = () => {
|
|
8721
8946
|
if (lineIdx < stdinLines.length && !child.stdin.destroyed) {
|
|
8722
|
-
const line = stdinLines[lineIdx++] + (
|
|
8947
|
+
const line = stdinLines[lineIdx++] + (IS_WINDOWS4 ? "\r\n" : "\n");
|
|
8723
8948
|
const canContinue = child.stdin.write(line);
|
|
8724
8949
|
if (canContinue) {
|
|
8725
8950
|
setTimeout(writeNextLine, 150);
|
|
@@ -8733,22 +8958,22 @@ var runInteractiveTool = {
|
|
|
8733
8958
|
setTimeout(writeNextLine, 400);
|
|
8734
8959
|
const timer = setTimeout(() => {
|
|
8735
8960
|
child.kill();
|
|
8736
|
-
|
|
8961
|
+
resolve14(`${prefixWarnings}[Timeout after ${timeout}ms]
|
|
8737
8962
|
${buildOutput(stdout, stderr)}`);
|
|
8738
8963
|
}, timeout);
|
|
8739
8964
|
child.on("close", (code) => {
|
|
8740
8965
|
clearTimeout(timer);
|
|
8741
8966
|
const output = buildOutput(stdout, stderr);
|
|
8742
8967
|
if (code !== 0 && code !== null) {
|
|
8743
|
-
|
|
8968
|
+
resolve14(`${prefixWarnings}Exit code ${code}:
|
|
8744
8969
|
${output}`);
|
|
8745
8970
|
} else {
|
|
8746
|
-
|
|
8971
|
+
resolve14(`${prefixWarnings}${output || "(no output)"}`);
|
|
8747
8972
|
}
|
|
8748
8973
|
});
|
|
8749
8974
|
child.on("error", (err) => {
|
|
8750
8975
|
clearTimeout(timer);
|
|
8751
|
-
|
|
8976
|
+
resolve14(
|
|
8752
8977
|
`${prefixWarnings}Failed to start process "${executable}": ${err.message}
|
|
8753
8978
|
Hint: On Windows, use the full path to the executable, e.g.:
|
|
8754
8979
|
C:\\Users\\Jinzd\\anaconda3\\envs\\python312\\python.exe`
|
|
@@ -9798,7 +10023,7 @@ function promptUser(rl, question) {
|
|
|
9798
10023
|
console.log();
|
|
9799
10024
|
console.log(chalk4.cyan("\u2753 ") + chalk4.bold(question));
|
|
9800
10025
|
process.stdout.write(chalk4.cyan("> "));
|
|
9801
|
-
return new Promise((
|
|
10026
|
+
return new Promise((resolve14) => {
|
|
9802
10027
|
let completed = false;
|
|
9803
10028
|
const cleanup = (answer) => {
|
|
9804
10029
|
if (completed) return;
|
|
@@ -9808,7 +10033,7 @@ function promptUser(rl, question) {
|
|
|
9808
10033
|
rl.pause();
|
|
9809
10034
|
rlAny.output = savedOutput;
|
|
9810
10035
|
askUserContext.prompting = false;
|
|
9811
|
-
|
|
10036
|
+
resolve14(answer);
|
|
9812
10037
|
};
|
|
9813
10038
|
const onLine = (line) => {
|
|
9814
10039
|
cleanup(line);
|
|
@@ -10935,9 +11160,9 @@ var spawnAgentTool = {
|
|
|
10935
11160
|
};
|
|
10936
11161
|
|
|
10937
11162
|
// src/tools/builtin/task-manager.ts
|
|
10938
|
-
import { spawn as
|
|
11163
|
+
import { spawn as spawn4 } from "child_process";
|
|
10939
11164
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
10940
|
-
import { platform as
|
|
11165
|
+
import { platform as platform5 } from "os";
|
|
10941
11166
|
var MAX_OUTPUT_CHARS = 1e4;
|
|
10942
11167
|
var MAX_OUTPUT_BYTES = 64 * 1024;
|
|
10943
11168
|
var MAX_TASKS = 20;
|
|
@@ -10971,7 +11196,7 @@ function createTask(command, description) {
|
|
|
10971
11196
|
}
|
|
10972
11197
|
const id = randomUUID2().slice(0, 8);
|
|
10973
11198
|
const { shell, args } = shellInvocation(command);
|
|
10974
|
-
const proc =
|
|
11199
|
+
const proc = spawn4(shell, args, {
|
|
10975
11200
|
stdio: ["ignore", "pipe", "pipe"],
|
|
10976
11201
|
detached: false,
|
|
10977
11202
|
env: { ...process.env, PYTHONUTF8: "1", PYTHONIOENCODING: "utf-8" }
|
|
@@ -11021,9 +11246,9 @@ function stopTask(id) {
|
|
|
11021
11246
|
if (!task || task.status !== "running") return false;
|
|
11022
11247
|
try {
|
|
11023
11248
|
const proc = task.process;
|
|
11024
|
-
if (
|
|
11249
|
+
if (platform5() === "win32" && proc.pid) {
|
|
11025
11250
|
try {
|
|
11026
|
-
|
|
11251
|
+
spawn4("taskkill", ["/PID", String(proc.pid), "/T", "/F"], {
|
|
11027
11252
|
windowsHide: true,
|
|
11028
11253
|
stdio: "ignore"
|
|
11029
11254
|
});
|
|
@@ -12119,7 +12344,7 @@ var ToolRegistry = class {
|
|
|
12119
12344
|
};
|
|
12120
12345
|
|
|
12121
12346
|
// src/mcp/client.ts
|
|
12122
|
-
import { spawn as
|
|
12347
|
+
import { spawn as spawn5 } from "child_process";
|
|
12123
12348
|
var McpClient = class {
|
|
12124
12349
|
serverId;
|
|
12125
12350
|
config;
|
|
@@ -12157,7 +12382,7 @@ var McpClient = class {
|
|
|
12157
12382
|
async connect() {
|
|
12158
12383
|
const timeout = this.config.timeout ?? MCP_CONNECT_TIMEOUT;
|
|
12159
12384
|
try {
|
|
12160
|
-
this.process =
|
|
12385
|
+
this.process = spawn5(this.config.command, this.config.args ?? [], {
|
|
12161
12386
|
stdio: ["pipe", "pipe", "pipe"],
|
|
12162
12387
|
env: { ...process.env, ...this.config.env },
|
|
12163
12388
|
// Windows 上 npx 等是 .cmd 脚本,需要 shell 模式
|
|
@@ -12253,7 +12478,7 @@ var McpClient = class {
|
|
|
12253
12478
|
// 内部方法:JSON-RPC 通信
|
|
12254
12479
|
// ══════════════════════════════════════════════════════════════════
|
|
12255
12480
|
sendRequest(method, params) {
|
|
12256
|
-
return new Promise((
|
|
12481
|
+
return new Promise((resolve14, reject) => {
|
|
12257
12482
|
if (!this.process?.stdin?.writable) {
|
|
12258
12483
|
return reject(new Error(`MCP server [${this.serverId}] stdin not writable`));
|
|
12259
12484
|
}
|
|
@@ -12277,7 +12502,7 @@ var McpClient = class {
|
|
|
12277
12502
|
this.pendingRequests.set(id, {
|
|
12278
12503
|
resolve: (result) => {
|
|
12279
12504
|
cleanup();
|
|
12280
|
-
|
|
12505
|
+
resolve14(result);
|
|
12281
12506
|
},
|
|
12282
12507
|
reject: (error) => {
|
|
12283
12508
|
cleanup();
|
|
@@ -12354,13 +12579,13 @@ var McpClient = class {
|
|
|
12354
12579
|
}
|
|
12355
12580
|
/** Promise 超时包装 */
|
|
12356
12581
|
withTimeout(promise, ms, label) {
|
|
12357
|
-
return new Promise((
|
|
12582
|
+
return new Promise((resolve14, reject) => {
|
|
12358
12583
|
const timer = setTimeout(() => {
|
|
12359
12584
|
reject(new Error(`MCP [${this.serverId}] ${label} timed out after ${ms}ms`));
|
|
12360
12585
|
}, ms);
|
|
12361
12586
|
promise.then((val) => {
|
|
12362
12587
|
clearTimeout(timer);
|
|
12363
|
-
|
|
12588
|
+
resolve14(val);
|
|
12364
12589
|
}).catch((err) => {
|
|
12365
12590
|
clearTimeout(timer);
|
|
12366
12591
|
reject(err);
|
|
@@ -12864,33 +13089,33 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
12864
13089
|
}
|
|
12865
13090
|
/** Resolve a pending confirm from client response */
|
|
12866
13091
|
resolveConfirm(requestId, approved) {
|
|
12867
|
-
const
|
|
12868
|
-
if (
|
|
13092
|
+
const resolve14 = this.pendingConfirms.get(requestId);
|
|
13093
|
+
if (resolve14) {
|
|
12869
13094
|
this.clearPendingTimer(requestId);
|
|
12870
13095
|
this.pendingConfirms.delete(requestId);
|
|
12871
13096
|
this.confirming = false;
|
|
12872
|
-
|
|
13097
|
+
resolve14(approved);
|
|
12873
13098
|
}
|
|
12874
13099
|
}
|
|
12875
13100
|
/** Resolve a pending batch confirm from client response */
|
|
12876
13101
|
resolveBatchConfirm(requestId, decision) {
|
|
12877
|
-
const
|
|
12878
|
-
if (
|
|
13102
|
+
const resolve14 = this.pendingBatchConfirms.get(requestId);
|
|
13103
|
+
if (resolve14) {
|
|
12879
13104
|
this.clearPendingTimer(requestId);
|
|
12880
13105
|
this.pendingBatchConfirms.delete(requestId);
|
|
12881
13106
|
this.confirming = false;
|
|
12882
13107
|
if (decision === "all" || decision === "none") {
|
|
12883
|
-
|
|
13108
|
+
resolve14(decision);
|
|
12884
13109
|
} else {
|
|
12885
|
-
|
|
13110
|
+
resolve14(new Set(decision));
|
|
12886
13111
|
}
|
|
12887
13112
|
}
|
|
12888
13113
|
}
|
|
12889
13114
|
/** Cancel all pending confirms (e.g., on disconnect) */
|
|
12890
13115
|
cancelAll() {
|
|
12891
|
-
for (const
|
|
13116
|
+
for (const resolve14 of this.pendingConfirms.values()) resolve14(false);
|
|
12892
13117
|
this.pendingConfirms.clear();
|
|
12893
|
-
for (const
|
|
13118
|
+
for (const resolve14 of this.pendingBatchConfirms.values()) resolve14("none");
|
|
12894
13119
|
this.pendingBatchConfirms.clear();
|
|
12895
13120
|
this.confirming = false;
|
|
12896
13121
|
}
|
|
@@ -12966,8 +13191,8 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
12966
13191
|
diff: this.getDiffPreview(call)
|
|
12967
13192
|
};
|
|
12968
13193
|
this.send(msg);
|
|
12969
|
-
return new Promise((
|
|
12970
|
-
this.pendingConfirms.set(requestId,
|
|
13194
|
+
return new Promise((resolve14) => {
|
|
13195
|
+
this.pendingConfirms.set(requestId, resolve14);
|
|
12971
13196
|
this.pendingTimers.set(requestId, setTimeout(() => {
|
|
12972
13197
|
if (this.pendingConfirms.has(requestId)) {
|
|
12973
13198
|
this.resolveConfirm(requestId, false);
|
|
@@ -12991,8 +13216,8 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
|
|
|
12991
13216
|
files
|
|
12992
13217
|
};
|
|
12993
13218
|
this.send(msg);
|
|
12994
|
-
return new Promise((
|
|
12995
|
-
this.pendingBatchConfirms.set(requestId,
|
|
13219
|
+
return new Promise((resolve14) => {
|
|
13220
|
+
this.pendingBatchConfirms.set(requestId, resolve14);
|
|
12996
13221
|
this.pendingTimers.set(requestId, setTimeout(() => {
|
|
12997
13222
|
if (this.pendingBatchConfirms.has(requestId)) {
|
|
12998
13223
|
this.resolveBatchConfirm(requestId, "none");
|
|
@@ -13661,8 +13886,8 @@ function loadContextFiles(options) {
|
|
|
13661
13886
|
}
|
|
13662
13887
|
|
|
13663
13888
|
// src/web/session-handler.ts
|
|
13664
|
-
import { mkdirSync as
|
|
13665
|
-
import { join as
|
|
13889
|
+
import { mkdirSync as mkdirSync16, createWriteStream as createWriteStream3 } from "fs";
|
|
13890
|
+
import { join as join26, dirname as dirname10 } from "path";
|
|
13666
13891
|
|
|
13667
13892
|
// src/repl/commands/project-init.ts
|
|
13668
13893
|
import { existsSync as existsSync26, readFileSync as readFileSync20, readdirSync as readdirSync11, statSync as statSync9 } from "fs";
|
|
@@ -14026,7 +14251,7 @@ ${topic}${contextSection}`;
|
|
|
14026
14251
|
messages.push({
|
|
14027
14252
|
role: "assistant",
|
|
14028
14253
|
content: msg.content,
|
|
14029
|
-
timestamp: msg.timestamp
|
|
14254
|
+
timestamp: new Date(msg.timestamp)
|
|
14030
14255
|
});
|
|
14031
14256
|
} else {
|
|
14032
14257
|
const prefix = `[${msg.speakerName} (${msg.speaker})]:`;
|
|
@@ -14034,7 +14259,7 @@ ${topic}${contextSection}`;
|
|
|
14034
14259
|
role: "user",
|
|
14035
14260
|
content: `${prefix}
|
|
14036
14261
|
${msg.content}`,
|
|
14037
|
-
timestamp: msg.timestamp
|
|
14262
|
+
timestamp: new Date(msg.timestamp)
|
|
14038
14263
|
});
|
|
14039
14264
|
}
|
|
14040
14265
|
}
|
|
@@ -14384,27 +14609,27 @@ function getPreset(id) {
|
|
|
14384
14609
|
// src/hub/resolve-providers.ts
|
|
14385
14610
|
function resolveRoleProviders(roles, lookup, defaultProvider, defaultModel, available, mix) {
|
|
14386
14611
|
const warnings = [];
|
|
14387
|
-
let
|
|
14612
|
+
let pool2 = null;
|
|
14388
14613
|
if (mix !== void 0 && mix !== false) {
|
|
14389
14614
|
if (typeof mix === "string" && mix.trim().length > 0) {
|
|
14390
14615
|
const requested = mix.split(",").map((s) => s.trim()).filter(Boolean);
|
|
14391
|
-
|
|
14616
|
+
pool2 = [];
|
|
14392
14617
|
for (const id of requested) {
|
|
14393
|
-
if (lookup.has(id))
|
|
14618
|
+
if (lookup.has(id)) pool2.push(id);
|
|
14394
14619
|
else warnings.push(`--mix: provider "${id}" not configured \u2014 skipped.`);
|
|
14395
14620
|
}
|
|
14396
14621
|
} else {
|
|
14397
|
-
|
|
14622
|
+
pool2 = [...available];
|
|
14398
14623
|
}
|
|
14399
|
-
if (
|
|
14624
|
+
if (pool2.length === 0) {
|
|
14400
14625
|
warnings.push(`--mix: no usable providers \u2014 all roles fall back to "${defaultProvider}".`);
|
|
14401
|
-
|
|
14626
|
+
pool2 = [defaultProvider];
|
|
14402
14627
|
}
|
|
14403
14628
|
}
|
|
14404
14629
|
const providerDefaultModel = (pid) => pid === defaultProvider ? defaultModel : lookup.defaultModelFor(pid) ?? defaultModel;
|
|
14405
14630
|
const assignments = [];
|
|
14406
14631
|
const outRoles = roles.map((role, i) => {
|
|
14407
|
-
const desired =
|
|
14632
|
+
const desired = pool2 ? pool2[i % pool2.length] : role.provider;
|
|
14408
14633
|
let providerId;
|
|
14409
14634
|
let fellBack = false;
|
|
14410
14635
|
if (desired && lookup.has(desired)) {
|
|
@@ -14417,7 +14642,7 @@ function resolveRoleProviders(roles, lookup, defaultProvider, defaultModel, avai
|
|
|
14417
14642
|
providerId = defaultProvider;
|
|
14418
14643
|
}
|
|
14419
14644
|
const explicitProvider = role.provider ?? defaultProvider;
|
|
14420
|
-
const modelId = !
|
|
14645
|
+
const modelId = !pool2 && role.model && explicitProvider === providerId ? role.model : providerDefaultModel(providerId);
|
|
14421
14646
|
assignments.push({ roleId: role.id, roleName: role.name, providerId, modelId, fellBack });
|
|
14422
14647
|
return { ...role, provider: providerId, model: modelId };
|
|
14423
14648
|
});
|
|
@@ -14429,14 +14654,14 @@ import { join as join21 } from "path";
|
|
|
14429
14654
|
function discussionToMessages(state2) {
|
|
14430
14655
|
const out = [];
|
|
14431
14656
|
const t0 = state2.messages[0]?.timestamp ?? /* @__PURE__ */ new Date();
|
|
14432
|
-
out.push({ role: "user", content: `\u{1F3DB} Topic: ${state2.topic}`, timestamp: t0 });
|
|
14657
|
+
out.push({ role: "user", content: `\u{1F3DB} Topic: ${state2.topic}`, timestamp: new Date(t0) });
|
|
14433
14658
|
for (const m of state2.messages) {
|
|
14434
14659
|
if (m.speaker === "system") {
|
|
14435
|
-
out.push({ role: "system", content: m.content, timestamp: m.timestamp });
|
|
14660
|
+
out.push({ role: "system", content: m.content, timestamp: new Date(m.timestamp) });
|
|
14436
14661
|
continue;
|
|
14437
14662
|
}
|
|
14438
14663
|
if (m.speaker === "human") {
|
|
14439
|
-
out.push({ role: "user", content: `\u{1F9ED} ${m.speakerName}: ${m.content}`, timestamp: m.timestamp });
|
|
14664
|
+
out.push({ role: "user", content: `\u{1F9ED} ${m.speakerName}: ${m.content}`, timestamp: new Date(m.timestamp) });
|
|
14440
14665
|
continue;
|
|
14441
14666
|
}
|
|
14442
14667
|
if (m.passed || !m.content.trim()) continue;
|
|
@@ -14446,7 +14671,7 @@ function discussionToMessages(state2) {
|
|
|
14446
14671
|
content: `**${m.speakerName}** (${m.speaker})${tag}
|
|
14447
14672
|
|
|
14448
14673
|
${m.content}`,
|
|
14449
|
-
timestamp: m.timestamp
|
|
14674
|
+
timestamp: new Date(m.timestamp)
|
|
14450
14675
|
});
|
|
14451
14676
|
}
|
|
14452
14677
|
if (state2.summary && state2.summary.trim()) {
|
|
@@ -14468,1708 +14693,1726 @@ async function persistDiscussion(state2, config, defaultProvider, defaultModel)
|
|
|
14468
14693
|
|
|
14469
14694
|
// src/web/commands/provider-commands.ts
|
|
14470
14695
|
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();
|
|
14696
|
+
const id = args[0];
|
|
14697
|
+
if (!id) {
|
|
14698
|
+
ctx.send({ type: "error", message: "Usage: /provider <id>" });
|
|
14699
|
+
return;
|
|
14700
|
+
}
|
|
14701
|
+
const p = ctx.providers.get(id);
|
|
14702
|
+
if (!p) {
|
|
14703
|
+
ctx.send({ type: "error", message: `Provider "${id}" not available` });
|
|
14704
|
+
return;
|
|
14487
14705
|
}
|
|
14706
|
+
ctx.currentProvider = id;
|
|
14707
|
+
ctx.currentModel = p.info.defaultModel;
|
|
14708
|
+
ctx.updateContextWindow();
|
|
14709
|
+
ctx.send({ type: "info", message: `Switched to provider: ${id} (${ctx.currentModel})` });
|
|
14710
|
+
ctx.sendStatus();
|
|
14488
14711
|
}
|
|
14489
14712
|
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;
|
|
14713
|
+
const sub = args[0];
|
|
14714
|
+
if (!sub) {
|
|
14715
|
+
ctx.send({ type: "error", message: "Usage: /model <id>|refresh [provider]|cache [clear [provider]]" });
|
|
14716
|
+
return;
|
|
14717
|
+
}
|
|
14718
|
+
if (sub === "refresh") {
|
|
14719
|
+
const providerId = args[1] ?? ctx.currentProvider;
|
|
14720
|
+
try {
|
|
14721
|
+
const models2 = await ctx.providers.refreshModels(providerId);
|
|
14722
|
+
ctx.send({ type: "info", message: `Refreshed ${models2.length} model(s) for ${providerId}.` });
|
|
14723
|
+
ctx.sendStatus();
|
|
14724
|
+
} catch (err) {
|
|
14725
|
+
ctx.send({ type: "error", message: `Model refresh failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
14520
14726
|
}
|
|
14521
|
-
|
|
14522
|
-
|
|
14523
|
-
|
|
14524
|
-
if (
|
|
14525
|
-
|
|
14727
|
+
return;
|
|
14728
|
+
}
|
|
14729
|
+
if (sub === "cache") {
|
|
14730
|
+
if ((args[1] ?? "").toLowerCase() === "clear") {
|
|
14731
|
+
const providerId = args[2];
|
|
14732
|
+
ctx.providers.clearModelCache(providerId);
|
|
14733
|
+
ctx.send({ type: "info", message: providerId ? `Cleared model cache for ${providerId}.` : "Cleared model cache." });
|
|
14526
14734
|
return;
|
|
14527
14735
|
}
|
|
14528
|
-
|
|
14529
|
-
ctx.
|
|
14530
|
-
|
|
14531
|
-
|
|
14736
|
+
const rows = ctx.providers.getModelCacheStatus();
|
|
14737
|
+
ctx.send({
|
|
14738
|
+
type: "info",
|
|
14739
|
+
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")
|
|
14740
|
+
});
|
|
14741
|
+
return;
|
|
14742
|
+
}
|
|
14743
|
+
const modelId = sub;
|
|
14744
|
+
const models = await ctx.providers.listModels(ctx.currentProvider);
|
|
14745
|
+
const found = models.find((m) => m.id === modelId);
|
|
14746
|
+
if (!found) {
|
|
14747
|
+
ctx.send({ type: "error", message: `Model "${modelId}" not found` });
|
|
14748
|
+
return;
|
|
14532
14749
|
}
|
|
14750
|
+
ctx.currentModel = modelId;
|
|
14751
|
+
ctx.updateContextWindow();
|
|
14752
|
+
ctx.send({ type: "info", message: `Switched to model: ${modelId}` });
|
|
14753
|
+
ctx.sendStatus();
|
|
14533
14754
|
}
|
|
14534
14755
|
async function handleThink(args, ctx) {
|
|
14535
|
-
|
|
14536
|
-
|
|
14537
|
-
|
|
14538
|
-
|
|
14539
|
-
|
|
14540
|
-
|
|
14541
|
-
ctx.sendStatus();
|
|
14542
|
-
}
|
|
14756
|
+
const sub = args[0];
|
|
14757
|
+
if (sub === "on") ctx.runtimeThinking = true;
|
|
14758
|
+
else if (sub === "off") ctx.runtimeThinking = false;
|
|
14759
|
+
else ctx.runtimeThinking = !ctx.runtimeThinking;
|
|
14760
|
+
ctx.send({ type: "info", message: `Thinking mode: ${ctx.runtimeThinking ? "ON" : "OFF"}` });
|
|
14761
|
+
ctx.sendStatus();
|
|
14543
14762
|
}
|
|
14544
14763
|
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();
|
|
14764
|
+
const sub = args[0];
|
|
14765
|
+
if (sub === "exit" || sub === "cancel") {
|
|
14766
|
+
ctx.planMode = false;
|
|
14767
|
+
ctx.send({ type: "info", message: "Plan mode OFF." });
|
|
14768
|
+
} else if (sub === "execute") {
|
|
14769
|
+
ctx.planMode = false;
|
|
14770
|
+
ctx.send({ type: "info", message: "Plan mode OFF. Executing with all tools enabled." });
|
|
14771
|
+
} else {
|
|
14772
|
+
ctx.planMode = !ctx.planMode;
|
|
14773
|
+
ctx.send({ type: "info", message: `Plan mode: ${ctx.planMode ? "ON (read-only tools only)" : "OFF"}` });
|
|
14558
14774
|
}
|
|
14775
|
+
ctx.sendStatus();
|
|
14559
14776
|
}
|
|
14560
14777
|
|
|
14561
|
-
// src/web/commands/
|
|
14562
|
-
|
|
14563
|
-
|
|
14564
|
-
|
|
14565
|
-
|
|
14566
|
-
|
|
14567
|
-
|
|
14568
|
-
|
|
14778
|
+
// src/web/commands/_shared.ts
|
|
14779
|
+
import { existsSync as existsSync29, readFileSync as readFileSync22, writeFileSync as writeFileSync3, mkdirSync as mkdirSync14, readdirSync as readdirSync13, statSync as statSync12, createWriteStream } from "fs";
|
|
14780
|
+
import { join as join24, resolve as resolve10, dirname as dirname8 } from "path";
|
|
14781
|
+
|
|
14782
|
+
// src/cli/review-prompts.ts
|
|
14783
|
+
function buildReviewPrompt(diff, gitContextStr, detailed) {
|
|
14784
|
+
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.";
|
|
14785
|
+
return `# Code Review Request
|
|
14786
|
+
|
|
14787
|
+
${level}
|
|
14788
|
+
|
|
14789
|
+
## Git Status
|
|
14790
|
+
${gitContextStr}
|
|
14791
|
+
|
|
14792
|
+
## Code Changes (diff)
|
|
14793
|
+
\`\`\`diff
|
|
14794
|
+
${diff}
|
|
14795
|
+
\`\`\`
|
|
14796
|
+
|
|
14797
|
+
## Output Format
|
|
14798
|
+
Please structure your review as follows:
|
|
14799
|
+
1. **Overall Assessment**: One-sentence summary of the change quality
|
|
14800
|
+
2. **Issues** (if any): Each issue with [Severity] file:line \u2014 description + suggested fix
|
|
14801
|
+
3. **Improvement Suggestions** (if any): Non-critical but recommended optimizations
|
|
14802
|
+
4. **Highlights** (if any): Good practices worth acknowledging
|
|
14803
|
+
|
|
14804
|
+
Severity levels: \u{1F534} Critical / \u{1F7E1} Warning / \u{1F535} Info`;
|
|
14569
14805
|
}
|
|
14570
|
-
|
|
14571
|
-
|
|
14806
|
+
function buildSecurityReviewPrompt(diff, gitContextStr) {
|
|
14807
|
+
return `# Security Vulnerability Review
|
|
14808
|
+
|
|
14809
|
+
Analyze the following code changes **exclusively for security vulnerabilities**.
|
|
14810
|
+
|
|
14811
|
+
## Categories to check:
|
|
14812
|
+
1. **Injection** \u2014 SQL, command, path traversal, XSS, template injection
|
|
14813
|
+
2. **Authentication & Authorization** \u2014 hardcoded credentials, missing auth checks, privilege escalation
|
|
14814
|
+
3. **Secrets & Sensitive Data** \u2014 API keys, tokens, passwords in code, logging sensitive data
|
|
14815
|
+
4. **Input Validation** \u2014 missing validation, unsafe deserialization, buffer issues
|
|
14816
|
+
5. **Cryptography** \u2014 weak algorithms, improper random, hardcoded IVs/salts
|
|
14817
|
+
6. **Dependencies** \u2014 known vulnerable packages, unsafe dynamic imports
|
|
14818
|
+
7. **File System** \u2014 path traversal, unsafe file permissions, symlink attacks
|
|
14819
|
+
8. **Network** \u2014 SSRF, insecure protocols, missing TLS validation
|
|
14820
|
+
|
|
14821
|
+
## Git Status
|
|
14822
|
+
${gitContextStr}
|
|
14823
|
+
|
|
14824
|
+
## Code Changes (diff)
|
|
14825
|
+
\`\`\`diff
|
|
14826
|
+
${diff}
|
|
14827
|
+
\`\`\`
|
|
14828
|
+
|
|
14829
|
+
## Output Format
|
|
14830
|
+
For each finding:
|
|
14831
|
+
- **Severity**: \u{1F534} CRITICAL / \u{1F7E0} HIGH / \u{1F7E1} MEDIUM / \u{1F535} LOW / \u2139\uFE0F INFO
|
|
14832
|
+
- **Category**: (from list above)
|
|
14833
|
+
- **File & location**: file:line
|
|
14834
|
+
- **Description**: what the vulnerability is and how it could be exploited
|
|
14835
|
+
- **Recommended fix**: specific code change to resolve
|
|
14836
|
+
|
|
14837
|
+
If no security issues found, state "\u2705 No security vulnerabilities detected" with a brief explanation of what was checked.`;
|
|
14572
14838
|
}
|
|
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
|
-
}
|
|
14839
|
+
|
|
14840
|
+
// src/core/git-diff.ts
|
|
14841
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
14842
|
+
function readGitDiff(options = {}) {
|
|
14843
|
+
const args = ["diff"];
|
|
14844
|
+
if (options.staged) args.push("--staged");
|
|
14845
|
+
return execFileSync3("git", args, {
|
|
14846
|
+
encoding: "utf-8",
|
|
14847
|
+
timeout: options.timeoutMs ?? 1e4
|
|
14848
|
+
}).trim();
|
|
14591
14849
|
}
|
|
14592
|
-
|
|
14593
|
-
|
|
14594
|
-
|
|
14595
|
-
|
|
14596
|
-
|
|
14597
|
-
|
|
14598
|
-
|
|
14599
|
-
|
|
14600
|
-
|
|
14601
|
-
|
|
14602
|
-
|
|
14603
|
-
|
|
14604
|
-
|
|
14605
|
-
|
|
14606
|
-
|
|
14607
|
-
|
|
14608
|
-
|
|
14609
|
-
|
|
14610
|
-
|
|
14611
|
-
|
|
14612
|
-
|
|
14613
|
-
|
|
14614
|
-
|
|
14615
|
-
|
|
14616
|
-
|
|
14617
|
-
|
|
14618
|
-
|
|
14619
|
-
|
|
14620
|
-
|
|
14621
|
-
|
|
14622
|
-
|
|
14623
|
-
|
|
14624
|
-
|
|
14625
|
-
|
|
14626
|
-
|
|
14627
|
-
|
|
14628
|
-
|
|
14629
|
-
|
|
14630
|
-
|
|
14631
|
-
|
|
14632
|
-
|
|
14633
|
-
|
|
14634
|
-
|
|
14635
|
-
|
|
14636
|
-
|
|
14637
|
-
|
|
14638
|
-
|
|
14639
|
-
|
|
14640
|
-
|
|
14641
|
-
|
|
14642
|
-
|
|
14643
|
-
|
|
14644
|
-
|
|
14645
|
-
|
|
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>" });
|
|
14850
|
+
|
|
14851
|
+
// src/diagnostics/doctor-report.ts
|
|
14852
|
+
import { existsSync as existsSync28, statSync as statSync11 } from "fs";
|
|
14853
|
+
import { join as join23 } from "path";
|
|
14854
|
+
import { arch as arch2, platform as platform7, release as release2 } from "os";
|
|
14855
|
+
|
|
14856
|
+
// src/diagnostics/crash-log.ts
|
|
14857
|
+
import {
|
|
14858
|
+
existsSync as existsSync27,
|
|
14859
|
+
mkdirSync as mkdirSync13,
|
|
14860
|
+
readdirSync as readdirSync12,
|
|
14861
|
+
readFileSync as readFileSync21,
|
|
14862
|
+
statSync as statSync10,
|
|
14863
|
+
unlinkSync as unlinkSync6,
|
|
14864
|
+
writeFileSync as writeFileSync2
|
|
14865
|
+
} from "fs";
|
|
14866
|
+
import { join as join22 } from "path";
|
|
14867
|
+
import { homedir as homedir8, platform as platform6, release, arch } from "os";
|
|
14868
|
+
var LOGS_DIR_NAME = "logs";
|
|
14869
|
+
var CRASH_LOG_PREFIX = "crash-";
|
|
14870
|
+
function getLogsDir(configDir) {
|
|
14871
|
+
const base = configDir ?? join22(homedir8(), CONFIG_DIR_NAME);
|
|
14872
|
+
return join22(base, LOGS_DIR_NAME);
|
|
14873
|
+
}
|
|
14874
|
+
function truncate(s, n) {
|
|
14875
|
+
return s.length <= n ? s : s.slice(0, n) + "\u2026";
|
|
14876
|
+
}
|
|
14877
|
+
function listRecentCrashes(limit = 10, configDir) {
|
|
14878
|
+
const dir = getLogsDir(configDir);
|
|
14879
|
+
if (!existsSync27(dir)) return [];
|
|
14880
|
+
const files = [];
|
|
14881
|
+
let names;
|
|
14882
|
+
try {
|
|
14883
|
+
names = readdirSync12(dir);
|
|
14884
|
+
} catch {
|
|
14885
|
+
return [];
|
|
14886
|
+
}
|
|
14887
|
+
for (const name of names) {
|
|
14888
|
+
if (!name.startsWith(CRASH_LOG_PREFIX) || !name.endsWith(".log")) continue;
|
|
14889
|
+
const full = join22(dir, name);
|
|
14890
|
+
try {
|
|
14891
|
+
const st = statSync10(full);
|
|
14892
|
+
const head = readFileSync21(full, "utf-8").split("\n").slice(0, 20);
|
|
14893
|
+
const tsLine = head.find((l) => l.startsWith("timestamp:"));
|
|
14894
|
+
const kindLine = head.find((l) => l.startsWith("kind:"));
|
|
14895
|
+
const msgIdx = head.findIndex((l) => l.trim() === "## message");
|
|
14896
|
+
const message = msgIdx >= 0 && head[msgIdx + 1] ? head[msgIdx + 1] : "";
|
|
14897
|
+
files.push({
|
|
14898
|
+
file: full,
|
|
14899
|
+
timestamp: tsLine ? tsLine.replace("timestamp:", "").trim() : st.mtime.toISOString(),
|
|
14900
|
+
kind: kindLine ? kindLine.replace("kind:", "").trim() : "unknown",
|
|
14901
|
+
message: truncate(message, 200),
|
|
14902
|
+
sizeBytes: st.size
|
|
14903
|
+
});
|
|
14904
|
+
} catch {
|
|
14720
14905
|
}
|
|
14721
14906
|
}
|
|
14907
|
+
files.sort((a, b) => a.timestamp < b.timestamp ? 1 : -1);
|
|
14908
|
+
return files.slice(0, limit);
|
|
14722
14909
|
}
|
|
14723
|
-
|
|
14724
|
-
|
|
14725
|
-
|
|
14726
|
-
|
|
14910
|
+
function getConfigDirUsage(configDir) {
|
|
14911
|
+
const base = configDir ?? join22(homedir8(), CONFIG_DIR_NAME);
|
|
14912
|
+
if (!existsSync27(base)) return { totalBytes: 0, entries: [] };
|
|
14913
|
+
const entries = [];
|
|
14914
|
+
let total = 0;
|
|
14915
|
+
let names;
|
|
14916
|
+
try {
|
|
14917
|
+
names = readdirSync12(base);
|
|
14918
|
+
} catch {
|
|
14919
|
+
return { totalBytes: 0, entries: [] };
|
|
14920
|
+
}
|
|
14921
|
+
for (const name of names) {
|
|
14922
|
+
const full = join22(base, name);
|
|
14923
|
+
const bytes = dirSize(full);
|
|
14924
|
+
entries.push({ name, bytes });
|
|
14925
|
+
total += bytes;
|
|
14727
14926
|
}
|
|
14927
|
+
entries.sort((a, b) => b.bytes - a.bytes);
|
|
14928
|
+
return { totalBytes: total, entries };
|
|
14728
14929
|
}
|
|
14729
|
-
|
|
14730
|
-
{
|
|
14731
|
-
const
|
|
14732
|
-
|
|
14733
|
-
|
|
14734
|
-
let
|
|
14735
|
-
|
|
14736
|
-
|
|
14737
|
-
inputTokens: u.inputTokens + u.cacheReadTokens,
|
|
14738
|
-
outputTokens: u.outputTokens,
|
|
14739
|
-
cacheCreationTokens: u.cacheCreationTokens,
|
|
14740
|
-
cacheReadTokens: 0
|
|
14741
|
-
});
|
|
14742
|
-
if (withoutCache != null) savings = withoutCache - cost;
|
|
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)`);
|
|
14930
|
+
function dirSize(path3) {
|
|
14931
|
+
try {
|
|
14932
|
+
const st = statSync10(path3);
|
|
14933
|
+
if (st.isFile()) return st.size;
|
|
14934
|
+
if (!st.isDirectory()) return 0;
|
|
14935
|
+
let sum = 0;
|
|
14936
|
+
for (const name of readdirSync12(path3)) {
|
|
14937
|
+
sum += dirSize(join22(path3, name));
|
|
14764
14938
|
}
|
|
14765
|
-
|
|
14939
|
+
return sum;
|
|
14940
|
+
} catch {
|
|
14941
|
+
return 0;
|
|
14766
14942
|
}
|
|
14767
14943
|
}
|
|
14768
|
-
|
|
14769
|
-
|
|
14770
|
-
|
|
14771
|
-
|
|
14772
|
-
|
|
14773
|
-
|
|
14774
|
-
|
|
14775
|
-
|
|
14776
|
-
|
|
14777
|
-
if (cps.length === 0) {
|
|
14778
|
-
ctx.send({ type: "info", message: "No checkpoints. Use /checkpoint save <name> to create one." });
|
|
14779
|
-
} else {
|
|
14780
|
-
const lines = [`\u{1F4CC} Checkpoints (${cps.length}):`, ""];
|
|
14781
|
-
for (const cp of cps) {
|
|
14782
|
-
const ts = cp.timestamp.toLocaleString();
|
|
14783
|
-
lines.push(` ${cp.name.padEnd(20)} msg #${cp.messageIndex} ${ts}`);
|
|
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.` });
|
|
14944
|
+
|
|
14945
|
+
// src/diagnostics/doctor-report.ts
|
|
14946
|
+
function checkFile(label, path3) {
|
|
14947
|
+
const exists = existsSync28(path3);
|
|
14948
|
+
let sizeBytes = null;
|
|
14949
|
+
if (exists) {
|
|
14950
|
+
try {
|
|
14951
|
+
sizeBytes = statSync11(path3).size;
|
|
14952
|
+
} catch {
|
|
14819
14953
|
}
|
|
14820
14954
|
}
|
|
14955
|
+
return { label, path: path3, exists, sizeBytes };
|
|
14821
14956
|
}
|
|
14822
|
-
|
|
14823
|
-
|
|
14824
|
-
|
|
14825
|
-
|
|
14826
|
-
|
|
14827
|
-
|
|
14828
|
-
|
|
14829
|
-
|
|
14830
|
-
|
|
14831
|
-
|
|
14832
|
-
|
|
14833
|
-
|
|
14834
|
-
|
|
14835
|
-
|
|
14836
|
-
|
|
14837
|
-
|
|
14838
|
-
|
|
14839
|
-
|
|
14840
|
-
|
|
14841
|
-
|
|
14842
|
-
|
|
14843
|
-
|
|
14844
|
-
|
|
14845
|
-
|
|
14846
|
-
|
|
14847
|
-
|
|
14848
|
-
|
|
14849
|
-
|
|
14850
|
-
|
|
14851
|
-
|
|
14852
|
-
|
|
14853
|
-
|
|
14854
|
-
|
|
14855
|
-
|
|
14856
|
-
|
|
14857
|
-
|
|
14858
|
-
|
|
14859
|
-
|
|
14860
|
-
|
|
14861
|
-
|
|
14862
|
-
|
|
14863
|
-
|
|
14864
|
-
|
|
14865
|
-
|
|
14866
|
-
|
|
14867
|
-
|
|
14868
|
-
|
|
14869
|
-
|
|
14870
|
-
|
|
14871
|
-
|
|
14872
|
-
|
|
14873
|
-
|
|
14874
|
-
|
|
14875
|
-
|
|
14876
|
-
|
|
14877
|
-
|
|
14878
|
-
|
|
14879
|
-
|
|
14880
|
-
|
|
14881
|
-
|
|
14882
|
-
|
|
14883
|
-
|
|
14884
|
-
|
|
14885
|
-
|
|
14886
|
-
|
|
14887
|
-
|
|
14888
|
-
|
|
14889
|
-
|
|
14890
|
-
|
|
14891
|
-
|
|
14892
|
-
|
|
14893
|
-
|
|
14894
|
-
|
|
14895
|
-
|
|
14896
|
-
|
|
14897
|
-
|
|
14898
|
-
|
|
14899
|
-
|
|
14900
|
-
|
|
14901
|
-
|
|
14902
|
-
|
|
14903
|
-
|
|
14904
|
-
|
|
14905
|
-
|
|
14906
|
-
|
|
14907
|
-
|
|
14908
|
-
|
|
14909
|
-
|
|
14910
|
-
|
|
14911
|
-
|
|
14912
|
-
|
|
14913
|
-
|
|
14914
|
-
|
|
14915
|
-
|
|
14916
|
-
|
|
14917
|
-
|
|
14918
|
-
|
|
14919
|
-
|
|
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
|
-
}
|
|
14957
|
+
function buildDoctorReport(options) {
|
|
14958
|
+
const config = options.config;
|
|
14959
|
+
const cwd = options.cwd ?? process.cwd();
|
|
14960
|
+
const configDir = config.getConfigDir();
|
|
14961
|
+
const projectRoot = getGitRoot(cwd) ?? cwd;
|
|
14962
|
+
const contextCfg = config.get("context");
|
|
14963
|
+
const contextSetting = config.get("contextFile");
|
|
14964
|
+
const context = loadContextFiles({
|
|
14965
|
+
cwd,
|
|
14966
|
+
configDir,
|
|
14967
|
+
projectRoot,
|
|
14968
|
+
setting: contextSetting,
|
|
14969
|
+
maxBytes: contextCfg.projectDocMaxBytes,
|
|
14970
|
+
fallbackFilenames: contextCfg.projectDocFallbackFilenames
|
|
14971
|
+
});
|
|
14972
|
+
const hooksConfig = config.get("hooks");
|
|
14973
|
+
const hooks = listHooks(hooksConfig, configDir);
|
|
14974
|
+
const pendingHooks = getPendingHookTrust(hooksConfig, configDir);
|
|
14975
|
+
const plugins = listInstalledPlugins(configDir);
|
|
14976
|
+
const activeAssets = getActivePluginAssets(configDir);
|
|
14977
|
+
const network = config.get("networkPolicy");
|
|
14978
|
+
const mcpServers = config.get("mcpServers");
|
|
14979
|
+
const allStats = getStatsSnapshot();
|
|
14980
|
+
return {
|
|
14981
|
+
version: VERSION,
|
|
14982
|
+
npmCheck: options.npmCheck,
|
|
14983
|
+
node: process.version,
|
|
14984
|
+
platform: `${platform7()} ${release2()} (${arch2()})`,
|
|
14985
|
+
cwd,
|
|
14986
|
+
projectRoot,
|
|
14987
|
+
configDir,
|
|
14988
|
+
providers: options.providers.listAll().map((p) => ({ id: p.id, displayName: p.displayName, configured: p.configured })),
|
|
14989
|
+
files: [
|
|
14990
|
+
checkFile("config.json", join23(configDir, "config.json")),
|
|
14991
|
+
checkFile("memory.md", join23(configDir, MEMORY_FILE_NAME)),
|
|
14992
|
+
checkFile("memory.jsonl", join23(configDir, "memory.jsonl")),
|
|
14993
|
+
checkFile("dev-state.md", join23(configDir, DEV_STATE_FILE_NAME)),
|
|
14994
|
+
checkFile("hooks-trust.json", join23(configDir, "hooks-trust.json")),
|
|
14995
|
+
checkFile("plugin-state.json", join23(configDir, "plugins", "plugin-state.json"))
|
|
14996
|
+
],
|
|
14997
|
+
context: {
|
|
14998
|
+
enabled: contextSetting !== false,
|
|
14999
|
+
setting: contextSetting,
|
|
15000
|
+
candidates: context.candidates,
|
|
15001
|
+
maxBytes: context.maxBytes,
|
|
15002
|
+
layers: context.layers.map((l) => ({ level: l.level, fileName: l.fileName, displayPath: l.displayPath, bytes: l.byteCount, chars: l.charCount, truncated: l.truncated })),
|
|
15003
|
+
skipped: context.skipped.map((s) => ({ level: s.level, fileName: s.fileName, displayPath: s.displayPath, reason: s.reason, message: s.message }))
|
|
15004
|
+
},
|
|
15005
|
+
permissions: {
|
|
15006
|
+
defaultProfile: config.get("defaultPermissionProfile"),
|
|
15007
|
+
allowedProfiles: config.get("allowedPermissionProfiles"),
|
|
15008
|
+
customProfileCount: Object.keys(config.get("permissionProfiles")).length,
|
|
15009
|
+
legacyDefaultAction: config.get("defaultPermission"),
|
|
15010
|
+
ruleCount: config.get("permissionRules").length
|
|
15011
|
+
},
|
|
15012
|
+
networkPolicy: {
|
|
15013
|
+
enabled: network.enabled,
|
|
15014
|
+
defaultAction: network.defaultAction,
|
|
15015
|
+
allowDomains: network.allowDomains.length,
|
|
15016
|
+
denyDomains: network.denyDomains.length,
|
|
15017
|
+
allowPorts: network.allowPorts.length,
|
|
15018
|
+
denyPorts: network.denyPorts.length,
|
|
15019
|
+
allowPrivateNetwork: network.allowPrivateNetwork,
|
|
15020
|
+
toolOverrides: Object.fromEntries(Object.entries(network.tools).filter(([, v]) => v !== void 0))
|
|
15021
|
+
},
|
|
15022
|
+
hooks: {
|
|
15023
|
+
enabled: hooksConfig?.enabled !== false,
|
|
15024
|
+
total: hooks.length,
|
|
15025
|
+
project: hooks.filter((h) => h.source === "project").length,
|
|
15026
|
+
pendingTrust: pendingHooks.length,
|
|
15027
|
+
disabled: hooks.filter((h) => h.disabled).length
|
|
15028
|
+
},
|
|
15029
|
+
plugins: {
|
|
15030
|
+
root: join23(configDir, "plugins"),
|
|
15031
|
+
installed: plugins.length,
|
|
15032
|
+
active: activeAssets.plugins.length,
|
|
15033
|
+
invalid: plugins.filter((p) => !p.valid).length,
|
|
15034
|
+
untrusted: plugins.filter((p) => p.valid && !p.trusted).length,
|
|
15035
|
+
disabled: plugins.filter((p) => p.valid && !p.enabled).length,
|
|
15036
|
+
mcpServers: Object.keys(activeAssets.mcpServers).length,
|
|
15037
|
+
skillDirs: activeAssets.skillDirs.length,
|
|
15038
|
+
commandDirs: activeAssets.commandDirs.length,
|
|
15039
|
+
agentDirs: activeAssets.agentDirs.length,
|
|
15040
|
+
entries: plugins.map((p) => ({ name: p.name, version: p.manifest.version, enabled: p.enabled, trusted: p.trusted, valid: p.valid, error: p.error }))
|
|
15041
|
+
},
|
|
15042
|
+
mcp: {
|
|
15043
|
+
enabled: config.get("mcpEnabled"),
|
|
15044
|
+
configured: Object.keys(mcpServers).length,
|
|
15045
|
+
projectConfigExists: existsSync28(join23(projectRoot, MCP_PROJECT_CONFIG_NAME)),
|
|
15046
|
+
statuses: options.mcpStatuses ?? []
|
|
15047
|
+
},
|
|
15048
|
+
recentCrashes: listRecentCrashes(5, configDir),
|
|
15049
|
+
diskUsage: getConfigDirUsage(configDir),
|
|
15050
|
+
toolStats: {
|
|
15051
|
+
totalCalls: allStats.reduce((a, b) => a + b.calls, 0),
|
|
15052
|
+
totalFailures: allStats.reduce((a, b) => a + b.failures, 0),
|
|
15053
|
+
topUsed: getTopUsedTools(5),
|
|
15054
|
+
topFailing: getTopFailingTools(5)
|
|
14926
15055
|
}
|
|
14927
|
-
|
|
14928
|
-
|
|
14929
|
-
|
|
14930
|
-
|
|
14931
|
-
|
|
14932
|
-
|
|
14933
|
-
|
|
14934
|
-
|
|
14935
|
-
|
|
14936
|
-
|
|
14937
|
-
|
|
14938
|
-
|
|
14939
|
-
|
|
14940
|
-
|
|
14941
|
-
|
|
14942
|
-
|
|
14943
|
-
|
|
14944
|
-
|
|
14945
|
-
|
|
14946
|
-
|
|
14947
|
-
|
|
14948
|
-
|
|
14949
|
-
|
|
14950
|
-
|
|
14951
|
-
|
|
14952
|
-
|
|
14953
|
-
|
|
14954
|
-
|
|
14955
|
-
|
|
14956
|
-
|
|
14957
|
-
|
|
14958
|
-
|
|
14959
|
-
|
|
14960
|
-
|
|
14961
|
-
|
|
14962
|
-
|
|
14963
|
-
|
|
14964
|
-
|
|
15056
|
+
};
|
|
15057
|
+
}
|
|
15058
|
+
|
|
15059
|
+
// src/diagnostics/doctor-cli.ts
|
|
15060
|
+
function formatBytes(n) {
|
|
15061
|
+
if (n < 1024) return `${n} B`;
|
|
15062
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
15063
|
+
if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
|
|
15064
|
+
return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
|
15065
|
+
}
|
|
15066
|
+
function formatDoctorReport(report, ansi = true) {
|
|
15067
|
+
const wrap = (code, s) => ansi ? `${code}${s}\x1B[0m` : s;
|
|
15068
|
+
const B = (s) => wrap("\x1B[1m", s);
|
|
15069
|
+
const D = (s) => wrap("\x1B[2m", s);
|
|
15070
|
+
const G = (s) => wrap("\x1B[32m", s);
|
|
15071
|
+
const Y = (s) => wrap("\x1B[33m", s);
|
|
15072
|
+
const R = (s) => wrap("\x1B[31m", s);
|
|
15073
|
+
const out = [];
|
|
15074
|
+
out.push("");
|
|
15075
|
+
out.push(B("\u{1FA7A} AI-CLI Health Check"));
|
|
15076
|
+
out.push("");
|
|
15077
|
+
const nc = report.npmCheck;
|
|
15078
|
+
let versionLine = ` version: ${report.version}`;
|
|
15079
|
+
if (nc?.status === "up-to-date") versionLine += ` ${D("(latest on npm)")}`;
|
|
15080
|
+
else if (nc?.status === "outdated" && nc.latest) versionLine += ` ${Y(`\u2192 ${nc.latest} available`)} ${D("(npm i -g jinzd-ai-cli@latest)")}`;
|
|
15081
|
+
else if (nc?.status === "skipped") versionLine += ` ${D(`(npm check skipped: ${nc.reason ?? "unknown"})`)}`;
|
|
15082
|
+
out.push(versionLine);
|
|
15083
|
+
out.push(` node: ${report.node}`);
|
|
15084
|
+
out.push(` platform: ${report.platform}`);
|
|
15085
|
+
out.push(` cwd: ${report.cwd}`);
|
|
15086
|
+
out.push(` project: ${report.projectRoot}`);
|
|
15087
|
+
out.push(` config: ${report.configDir}`);
|
|
15088
|
+
out.push("");
|
|
15089
|
+
out.push(B("API Keys:"));
|
|
15090
|
+
for (const p of report.providers) {
|
|
15091
|
+
out.push(` ${p.configured ? G("\u2713") : D("\u25CB")} ${p.id.padEnd(14)} ${p.configured ? G("configured") : D("not configured")}`);
|
|
15092
|
+
}
|
|
15093
|
+
out.push("");
|
|
15094
|
+
out.push(B("Context Files:"));
|
|
15095
|
+
out.push(` status: ${report.context.enabled ? G("enabled") : D("disabled")} \xB7 max ${formatBytes(report.context.maxBytes)}`);
|
|
15096
|
+
if (report.context.layers.length === 0) out.push(` ${D("(no context files loaded)")}`);
|
|
15097
|
+
for (const l of report.context.layers) {
|
|
15098
|
+
out.push(` ${G("\u2713")} ${l.level.padEnd(7)} ${l.displayPath} ${D(`(${formatBytes(l.bytes)}${l.truncated ? ", truncated" : ""})`)}`);
|
|
15099
|
+
}
|
|
15100
|
+
if (report.context.skipped.length > 0) {
|
|
15101
|
+
out.push(` ${D("skipped:")}`);
|
|
15102
|
+
for (const s of report.context.skipped.slice(0, 5)) {
|
|
15103
|
+
out.push(` ${Y("!")} ${s.level}:${s.fileName} ${s.reason}${s.message ? ` \xB7 ${s.message}` : ""}`);
|
|
14965
15104
|
}
|
|
14966
|
-
|
|
14967
|
-
|
|
14968
|
-
|
|
14969
|
-
|
|
14970
|
-
|
|
14971
|
-
|
|
14972
|
-
|
|
14973
|
-
|
|
14974
|
-
|
|
14975
|
-
|
|
14976
|
-
|
|
14977
|
-
|
|
14978
|
-
|
|
14979
|
-
|
|
14980
|
-
|
|
14981
|
-
|
|
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();
|
|
15105
|
+
}
|
|
15106
|
+
out.push("");
|
|
15107
|
+
out.push(B("Permissions / Network:"));
|
|
15108
|
+
out.push(` profile: ${report.permissions.defaultProfile} \xB7 allowed ${report.permissions.allowedProfiles.join(", ")} \xB7 rules ${report.permissions.ruleCount} \xB7 custom profiles ${report.permissions.customProfileCount}`);
|
|
15109
|
+
const np = report.networkPolicy;
|
|
15110
|
+
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"}`);
|
|
15111
|
+
const overrides = Object.entries(np.toolOverrides).map(([k, v]) => `${k}=${v}`).join(", ");
|
|
15112
|
+
if (overrides) out.push(` tool overrides: ${overrides}`);
|
|
15113
|
+
out.push("");
|
|
15114
|
+
out.push(B("Hooks / Plugins:"));
|
|
15115
|
+
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}`);
|
|
15116
|
+
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}`);
|
|
15117
|
+
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)`);
|
|
15118
|
+
if (report.plugins.entries.length > 0) {
|
|
15119
|
+
for (const p of report.plugins.entries.slice(0, 5)) {
|
|
15120
|
+
const state2 = p.valid ? `${p.enabled ? "enabled" : "disabled"} \xB7 ${p.trusted ? "trusted" : "untrusted"}` : `invalid \xB7 ${p.error ?? ""}`;
|
|
15121
|
+
out.push(` ${p.valid ? G("\u2713") : R("\u2717")} ${p.name}@${p.version} ${D(state2)}`);
|
|
14988
15122
|
}
|
|
14989
|
-
ctx.send({ type: "error", message: `Unknown subcommand: ${sub}. Use list/new/switch/delete/rename/diff/cherry-pick.` });
|
|
14990
15123
|
}
|
|
14991
|
-
|
|
14992
|
-
|
|
14993
|
-
{
|
|
14994
|
-
|
|
14995
|
-
|
|
14996
|
-
|
|
14997
|
-
}
|
|
14998
|
-
|
|
14999
|
-
|
|
15000
|
-
|
|
15001
|
-
|
|
15002
|
-
|
|
15003
|
-
|
|
15004
|
-
|
|
15005
|
-
|
|
15006
|
-
|
|
15007
|
-
|
|
15008
|
-
|
|
15009
|
-
}
|
|
15010
|
-
|
|
15011
|
-
|
|
15012
|
-
|
|
15013
|
-
|
|
15014
|
-
|
|
15015
|
-
|
|
15016
|
-
|
|
15017
|
-
|
|
15018
|
-
|
|
15019
|
-
|
|
15020
|
-
|
|
15021
|
-
|
|
15022
|
-
|
|
15023
|
-
}
|
|
15124
|
+
out.push("");
|
|
15125
|
+
out.push(B("MCP Servers:"));
|
|
15126
|
+
out.push(` global configured: ${report.mcp.configured} \xB7 project .mcp.json: ${report.mcp.projectConfigExists ? "yes" : "no"} \xB7 mcpEnabled: ${report.mcp.enabled ? "true" : "false"}`);
|
|
15127
|
+
if (report.mcp.statuses.length === 0) out.push(` ${D("(no connected manager status in this mode)")}`);
|
|
15128
|
+
for (const s of report.mcp.statuses) {
|
|
15129
|
+
const state2 = s.connected ? `${G("connected")} \xB7 ${s.serverName} \xB7 ${s.toolCount} tools` : `${R("disconnected")}${s.error ? D(` \xB7 ${s.error}`) : ""}`;
|
|
15130
|
+
out.push(` ${s.connected ? G("\u2713") : R("\u2717")} ${s.serverId.padEnd(16)} ${state2}`);
|
|
15131
|
+
}
|
|
15132
|
+
out.push("");
|
|
15133
|
+
out.push(B("Config Files:"));
|
|
15134
|
+
for (const f of report.files) {
|
|
15135
|
+
const extra = f.exists && f.sizeBytes !== null ? ` ${D(`(${formatBytes(f.sizeBytes)})`)}` : "";
|
|
15136
|
+
out.push(` ${f.exists ? G("\u2713") : D("\u2013")} ${f.label.padEnd(16)} ${f.exists ? f.path : D("(not found)")}${extra}`);
|
|
15137
|
+
}
|
|
15138
|
+
out.push("");
|
|
15139
|
+
out.push(B("Recent Crashes (last 5):"));
|
|
15140
|
+
if (report.recentCrashes.length === 0) out.push(` ${D("(none - clean!)")}`);
|
|
15141
|
+
for (const c of report.recentCrashes) {
|
|
15142
|
+
out.push(` ${R("\u2717")} ${c.timestamp} ${Y(c.kind)}`);
|
|
15143
|
+
out.push(` ${D(c.message)}`);
|
|
15144
|
+
out.push(` ${D(c.file)}`);
|
|
15145
|
+
}
|
|
15146
|
+
out.push("");
|
|
15147
|
+
const ts = report.toolStats;
|
|
15148
|
+
out.push(B("Tool Usage:"));
|
|
15149
|
+
out.push(` total: ${ts.totalCalls} calls \xB7 ${ts.totalFailures} failures (${ts.totalCalls ? (ts.totalFailures * 100 / ts.totalCalls).toFixed(1) : "0.0"}%)`);
|
|
15150
|
+
if (ts.topUsed.length > 0) {
|
|
15151
|
+
out.push(` ${D("top used:")}`);
|
|
15152
|
+
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`);
|
|
15153
|
+
}
|
|
15154
|
+
if (ts.topFailing.length > 0) {
|
|
15155
|
+
out.push(` ${D("top failing:")}`);
|
|
15156
|
+
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));
|
|
15157
|
+
}
|
|
15158
|
+
out.push("");
|
|
15159
|
+
const dirAnnotations = {
|
|
15160
|
+
models: "semantic search model -> /index semantic-clear",
|
|
15161
|
+
index: "symbol index -> /index clear",
|
|
15162
|
+
history: "conversation history",
|
|
15163
|
+
logs: "crash logs",
|
|
15164
|
+
"memory-index": "chat memory index -> /memory index-clear",
|
|
15165
|
+
plugins: "installed package plugins"
|
|
15166
|
+
};
|
|
15167
|
+
out.push(B("Disk Usage (~/.aicli):"));
|
|
15168
|
+
out.push(` total: ${formatBytes(report.diskUsage.totalBytes)}`);
|
|
15169
|
+
for (const e of report.diskUsage.entries.slice(0, 10)) {
|
|
15170
|
+
const note = dirAnnotations[e.name];
|
|
15171
|
+
out.push(` ${e.name.padEnd(18)} ${formatBytes(e.bytes)}${note ? ` ${D(`\xB7 ${note}`)}` : ""}`);
|
|
15024
15172
|
}
|
|
15173
|
+
out.push("");
|
|
15174
|
+
out.push(G("\u2713 Health check complete"));
|
|
15175
|
+
out.push("");
|
|
15176
|
+
return out.join("\n");
|
|
15025
15177
|
}
|
|
15026
|
-
|
|
15027
|
-
|
|
15028
|
-
|
|
15029
|
-
|
|
15030
|
-
|
|
15031
|
-
|
|
15032
|
-
const
|
|
15033
|
-
if (
|
|
15034
|
-
|
|
15035
|
-
|
|
15036
|
-
|
|
15037
|
-
for (let i = 0; i < session.messages.length; i++) {
|
|
15038
|
-
const m = session.messages[i];
|
|
15039
|
-
const text = getContentText(m.content).replace(/\n/g, " ").slice(0, 60);
|
|
15040
|
-
const pin = cpIndices.includes(i) ? " \u{1F4CC}" : "";
|
|
15041
|
-
lines.push(` [${i + 1}] ${m.role.padEnd(10)} ${text}${pin}`);
|
|
15042
|
-
}
|
|
15043
|
-
lines.push("", "Usage: /rewind <n> \u2014 rewind to message N", "\u{1F4CC} = file checkpoint");
|
|
15044
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
15045
|
-
}
|
|
15046
|
-
const rewindN = parseInt(rewindSub, 10);
|
|
15047
|
-
if (isNaN(rewindN) || rewindN < 1 || rewindN > session.messages.length) {
|
|
15048
|
-
ctx.send({ type: "error", message: `Invalid message number: ${rewindSub}. Range: 1-${session.messages.length}` });
|
|
15049
|
-
}
|
|
15050
|
-
const { fileCheckpoints: fc } = await import("./file-checkpoint-CGH6OJVI.js");
|
|
15051
|
-
const rewindRemoved = session.messages.length - rewindN;
|
|
15052
|
-
const rewindResult = fc.restoreToMessageIndex(rewindN);
|
|
15053
|
-
session.messages = session.messages.slice(0, rewindN);
|
|
15054
|
-
session.checkpoints = session.checkpoints.filter((c) => c.messageIndex <= rewindN);
|
|
15055
|
-
session.updated = /* @__PURE__ */ new Date();
|
|
15056
|
-
ctx.sessions.save();
|
|
15057
|
-
const rewindLines = [`\u2713 Rewound to message ${rewindN}`, ` Messages removed: ${rewindRemoved}`];
|
|
15058
|
-
if (rewindResult.restored > 0 || rewindResult.deleted > 0) {
|
|
15059
|
-
rewindLines.push(` Files restored: ${rewindResult.restored}, deleted: ${rewindResult.deleted}`);
|
|
15060
|
-
for (const f of rewindResult.files) rewindLines.push(` ${f}`);
|
|
15061
|
-
} else {
|
|
15062
|
-
rewindLines.push(" No file changes to revert.");
|
|
15178
|
+
|
|
15179
|
+
// src/web/commands/_shared.ts
|
|
15180
|
+
function lastAssistantText(messages) {
|
|
15181
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
15182
|
+
const m = messages[i];
|
|
15183
|
+
if (!m || m.role !== "assistant") continue;
|
|
15184
|
+
const c = m.content;
|
|
15185
|
+
if (typeof c === "string") return c;
|
|
15186
|
+
if (Array.isArray(c)) {
|
|
15187
|
+
const text = c.filter((p) => p && p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
|
|
15188
|
+
if (text) return text;
|
|
15063
15189
|
}
|
|
15064
|
-
|
|
15065
|
-
ctx.sendSessionMessages();
|
|
15066
|
-
ctx.sendStatus();
|
|
15190
|
+
return void 0;
|
|
15067
15191
|
}
|
|
15192
|
+
return void 0;
|
|
15068
15193
|
}
|
|
15069
15194
|
|
|
15070
|
-
// src/web/commands/
|
|
15071
|
-
|
|
15072
|
-
|
|
15073
|
-
|
|
15074
|
-
ctx.
|
|
15195
|
+
// src/web/commands/session-commands.ts
|
|
15196
|
+
async function handleClear(_args, ctx) {
|
|
15197
|
+
ctx.saveIfNeeded();
|
|
15198
|
+
ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
|
|
15199
|
+
ctx.resetWebSessionUsage();
|
|
15200
|
+
ctx.send({ type: "info", message: "Conversation cleared." });
|
|
15201
|
+
ctx.sendStatus();
|
|
15202
|
+
ctx.sendSessionList();
|
|
15075
15203
|
}
|
|
15076
|
-
async function
|
|
15077
|
-
|
|
15078
|
-
const configDir = ctx.config.getConfigDir();
|
|
15079
|
-
const pluginsDir = join22(configDir, PLUGINS_DIR_NAME);
|
|
15080
|
-
const pluginTools = ctx.toolRegistry.listPluginTools();
|
|
15081
|
-
const pluginPackages = listInstalledPlugins(configDir);
|
|
15082
|
-
const lines = [`\u{1F50C} **Plugins:**`, `Dir: ${pluginsDir}`, ""];
|
|
15083
|
-
if (pluginTools.length === 0 && pluginPackages.length === 0) {
|
|
15084
|
-
lines.push("No plugins loaded.");
|
|
15085
|
-
lines.push("Use /plugin install <path>, /plugin trust <name>, then /plugin enable <name>.");
|
|
15086
|
-
} else {
|
|
15087
|
-
if (pluginPackages.length > 0) {
|
|
15088
|
-
lines.push(`Package plugins (${pluginPackages.length}):`);
|
|
15089
|
-
for (const p of pluginPackages) lines.push(` \u2022 ${p.name}@${p.manifest.version} \u2014 ${p.enabled ? "enabled" : "disabled"} / ${p.trusted ? "trusted" : "untrusted"}`);
|
|
15090
|
-
lines.push("");
|
|
15091
|
-
}
|
|
15092
|
-
if (pluginTools.length > 0) {
|
|
15093
|
-
lines.push(`Legacy JS plugin tools (${pluginTools.length}):`);
|
|
15094
|
-
for (const t of pluginTools) lines.push(` \u2022 ${t.definition.name} \u2014 ${t.definition.description}`);
|
|
15095
|
-
}
|
|
15096
|
-
}
|
|
15097
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
15098
|
-
}
|
|
15204
|
+
async function handleCompact(args, ctx) {
|
|
15205
|
+
await ctx.compactSession(args.join(" ") || void 0);
|
|
15099
15206
|
}
|
|
15100
|
-
async function
|
|
15101
|
-
|
|
15102
|
-
|
|
15103
|
-
|
|
15104
|
-
|
|
15105
|
-
|
|
15106
|
-
|
|
15107
|
-
|
|
15108
|
-
|
|
15109
|
-
|
|
15110
|
-
|
|
15111
|
-
|
|
15112
|
-
|
|
15113
|
-
|
|
15114
|
-
|
|
15115
|
-
|
|
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) });
|
|
15138
|
-
}
|
|
15139
|
-
}
|
|
15207
|
+
async function handleStatus(_args, ctx) {
|
|
15208
|
+
const session = ctx.sessions.current;
|
|
15209
|
+
const cacheRead = ctx.sessionTokenUsage.cacheReadTokens;
|
|
15210
|
+
const cacheCreate = ctx.sessionTokenUsage.cacheCreationTokens;
|
|
15211
|
+
const cost = computeCost(ctx.currentProvider, ctx.currentModel, ctx.sessionTokenUsage);
|
|
15212
|
+
const cacheLine = cacheRead > 0 || cacheCreate > 0 ? `
|
|
15213
|
+
Cache: write=${cacheCreate} read=${cacheRead}` : "";
|
|
15214
|
+
const costLine = cost != null ? `
|
|
15215
|
+
Cost: ${formatCost(cost)}` : "";
|
|
15216
|
+
ctx.send({
|
|
15217
|
+
type: "info",
|
|
15218
|
+
message: `Provider: ${ctx.currentProvider}
|
|
15219
|
+
Model: ${ctx.currentModel}
|
|
15220
|
+
Session: ${session?.id ?? "none"} (${session?.messages.length ?? 0} messages)
|
|
15221
|
+
Tokens: in=${ctx.sessionTokenUsage.inputTokens} out=${ctx.sessionTokenUsage.outputTokens}${cacheLine}${costLine}`
|
|
15222
|
+
});
|
|
15140
15223
|
}
|
|
15141
|
-
async function
|
|
15142
|
-
|
|
15143
|
-
|
|
15144
|
-
|
|
15224
|
+
async function handleSession(args, ctx) {
|
|
15225
|
+
const sub = args[0];
|
|
15226
|
+
if (sub === "new") {
|
|
15227
|
+
ctx.saveIfNeeded();
|
|
15228
|
+
const created = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
|
|
15229
|
+
ctx.unsavedSessions.set(created.id, created);
|
|
15230
|
+
ctx.resetWebSessionUsage();
|
|
15231
|
+
ctx.send({ type: "info", message: "New session created." });
|
|
15232
|
+
ctx.sendStatus();
|
|
15233
|
+
ctx.sendSessionList();
|
|
15234
|
+
return;
|
|
15235
|
+
}
|
|
15236
|
+
if (sub === "load" && args[1]) {
|
|
15237
|
+
const targetId = args[1];
|
|
15238
|
+
ctx.saveIfNeeded();
|
|
15239
|
+
const cachedExact = ctx.unsavedSessions.get(targetId);
|
|
15240
|
+
const cached = cachedExact ?? [...ctx.unsavedSessions.values()].find((s) => s.id.startsWith(targetId));
|
|
15241
|
+
if (cached) {
|
|
15242
|
+
ctx.sessions.setCurrent(cached);
|
|
15243
|
+
ctx.resetWebSessionUsage();
|
|
15244
|
+
ctx.send({
|
|
15245
|
+
type: "info",
|
|
15246
|
+
message: `Loaded session: ${cached.id.slice(0, 8)} "${cached.title ?? ""}" (${cached.messages.length} messages)`
|
|
15247
|
+
});
|
|
15248
|
+
ctx.sendSessionMessages();
|
|
15249
|
+
ctx.sendStatus();
|
|
15250
|
+
ctx.sendSessionList();
|
|
15251
|
+
return;
|
|
15145
15252
|
}
|
|
15146
|
-
const
|
|
15147
|
-
|
|
15148
|
-
|
|
15149
|
-
|
|
15150
|
-
|
|
15151
|
-
|
|
15152
|
-
|
|
15153
|
-
|
|
15253
|
+
const list = ctx.sessions.listSessions();
|
|
15254
|
+
const found = list.find((s) => s.id.startsWith(targetId));
|
|
15255
|
+
if (found) {
|
|
15256
|
+
ctx.sessions.loadSession(found.id);
|
|
15257
|
+
ctx.resetWebSessionUsage();
|
|
15258
|
+
ctx.send({ type: "info", message: `Loaded session: ${found.id.slice(0, 8)} "${found.title ?? ""}" (${found.messageCount} messages)` });
|
|
15259
|
+
const loadedSession = ctx.sessions.current;
|
|
15260
|
+
if (loadedSession && loadedSession.messages.length > 0) {
|
|
15261
|
+
const lastMsg = loadedSession.messages[loadedSession.messages.length - 1];
|
|
15262
|
+
const isIncomplete = lastMsg.role === "tool" || lastMsg.role === "assistant" && lastMsg.toolCalls && lastMsg.toolCalls.length > 0;
|
|
15263
|
+
if (isIncomplete) {
|
|
15264
|
+
ctx.send({
|
|
15265
|
+
type: "info",
|
|
15266
|
+
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.'
|
|
15267
|
+
});
|
|
15268
|
+
}
|
|
15154
15269
|
}
|
|
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}`);
|
|
15270
|
+
ctx.sendSessionMessages();
|
|
15271
|
+
ctx.sendStatus();
|
|
15272
|
+
ctx.sendSessionList();
|
|
15273
|
+
return;
|
|
15165
15274
|
}
|
|
15166
|
-
|
|
15167
|
-
|
|
15168
|
-
ctx.
|
|
15275
|
+
const recreated = ctx.sessions.createSession(ctx.currentProvider, ctx.currentModel);
|
|
15276
|
+
ctx.unsavedSessions.set(recreated.id, recreated);
|
|
15277
|
+
ctx.resetWebSessionUsage();
|
|
15278
|
+
ctx.send({
|
|
15279
|
+
type: "info",
|
|
15280
|
+
message: `Previous session (${targetId.slice(0, 8)}) is no longer available \u2014 started a new one.`
|
|
15281
|
+
});
|
|
15282
|
+
ctx.sendStatus();
|
|
15283
|
+
ctx.sendSessionList();
|
|
15284
|
+
return;
|
|
15169
15285
|
}
|
|
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")}` });
|
|
15286
|
+
if (sub === "list") {
|
|
15287
|
+
ctx.sendSessionList();
|
|
15288
|
+
return;
|
|
15289
|
+
}
|
|
15290
|
+
if (sub === "delete" && args[1]) {
|
|
15291
|
+
const targetId = args[1];
|
|
15292
|
+
const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
|
|
15293
|
+
if (cachedKey) {
|
|
15294
|
+
ctx.unsavedSessions.delete(cachedKey);
|
|
15295
|
+
if (ctx.sessions.current?.id === cachedKey) {
|
|
15296
|
+
ctx.sessions.clearCurrent();
|
|
15190
15297
|
}
|
|
15191
|
-
|
|
15192
|
-
ctx.
|
|
15193
|
-
|
|
15194
|
-
|
|
15195
|
-
|
|
15196
|
-
|
|
15197
|
-
|
|
15198
|
-
ctx.
|
|
15199
|
-
ctx.
|
|
15298
|
+
ctx.send({ type: "info", message: `Deleted session: ${cachedKey.slice(0, 8)}` });
|
|
15299
|
+
ctx.sendSessionList();
|
|
15300
|
+
return;
|
|
15301
|
+
}
|
|
15302
|
+
const list = ctx.sessions.listSessions();
|
|
15303
|
+
const found = list.find((s) => s.id.startsWith(targetId));
|
|
15304
|
+
if (found) {
|
|
15305
|
+
ctx.sessions.deleteSession(found.id);
|
|
15306
|
+
ctx.send({ type: "info", message: `Deleted session: ${found.id.slice(0, 8)}` });
|
|
15307
|
+
ctx.sendSessionList();
|
|
15200
15308
|
} 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
|
-
}
|
|
15309
|
+
ctx.send({ type: "error", message: `Session not found: ${targetId}` });
|
|
15211
15310
|
}
|
|
15311
|
+
return;
|
|
15212
15312
|
}
|
|
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.` });
|
|
15313
|
+
if (sub === "batch-delete") {
|
|
15314
|
+
const ids = args.slice(1).filter(Boolean);
|
|
15315
|
+
if (ids.length === 0) {
|
|
15316
|
+
ctx.send({ type: "error", message: "No session IDs provided." });
|
|
15317
|
+
return;
|
|
15221
15318
|
}
|
|
15222
|
-
|
|
15223
|
-
|
|
15224
|
-
|
|
15225
|
-
|
|
15226
|
-
|
|
15227
|
-
|
|
15228
|
-
|
|
15229
|
-
|
|
15230
|
-
|
|
15231
|
-
|
|
15232
|
-
|
|
15233
|
-
ctx.
|
|
15319
|
+
const list = ctx.sessions.listSessions();
|
|
15320
|
+
let deleted = 0;
|
|
15321
|
+
for (const targetId of ids) {
|
|
15322
|
+
const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
|
|
15323
|
+
if (cachedKey) {
|
|
15324
|
+
ctx.unsavedSessions.delete(cachedKey);
|
|
15325
|
+
deleted++;
|
|
15326
|
+
continue;
|
|
15327
|
+
}
|
|
15328
|
+
const found = list.find((s) => s.id.startsWith(targetId));
|
|
15329
|
+
if (found) {
|
|
15330
|
+
ctx.sessions.deleteSession(found.id);
|
|
15331
|
+
deleted++;
|
|
15234
15332
|
}
|
|
15235
|
-
} catch {
|
|
15236
|
-
ctx.send({ type: "info", message: `Cannot read: ${commandsDir}` });
|
|
15237
15333
|
}
|
|
15334
|
+
ctx.send({ type: "info", message: `Deleted ${deleted} session(s).` });
|
|
15335
|
+
ctx.sendSessionList();
|
|
15336
|
+
return;
|
|
15238
15337
|
}
|
|
15338
|
+
if (sub === "rename" && args[1] && args.length >= 3) {
|
|
15339
|
+
const targetId = args[1];
|
|
15340
|
+
const newTitle = args.slice(2).join(" ").trim();
|
|
15341
|
+
if (!newTitle) {
|
|
15342
|
+
ctx.send({ type: "error", message: "Title cannot be empty." });
|
|
15343
|
+
return;
|
|
15344
|
+
}
|
|
15345
|
+
const cachedKey = ctx.unsavedSessions.has(targetId) ? targetId : [...ctx.unsavedSessions.keys()].find((k) => k.startsWith(targetId));
|
|
15346
|
+
if (cachedKey) {
|
|
15347
|
+
const session = ctx.unsavedSessions.get(cachedKey);
|
|
15348
|
+
session.title = newTitle;
|
|
15349
|
+
ctx.send({ type: "info", message: `Renamed session: "${newTitle}"` });
|
|
15350
|
+
ctx.sendSessionList();
|
|
15351
|
+
return;
|
|
15352
|
+
}
|
|
15353
|
+
const list = ctx.sessions.listSessions();
|
|
15354
|
+
const found = list.find((s) => s.id.startsWith(targetId));
|
|
15355
|
+
if (found) {
|
|
15356
|
+
const session = ctx.sessions.loadSession(found.id);
|
|
15357
|
+
session.title = newTitle;
|
|
15358
|
+
ctx.sessions.save();
|
|
15359
|
+
ctx.send({ type: "info", message: `Renamed session: "${newTitle}"` });
|
|
15360
|
+
ctx.sendSessionList();
|
|
15361
|
+
} else {
|
|
15362
|
+
ctx.send({ type: "error", message: `Session not found: ${targetId}` });
|
|
15363
|
+
}
|
|
15364
|
+
return;
|
|
15365
|
+
}
|
|
15366
|
+
ctx.send({ type: "info", message: "Usage: /session new | list | load <id> | delete <id> | rename <id> <title>" });
|
|
15239
15367
|
}
|
|
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();
|
|
15368
|
+
async function handleExport(args, ctx) {
|
|
15369
|
+
const format = args[0] === "json" ? "json" : "md";
|
|
15370
|
+
ctx.exportConversation(format);
|
|
15371
|
+
}
|
|
15372
|
+
async function handleCost(_args, ctx) {
|
|
15373
|
+
const u = ctx.sessionTokenUsage;
|
|
15374
|
+
const total = u.inputTokens + u.outputTokens + u.cacheCreationTokens + u.cacheReadTokens;
|
|
15375
|
+
const cost = computeCost(ctx.currentProvider, ctx.currentModel, u);
|
|
15376
|
+
let savings = null;
|
|
15377
|
+
if (cost != null && u.cacheReadTokens > 0) {
|
|
15378
|
+
const withoutCache = computeCost(ctx.currentProvider, ctx.currentModel, {
|
|
15379
|
+
inputTokens: u.inputTokens + u.cacheReadTokens,
|
|
15380
|
+
outputTokens: u.outputTokens,
|
|
15381
|
+
cacheCreationTokens: u.cacheCreationTokens,
|
|
15382
|
+
cacheReadTokens: 0
|
|
15383
|
+
});
|
|
15384
|
+
if (withoutCache != null) savings = withoutCache - cost;
|
|
15385
|
+
}
|
|
15386
|
+
const lines = [
|
|
15387
|
+
"\u{1F4B0} Session Cost & Token Usage",
|
|
15388
|
+
` Provider : ${ctx.currentProvider}`,
|
|
15389
|
+
` Model : ${ctx.currentModel}`,
|
|
15390
|
+
` Input (uncached): ${u.inputTokens.toLocaleString()}`,
|
|
15391
|
+
` Output : ${u.outputTokens.toLocaleString()}`
|
|
15392
|
+
];
|
|
15393
|
+
if (u.cacheCreationTokens > 0) lines.push(` Cache write : ${u.cacheCreationTokens.toLocaleString()}`);
|
|
15394
|
+
if (u.cacheReadTokens > 0) {
|
|
15395
|
+
const pct = Math.round(u.cacheReadTokens / (u.cacheReadTokens + u.inputTokens) * 100);
|
|
15396
|
+
lines.push(` Cache read : ${u.cacheReadTokens.toLocaleString()} (${pct}% hit rate)`);
|
|
15397
|
+
}
|
|
15398
|
+
lines.push(` Total tokens : ${total.toLocaleString()}`);
|
|
15399
|
+
if (cost != null) {
|
|
15400
|
+
lines.push(` Cost : ${formatCost(cost)}`);
|
|
15401
|
+
if (savings != null && savings > 0) {
|
|
15402
|
+
lines.push(` Cache savings : -${formatCost(savings)} (vs no cache)`);
|
|
15403
|
+
}
|
|
15404
|
+
} else {
|
|
15405
|
+
lines.push(` Cost : \u2014 (pricing unknown for this model)`);
|
|
15406
|
+
}
|
|
15407
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
15312
15408
|
}
|
|
15313
|
-
|
|
15314
|
-
|
|
15315
|
-
|
|
15316
|
-
|
|
15317
|
-
|
|
15318
|
-
|
|
15319
|
-
|
|
15409
|
+
async function handleCheckpoint(args, ctx) {
|
|
15410
|
+
const session = ctx.sessions.current;
|
|
15411
|
+
if (!session) {
|
|
15412
|
+
ctx.send({ type: "error", message: "No active session." });
|
|
15413
|
+
return;
|
|
15414
|
+
}
|
|
15415
|
+
const sub = args[0]?.toLowerCase();
|
|
15416
|
+
if (!sub || sub === "list") {
|
|
15417
|
+
const cps = session.checkpoints;
|
|
15418
|
+
if (cps.length === 0) {
|
|
15419
|
+
ctx.send({ type: "info", message: "No checkpoints. Use /checkpoint save <name> to create one." });
|
|
15420
|
+
} else {
|
|
15421
|
+
const lines = [`\u{1F4CC} Checkpoints (${cps.length}):`, ""];
|
|
15422
|
+
for (const cp of cps) {
|
|
15423
|
+
const ts = cp.timestamp.toLocaleString();
|
|
15424
|
+
lines.push(` ${cp.name.padEnd(20)} msg #${cp.messageIndex} ${ts}`);
|
|
15425
|
+
}
|
|
15426
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
15320
15427
|
}
|
|
15321
|
-
|
|
15322
|
-
|
|
15323
|
-
|
|
15324
|
-
|
|
15325
|
-
|
|
15326
|
-
|
|
15327
|
-
|
|
15428
|
+
return;
|
|
15429
|
+
}
|
|
15430
|
+
const cpName = args.slice(1).join(" ").trim();
|
|
15431
|
+
if (sub === "save") {
|
|
15432
|
+
if (!cpName) {
|
|
15433
|
+
ctx.send({ type: "error", message: "Usage: /checkpoint save <name>" });
|
|
15434
|
+
return;
|
|
15328
15435
|
}
|
|
15329
|
-
|
|
15330
|
-
|
|
15436
|
+
session.createCheckpoint(cpName);
|
|
15437
|
+
ctx.send({ type: "info", message: `\u2713 Checkpoint "${cpName}" saved at message #${session.messages.length}` });
|
|
15438
|
+
} else if (sub === "restore") {
|
|
15439
|
+
if (!cpName) {
|
|
15440
|
+
ctx.send({ type: "error", message: "Usage: /checkpoint restore <name>" });
|
|
15441
|
+
return;
|
|
15331
15442
|
}
|
|
15332
|
-
const
|
|
15333
|
-
|
|
15334
|
-
|
|
15335
|
-
|
|
15336
|
-
|
|
15337
|
-
|
|
15338
|
-
|
|
15443
|
+
const ok = session.restoreCheckpoint(cpName);
|
|
15444
|
+
if (ok) {
|
|
15445
|
+
ctx.send({ type: "info", message: `\u2713 Restored to checkpoint "${cpName}" (${session.messages.length} messages)` });
|
|
15446
|
+
ctx.sendSessionMessages();
|
|
15447
|
+
ctx.sendStatus();
|
|
15448
|
+
} else {
|
|
15449
|
+
ctx.send({ type: "error", message: `Checkpoint "${cpName}" not found.` });
|
|
15339
15450
|
}
|
|
15340
|
-
|
|
15341
|
-
|
|
15342
|
-
|
|
15343
|
-
|
|
15344
|
-
|
|
15345
|
-
|
|
15346
|
-
|
|
15347
|
-
ctx.send({ type: "
|
|
15451
|
+
} else if (sub === "delete") {
|
|
15452
|
+
if (!cpName) {
|
|
15453
|
+
ctx.send({ type: "error", message: "Usage: /checkpoint delete <name>" });
|
|
15454
|
+
return;
|
|
15455
|
+
}
|
|
15456
|
+
const ok = session.deleteCheckpoint(cpName);
|
|
15457
|
+
if (ok) {
|
|
15458
|
+
ctx.send({ type: "info", message: `\u2713 Deleted checkpoint "${cpName}"` });
|
|
15459
|
+
} else {
|
|
15460
|
+
ctx.send({ type: "error", message: `Checkpoint "${cpName}" not found.` });
|
|
15348
15461
|
}
|
|
15462
|
+
} else {
|
|
15463
|
+
ctx.send({ type: "error", message: `Unknown subcommand: ${sub}. Use save/restore/list/delete.` });
|
|
15349
15464
|
}
|
|
15350
15465
|
}
|
|
15351
|
-
async function
|
|
15352
|
-
|
|
15353
|
-
|
|
15354
|
-
|
|
15355
|
-
|
|
15466
|
+
async function handleBranch(args, ctx) {
|
|
15467
|
+
const session = ctx.sessions.current;
|
|
15468
|
+
if (!session) {
|
|
15469
|
+
ctx.send({ type: "error", message: "No active session." });
|
|
15470
|
+
return;
|
|
15471
|
+
}
|
|
15472
|
+
const sub = args[0]?.toLowerCase();
|
|
15473
|
+
const resolve14 = (ref) => {
|
|
15474
|
+
const r = session.resolveBranchRef(ref);
|
|
15475
|
+
if (r.ok) return r.id;
|
|
15476
|
+
if (r.reason === "ambiguous") {
|
|
15477
|
+
const list = r.matches.map((m) => `${m.id} (${m.title})`).join(", ");
|
|
15478
|
+
ctx.send({ type: "error", message: `Ambiguous branch reference "${ref}". Matches: ${list}. Use the 6-char id.` });
|
|
15479
|
+
} else {
|
|
15480
|
+
ctx.send({ type: "error", message: `Branch "${ref}" not found. Use /branch list to see all branches.` });
|
|
15356
15481
|
}
|
|
15357
|
-
|
|
15358
|
-
|
|
15359
|
-
|
|
15360
|
-
|
|
15361
|
-
}
|
|
15362
|
-
|
|
15482
|
+
return null;
|
|
15483
|
+
};
|
|
15484
|
+
if (!sub || sub === "list") {
|
|
15485
|
+
const branches = session.listBranches();
|
|
15486
|
+
const lines = [`\u{1F33F} Branches (${branches.length}):`, ""];
|
|
15487
|
+
for (const b of branches) {
|
|
15488
|
+
const marker = b.id === session.activeBranchId ? "\u25CF " : " ";
|
|
15489
|
+
const count = b.id === session.activeBranchId ? session.messages.length : session.getBranchMessages(b.id)?.length ?? 0;
|
|
15490
|
+
const parent = b.parentBranchId ? ` \u2190 ${b.parentBranchId}@${b.parentMessageIndex}` : "";
|
|
15491
|
+
lines.push(` ${marker}${b.id.padEnd(10)} ${b.title.padEnd(20)} (${count} msgs)${parent}`);
|
|
15363
15492
|
}
|
|
15364
|
-
|
|
15365
|
-
|
|
15493
|
+
lines.push("");
|
|
15494
|
+
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>");
|
|
15495
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
15496
|
+
return;
|
|
15497
|
+
}
|
|
15498
|
+
if (sub === "new") {
|
|
15499
|
+
const idxArg = args[1];
|
|
15500
|
+
if (!idxArg) {
|
|
15501
|
+
ctx.send({ type: "error", message: "Usage: /branch new <msgIndex> [title]" });
|
|
15502
|
+
return;
|
|
15366
15503
|
}
|
|
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;
|
|
15504
|
+
const fromIndex = parseInt(idxArg, 10);
|
|
15505
|
+
if (isNaN(fromIndex) || fromIndex < 0 || fromIndex > session.messages.length) {
|
|
15506
|
+
ctx.send({ type: "error", message: `Invalid msgIndex: ${idxArg}. Range: 0-${session.messages.length}` });
|
|
15507
|
+
return;
|
|
15374
15508
|
}
|
|
15375
|
-
const
|
|
15376
|
-
ctx.send({ type: "info", message: "\u{1F512} Scanning for security vulnerabilities..." });
|
|
15509
|
+
const title = args.slice(2).join(" ").trim() || void 0;
|
|
15377
15510
|
try {
|
|
15378
|
-
const
|
|
15379
|
-
|
|
15380
|
-
ctx.send({ type: "info", message:
|
|
15511
|
+
const newId = session.createBranch(fromIndex, title);
|
|
15512
|
+
await ctx.sessions.save();
|
|
15513
|
+
ctx.send({ type: "info", message: `\u2713 Created branch "${newId}" from message #${fromIndex}. Now active (${session.messages.length} messages).` });
|
|
15514
|
+
ctx.sendSessionMessages();
|
|
15515
|
+
ctx.sendStatus();
|
|
15381
15516
|
} catch (err) {
|
|
15382
|
-
ctx.send({ type: "error", message:
|
|
15517
|
+
ctx.send({ type: "error", message: err.message });
|
|
15383
15518
|
}
|
|
15519
|
+
return;
|
|
15384
15520
|
}
|
|
15385
|
-
|
|
15386
|
-
|
|
15387
|
-
|
|
15388
|
-
|
|
15389
|
-
|
|
15390
|
-
|
|
15391
|
-
|
|
15392
|
-
|
|
15393
|
-
|
|
15394
|
-
|
|
15395
|
-
|
|
15396
|
-
|
|
15397
|
-
|
|
15398
|
-
ctx.
|
|
15399
|
-
|
|
15400
|
-
|
|
15521
|
+
if (sub === "switch") {
|
|
15522
|
+
const ref = args[1];
|
|
15523
|
+
if (!ref) {
|
|
15524
|
+
ctx.send({ type: "error", message: "Usage: /branch switch <id|title>" });
|
|
15525
|
+
return;
|
|
15526
|
+
}
|
|
15527
|
+
const id = resolve14(ref);
|
|
15528
|
+
if (!id) return;
|
|
15529
|
+
const ok = session.switchBranch(id);
|
|
15530
|
+
if (ok) {
|
|
15531
|
+
await ctx.sessions.save();
|
|
15532
|
+
const b = session.getActiveBranch();
|
|
15533
|
+
ctx.send({ type: "info", message: `\u2713 Switched to branch "${b.id}" \u2014 ${b.title} (${session.messages.length} messages)` });
|
|
15534
|
+
ctx.sendSessionMessages();
|
|
15535
|
+
ctx.sendStatus();
|
|
15536
|
+
} else {
|
|
15537
|
+
ctx.send({ type: "error", message: `Cannot switch to "${id}" (already active).` });
|
|
15401
15538
|
}
|
|
15539
|
+
return;
|
|
15402
15540
|
}
|
|
15403
|
-
|
|
15404
|
-
|
|
15405
|
-
|
|
15406
|
-
|
|
15407
|
-
|
|
15408
|
-
ctx.send({ type: "info", message: "No file modifications in this session." });
|
|
15541
|
+
if (sub === "delete") {
|
|
15542
|
+
const ref = args[1];
|
|
15543
|
+
if (!ref) {
|
|
15544
|
+
ctx.send({ type: "error", message: "Usage: /branch delete <id|title>" });
|
|
15545
|
+
return;
|
|
15409
15546
|
}
|
|
15410
|
-
const
|
|
15411
|
-
|
|
15412
|
-
|
|
15413
|
-
|
|
15414
|
-
|
|
15547
|
+
const id = resolve14(ref);
|
|
15548
|
+
if (!id) return;
|
|
15549
|
+
const ok = session.deleteBranch(id);
|
|
15550
|
+
if (ok) {
|
|
15551
|
+
await ctx.sessions.save();
|
|
15552
|
+
ctx.send({ type: "info", message: `\u2713 Deleted branch "${id}"` });
|
|
15553
|
+
ctx.sendStatus();
|
|
15554
|
+
} else {
|
|
15555
|
+
ctx.send({ type: "error", message: `Cannot delete "${id}" (active, or last remaining branch).` });
|
|
15415
15556
|
}
|
|
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
|
-
|
|
15557
|
+
return;
|
|
15558
|
+
}
|
|
15559
|
+
if (sub === "rename") {
|
|
15560
|
+
const ref = args[1];
|
|
15561
|
+
const title = args.slice(2).join(" ").trim();
|
|
15562
|
+
if (!ref || !title) {
|
|
15563
|
+
ctx.send({ type: "error", message: "Usage: /branch rename <id|title> <new title>" });
|
|
15564
|
+
return;
|
|
15565
|
+
}
|
|
15566
|
+
const id = resolve14(ref);
|
|
15567
|
+
if (!id) return;
|
|
15568
|
+
const ok = session.renameBranch(id, title);
|
|
15569
|
+
if (ok) {
|
|
15570
|
+
await ctx.sessions.save();
|
|
15571
|
+
ctx.send({ type: "info", message: `\u2713 Renamed branch "${id}" \u2192 "${title}"` });
|
|
15572
|
+
ctx.sendStatus();
|
|
15573
|
+
} else {
|
|
15574
|
+
ctx.send({ type: "error", message: `Branch "${id}" not found.` });
|
|
15575
|
+
}
|
|
15576
|
+
return;
|
|
15577
|
+
}
|
|
15578
|
+
if (sub === "diff") {
|
|
15579
|
+
const ref = args[1];
|
|
15580
|
+
if (!ref) {
|
|
15581
|
+
ctx.send({ type: "error", message: "Usage: /branch diff <id|title>" });
|
|
15582
|
+
return;
|
|
15583
|
+
}
|
|
15584
|
+
const id = resolve14(ref);
|
|
15585
|
+
if (!id) return;
|
|
15586
|
+
const d = session.diffBranches(id);
|
|
15587
|
+
if (!d) {
|
|
15588
|
+
ctx.send({ type: "error", message: `Branch "${id}" not found.` });
|
|
15589
|
+
return;
|
|
15590
|
+
}
|
|
15591
|
+
const preview = (m) => {
|
|
15592
|
+
const text = typeof m.content === "string" ? m.content : "[complex content]";
|
|
15593
|
+
return text.replace(/\s+/g, " ").trim().slice(0, 70);
|
|
15594
|
+
};
|
|
15595
|
+
const lines = [
|
|
15596
|
+
`\u{1F500} diff ${id} (source) \u2194 ${session.activeBranchId} (active)`,
|
|
15597
|
+
`Common prefix: ${d.commonPrefix} message(s)`
|
|
15598
|
+
];
|
|
15599
|
+
if (d.sourceOnly.length === 0 && d.targetOnly.length === 0) {
|
|
15600
|
+
lines.push("Branches are identical.");
|
|
15601
|
+
} else {
|
|
15602
|
+
if (d.sourceOnly.length > 0) {
|
|
15603
|
+
lines.push("", `Only in ${id} (${d.sourceOnly.length}):`);
|
|
15604
|
+
d.sourceOnly.forEach(
|
|
15605
|
+
(m, i) => lines.push(` [${d.commonPrefix + i}] ${m.role}: ${preview(m)}`)
|
|
15606
|
+
);
|
|
15450
15607
|
}
|
|
15608
|
+
if (d.targetOnly.length > 0) {
|
|
15609
|
+
lines.push("", `Only in ${session.activeBranchId} (active) (${d.targetOnly.length}):`);
|
|
15610
|
+
d.targetOnly.forEach(
|
|
15611
|
+
(m, i) => lines.push(` [${d.commonPrefix + i}] ${m.role}: ${preview(m)}`)
|
|
15612
|
+
);
|
|
15613
|
+
}
|
|
15614
|
+
lines.push("", `Use /branch cherry-pick ${id} <index> to copy a message into the active branch.`);
|
|
15451
15615
|
}
|
|
15452
|
-
|
|
15453
|
-
|
|
15454
|
-
ctx.send({ type: "info", message: diffLines2.join("\n") });
|
|
15616
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
15617
|
+
return;
|
|
15455
15618
|
}
|
|
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.` });
|
|
15619
|
+
if (sub === "cherry-pick") {
|
|
15620
|
+
const ref = args[1];
|
|
15621
|
+
const idxStr = args[2];
|
|
15622
|
+
if (!ref || !idxStr) {
|
|
15623
|
+
ctx.send({ type: "error", message: "Usage: /branch cherry-pick <source-id|title> <msg-index>" });
|
|
15624
|
+
return;
|
|
15465
15625
|
}
|
|
15466
|
-
|
|
15467
|
-
|
|
15468
|
-
|
|
15469
|
-
|
|
15470
|
-
|
|
15471
|
-
|
|
15472
|
-
|
|
15473
|
-
|
|
15474
|
-
|
|
15475
|
-
ctx.send({ type: "error", message: `
|
|
15626
|
+
const id = resolve14(ref);
|
|
15627
|
+
if (!id) return;
|
|
15628
|
+
const idx = parseInt(idxStr, 10);
|
|
15629
|
+
if (Number.isNaN(idx)) {
|
|
15630
|
+
ctx.send({ type: "error", message: `Invalid message index: "${idxStr}"` });
|
|
15631
|
+
return;
|
|
15632
|
+
}
|
|
15633
|
+
const picked = session.cherryPickMessage(id, idx);
|
|
15634
|
+
if (!picked) {
|
|
15635
|
+
ctx.send({ type: "error", message: `Cherry-pick failed \u2014 index ${idx} out of range for branch "${id}".` });
|
|
15636
|
+
return;
|
|
15476
15637
|
}
|
|
15638
|
+
await ctx.sessions.save();
|
|
15639
|
+
const pickPreview = typeof picked.content === "string" ? picked.content.replace(/\s+/g, " ").trim().slice(0, 60) : "[complex content]";
|
|
15640
|
+
ctx.send({ type: "info", message: `\u2713 Cherry-picked ${picked.role} message from "${id}"[${idx}] \u2192 active branch (${session.messages.length} msgs): ${pickPreview}` });
|
|
15641
|
+
ctx.sendSessionMessages();
|
|
15642
|
+
ctx.sendStatus();
|
|
15643
|
+
return;
|
|
15477
15644
|
}
|
|
15645
|
+
ctx.send({ type: "error", message: `Unknown subcommand: ${sub}. Use list/new/switch/delete/rename/diff/cherry-pick.` });
|
|
15478
15646
|
}
|
|
15479
|
-
async function
|
|
15480
|
-
|
|
15481
|
-
|
|
15482
|
-
|
|
15483
|
-
|
|
15647
|
+
async function handleFork(args, ctx) {
|
|
15648
|
+
const session = ctx.sessions.current;
|
|
15649
|
+
if (!session) {
|
|
15650
|
+
ctx.send({ type: "info", message: "No active session to fork." });
|
|
15651
|
+
return;
|
|
15652
|
+
}
|
|
15653
|
+
const sub = args.join(" ").trim();
|
|
15654
|
+
let messageCount = session.messages.length;
|
|
15655
|
+
let fromLabel = "current position";
|
|
15656
|
+
if (sub) {
|
|
15657
|
+
const cp = session.checkpoints.find((c) => c.name === sub);
|
|
15658
|
+
if (!cp) {
|
|
15659
|
+
const available = session.checkpoints.map((c) => c.name);
|
|
15660
|
+
ctx.send({ type: "error", message: available.length > 0 ? `Checkpoint "${sub}" not found. Available: ${available.join(", ")}` : `Checkpoint "${sub}" not found. No checkpoints saved.` });
|
|
15661
|
+
return;
|
|
15484
15662
|
}
|
|
15485
|
-
|
|
15486
|
-
|
|
15487
|
-
|
|
15488
|
-
|
|
15489
|
-
|
|
15663
|
+
messageCount = cp.messageIndex;
|
|
15664
|
+
fromLabel = `checkpoint "${cp.name}" (${cp.messageIndex} messages)`;
|
|
15665
|
+
}
|
|
15666
|
+
try {
|
|
15667
|
+
const originalId = session.id.slice(0, 8);
|
|
15668
|
+
const forked = await ctx.sessions.forkSession(messageCount);
|
|
15669
|
+
ctx.send({ type: "info", message: `\u{1F500} Session Forked
|
|
15670
|
+
Original: ${originalId}
|
|
15671
|
+
Forked: ${forked.id.slice(0, 8)} "${forked.title ?? "(untitled)"}"
|
|
15672
|
+
From: ${fromLabel}
|
|
15673
|
+
Messages: ${forked.messages.length} copied` });
|
|
15674
|
+
ctx.sendSessionMessages();
|
|
15675
|
+
ctx.sendStatus();
|
|
15676
|
+
ctx.sendSessionList();
|
|
15677
|
+
} catch (err) {
|
|
15678
|
+
ctx.send({ type: "error", message: `Fork failed: ${err.message}` });
|
|
15490
15679
|
}
|
|
15491
15680
|
}
|
|
15681
|
+
async function handleRewind(args, ctx) {
|
|
15682
|
+
const session = ctx.sessions.current;
|
|
15683
|
+
if (!session || session.messages.length === 0) {
|
|
15684
|
+
ctx.send({ type: "info", message: "No messages to rewind." });
|
|
15685
|
+
return;
|
|
15686
|
+
}
|
|
15687
|
+
const rewindSub = args[0];
|
|
15688
|
+
if (rewindSub === "list" || !rewindSub) {
|
|
15689
|
+
const lines = [`Conversation messages (${session.messages.length} total):
|
|
15690
|
+
`];
|
|
15691
|
+
const cpIndices = (await import("./file-checkpoint-CGH6OJVI.js")).fileCheckpoints.getMessageIndices();
|
|
15692
|
+
for (let i = 0; i < session.messages.length; i++) {
|
|
15693
|
+
const m = session.messages[i];
|
|
15694
|
+
const text = getContentText(m.content).replace(/\n/g, " ").slice(0, 60);
|
|
15695
|
+
const pin = cpIndices.includes(i) ? " \u{1F4CC}" : "";
|
|
15696
|
+
lines.push(` [${i + 1}] ${m.role.padEnd(10)} ${text}${pin}`);
|
|
15697
|
+
}
|
|
15698
|
+
lines.push("", "Usage: /rewind <n> \u2014 rewind to message N", "\u{1F4CC} = file checkpoint");
|
|
15699
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
15700
|
+
return;
|
|
15701
|
+
}
|
|
15702
|
+
const rewindN = parseInt(rewindSub, 10);
|
|
15703
|
+
if (isNaN(rewindN) || rewindN < 1 || rewindN > session.messages.length) {
|
|
15704
|
+
ctx.send({ type: "error", message: `Invalid message number: ${rewindSub}. Range: 1-${session.messages.length}` });
|
|
15705
|
+
return;
|
|
15706
|
+
}
|
|
15707
|
+
const { fileCheckpoints: fc } = await import("./file-checkpoint-CGH6OJVI.js");
|
|
15708
|
+
const rewindRemoved = session.messages.length - rewindN;
|
|
15709
|
+
const rewindResult = fc.restoreToMessageIndex(rewindN);
|
|
15710
|
+
session.messages = session.messages.slice(0, rewindN);
|
|
15711
|
+
session.checkpoints = session.checkpoints.filter((c) => c.messageIndex <= rewindN);
|
|
15712
|
+
session.updated = /* @__PURE__ */ new Date();
|
|
15713
|
+
ctx.sessions.save();
|
|
15714
|
+
const rewindLines = [`\u2713 Rewound to message ${rewindN}`, ` Messages removed: ${rewindRemoved}`];
|
|
15715
|
+
if (rewindResult.restored > 0 || rewindResult.deleted > 0) {
|
|
15716
|
+
rewindLines.push(` Files restored: ${rewindResult.restored}, deleted: ${rewindResult.deleted}`);
|
|
15717
|
+
for (const f of rewindResult.files) rewindLines.push(` ${f}`);
|
|
15718
|
+
} else {
|
|
15719
|
+
rewindLines.push(" No file changes to revert.");
|
|
15720
|
+
}
|
|
15721
|
+
ctx.send({ type: "info", message: rewindLines.join("\n") });
|
|
15722
|
+
ctx.sendSessionMessages();
|
|
15723
|
+
ctx.sendStatus();
|
|
15724
|
+
}
|
|
15492
15725
|
|
|
15493
|
-
// src/web/commands/
|
|
15494
|
-
|
|
15495
|
-
|
|
15496
|
-
|
|
15497
|
-
|
|
15498
|
-
|
|
15499
|
-
|
|
15500
|
-
|
|
15501
|
-
|
|
15502
|
-
|
|
15503
|
-
|
|
15504
|
-
|
|
15505
|
-
|
|
15506
|
-
|
|
15507
|
-
|
|
15508
|
-
}
|
|
15726
|
+
// src/web/commands/tooling-commands.ts
|
|
15727
|
+
async function handleTools(_args, ctx) {
|
|
15728
|
+
ctx.sendToolsList();
|
|
15729
|
+
}
|
|
15730
|
+
async function handlePlugins(_args, ctx) {
|
|
15731
|
+
const configDir = ctx.config.getConfigDir();
|
|
15732
|
+
const pluginsDir = join24(configDir, PLUGINS_DIR_NAME);
|
|
15733
|
+
const pluginTools = ctx.toolRegistry.listPluginTools();
|
|
15734
|
+
const pluginPackages = listInstalledPlugins(configDir);
|
|
15735
|
+
const lines = [`\u{1F50C} **Plugins:**`, `Dir: ${pluginsDir}`, ""];
|
|
15736
|
+
if (pluginTools.length === 0 && pluginPackages.length === 0) {
|
|
15737
|
+
lines.push("No plugins loaded.");
|
|
15738
|
+
lines.push("Use /plugin install <path>, /plugin trust <name>, then /plugin enable <name>.");
|
|
15739
|
+
} else {
|
|
15740
|
+
if (pluginPackages.length > 0) {
|
|
15741
|
+
lines.push(`Package plugins (${pluginPackages.length}):`);
|
|
15742
|
+
for (const p of pluginPackages) lines.push(` \u2022 ${p.name}@${p.manifest.version} \u2014 ${p.enabled ? "enabled" : "disabled"} / ${p.trusted ? "trusted" : "untrusted"}`);
|
|
15743
|
+
lines.push("");
|
|
15509
15744
|
}
|
|
15510
|
-
if (
|
|
15511
|
-
|
|
15512
|
-
|
|
15513
|
-
} else {
|
|
15514
|
-
ctx.activeSystemPrompt = text;
|
|
15515
|
-
ctx.send({ type: "info", message: `\u2713 System prompt set (${text.length} chars).` });
|
|
15745
|
+
if (pluginTools.length > 0) {
|
|
15746
|
+
lines.push(`Legacy JS plugin tools (${pluginTools.length}):`);
|
|
15747
|
+
for (const t of pluginTools) lines.push(` \u2022 ${t.definition.name} \u2014 ${t.definition.description}`);
|
|
15516
15748
|
}
|
|
15517
15749
|
}
|
|
15750
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
15518
15751
|
}
|
|
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");
|
|
15752
|
+
async function handlePlugin(args, ctx) {
|
|
15753
|
+
const configDir = ctx.config.getConfigDir();
|
|
15754
|
+
const sub = args[0] ?? "list";
|
|
15755
|
+
try {
|
|
15756
|
+
if (sub === "list") {
|
|
15757
|
+
const plugins = listInstalledPlugins(configDir);
|
|
15758
|
+
const lines = [`\u{1F50C} **Package Plugins**`, `Dir: ${pluginRoot(configDir)}`, ""];
|
|
15759
|
+
if (plugins.length === 0) lines.push("No package plugins installed.");
|
|
15760
|
+
for (const p of plugins) lines.push(` \u2022 ${p.name}@${p.manifest.version} \u2014 ${p.enabled ? "enabled" : "disabled"} / ${p.trusted ? "trusted" : "untrusted"}`);
|
|
15761
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
15762
|
+
return;
|
|
15537
15763
|
}
|
|
15538
|
-
if (
|
|
15539
|
-
|
|
15540
|
-
|
|
15541
|
-
|
|
15542
|
-
|
|
15543
|
-
|
|
15764
|
+
if (sub === "install") {
|
|
15765
|
+
const input = args[1];
|
|
15766
|
+
if (!input) throw new Error("Usage: /plugin install <path>");
|
|
15767
|
+
const plugin = installPlugin(configDir, input);
|
|
15768
|
+
ctx.send({ type: "info", message: `Installed plugin ${plugin.name}@${plugin.manifest.version}. Run /plugin trust ${plugin.name}, then /plugin enable ${plugin.name}.` });
|
|
15769
|
+
return;
|
|
15544
15770
|
}
|
|
15545
|
-
|
|
15546
|
-
|
|
15547
|
-
|
|
15548
|
-
|
|
15771
|
+
if (sub === "trust") {
|
|
15772
|
+
const name = args[1];
|
|
15773
|
+
if (!name) throw new Error("Usage: /plugin trust <name>");
|
|
15774
|
+
const state2 = trustPlugin(configDir, name);
|
|
15775
|
+
ctx.send({ type: "info", message: `Trusted plugin ${state2.name}.` });
|
|
15776
|
+
return;
|
|
15777
|
+
}
|
|
15778
|
+
if (sub === "enable" || sub === "disable") {
|
|
15779
|
+
const name = args[1];
|
|
15780
|
+
if (!name) throw new Error(`Usage: /plugin ${sub} <name>`);
|
|
15781
|
+
const state2 = setPluginEnabled(configDir, name, sub === "enable");
|
|
15782
|
+
ctx.send({ type: "info", message: `${sub === "enable" ? "Enabled" : "Disabled"} plugin ${state2.name}. Restart Web UI for changes to take effect.` });
|
|
15783
|
+
return;
|
|
15784
|
+
}
|
|
15785
|
+
if (sub === "inspect") {
|
|
15786
|
+
const name = args[1];
|
|
15787
|
+
if (!name) throw new Error("Usage: /plugin inspect <name>");
|
|
15788
|
+
ctx.send({ type: "info", message: describePlugin(getInstalledPlugin(configDir, name)).join("\n") });
|
|
15789
|
+
return;
|
|
15790
|
+
}
|
|
15791
|
+
throw new Error("Usage: /plugin [list|install|enable|disable|inspect|trust]");
|
|
15792
|
+
} catch (err) {
|
|
15793
|
+
ctx.send({ type: "error", message: err instanceof Error ? err.message : String(err) });
|
|
15549
15794
|
}
|
|
15550
15795
|
}
|
|
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)" });
|
|
15796
|
+
async function handleMcp(_args, ctx) {
|
|
15797
|
+
if (!ctx.mcpManager) {
|
|
15798
|
+
ctx.send({ type: "info", message: "MCP not configured. Add mcpServers to config.json." });
|
|
15799
|
+
return;
|
|
15800
|
+
}
|
|
15801
|
+
const sub = _args[0];
|
|
15802
|
+
if (sub === "reconnect") {
|
|
15803
|
+
ctx.send({ type: "info", message: "\u{1F504} Reconnecting MCP servers..." });
|
|
15804
|
+
try {
|
|
15805
|
+
await ctx.mcpManager.reconnectAll();
|
|
15806
|
+
ctx.send({ type: "info", message: "\u2713 MCP reconnect complete." });
|
|
15807
|
+
} catch (err) {
|
|
15808
|
+
ctx.send({ type: "error", message: `Reconnect failed: ${err.message}` });
|
|
15809
|
+
}
|
|
15810
|
+
ctx.sendToolsList();
|
|
15811
|
+
return;
|
|
15812
|
+
}
|
|
15813
|
+
const statuses = ctx.mcpManager.getStatus();
|
|
15814
|
+
if (statuses.length === 0) {
|
|
15815
|
+
ctx.send({ type: "info", message: "No MCP servers configured." });
|
|
15816
|
+
return;
|
|
15817
|
+
}
|
|
15818
|
+
const lines = [`\u{1F50C} **MCP Servers (${statuses.length}):**`, ""];
|
|
15819
|
+
for (const s of statuses) {
|
|
15820
|
+
const state2 = s.connected ? `\u2713 connected \xB7 ${s.serverName} \xB7 ${s.toolCount} tools` : `\u2717 disconnected${s.error ? ` \xB7 ${s.error}` : ""}`;
|
|
15821
|
+
lines.push(` ${s.serverId}: ${state2}`);
|
|
15822
|
+
}
|
|
15823
|
+
lines.push("");
|
|
15824
|
+
lines.push("Use /mcp reconnect to reconnect.");
|
|
15825
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
15826
|
+
}
|
|
15827
|
+
async function handleSkill(args, ctx) {
|
|
15828
|
+
const sub = args[0];
|
|
15829
|
+
if (!ctx.skillManager) {
|
|
15830
|
+
ctx.send({ type: "error", message: "Skill system not available." });
|
|
15831
|
+
return;
|
|
15832
|
+
}
|
|
15833
|
+
if (!sub || sub === "list") {
|
|
15834
|
+
const skills = ctx.skillManager.listSkills();
|
|
15835
|
+
const active = ctx.skillManager.getActive();
|
|
15836
|
+
if (skills.length === 0) {
|
|
15837
|
+
ctx.send({ type: "info", message: "No skills available. Add .md files to ~/.aicli/skills/" });
|
|
15618
15838
|
} else {
|
|
15619
|
-
|
|
15620
|
-
|
|
15621
|
-
|
|
15839
|
+
const lines = skills.map((s) => {
|
|
15840
|
+
const isActive = active?.meta.name === s.meta.name;
|
|
15841
|
+
const marker = isActive ? " \u2705 (active)" : "";
|
|
15842
|
+
return ` ${s.meta.name}${marker} \u2014 ${s.meta.description || "(no description)"}`;
|
|
15622
15843
|
});
|
|
15844
|
+
ctx.send({ type: "info", message: `\u{1F4DA} Skills:
|
|
15845
|
+
${lines.join("\n")}` });
|
|
15623
15846
|
}
|
|
15847
|
+
return;
|
|
15848
|
+
}
|
|
15849
|
+
if (sub === "off") {
|
|
15850
|
+
ctx.skillManager.deactivate();
|
|
15851
|
+
ctx.send({ type: "info", message: "\u{1F50C} Skill deactivated." });
|
|
15852
|
+
ctx.sendToolsList();
|
|
15853
|
+
ctx.sendStatus();
|
|
15854
|
+
return;
|
|
15855
|
+
}
|
|
15856
|
+
if (sub === "reload") {
|
|
15857
|
+
ctx.skillManager.loadSkills(getActivePluginAssets(ctx.config.getConfigDir()).skillDirs);
|
|
15858
|
+
ctx.send({ type: "info", message: `\u{1F504} Reloaded ${ctx.skillManager.listSkills().length} skill(s).` });
|
|
15859
|
+
ctx.sendToolsList();
|
|
15860
|
+
return;
|
|
15861
|
+
}
|
|
15862
|
+
const activated = ctx.skillManager.activate(sub);
|
|
15863
|
+
if (activated) {
|
|
15864
|
+
ctx.send({ type: "info", message: `\u2705 Skill activated: ${activated.meta.name}
|
|
15865
|
+
${activated.meta.description || ""}` });
|
|
15866
|
+
ctx.sendToolsList();
|
|
15867
|
+
ctx.sendStatus();
|
|
15868
|
+
} else {
|
|
15869
|
+
const available = ctx.skillManager.listSkills().map((s) => s.meta.name).join(", ");
|
|
15870
|
+
ctx.send({ type: "error", message: `Skill "${sub}" not found. Available: ${available}` });
|
|
15624
15871
|
}
|
|
15625
15872
|
}
|
|
15626
|
-
async function
|
|
15627
|
-
|
|
15628
|
-
|
|
15629
|
-
|
|
15630
|
-
|
|
15631
|
-
|
|
15632
|
-
|
|
15873
|
+
async function handleCommands(_args, ctx) {
|
|
15874
|
+
const configDir = ctx.config.getConfigDir();
|
|
15875
|
+
const commandsDir = join24(configDir, CUSTOM_COMMANDS_DIR_NAME);
|
|
15876
|
+
if (!existsSync29(commandsDir)) {
|
|
15877
|
+
ctx.send({ type: "info", message: `No custom commands directory.
|
|
15878
|
+
Create: ${commandsDir}/ with .md files.` });
|
|
15879
|
+
return;
|
|
15880
|
+
}
|
|
15881
|
+
try {
|
|
15882
|
+
const files = readdirSync13(commandsDir).filter((f) => f.endsWith(".md"));
|
|
15883
|
+
if (files.length === 0) {
|
|
15884
|
+
ctx.send({ type: "info", message: `No custom commands found in ${commandsDir}
|
|
15885
|
+
Add .md files to create commands.` });
|
|
15633
15886
|
} else {
|
|
15634
|
-
|
|
15887
|
+
const lines = [`\u{1F4CB} **Custom Commands (${files.length}):**`, `Dir: ${commandsDir}`, ""];
|
|
15888
|
+
for (const f of files) {
|
|
15889
|
+
const name = f.replace(/\.md$/, "");
|
|
15890
|
+
lines.push(` /${name}`);
|
|
15891
|
+
}
|
|
15892
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
15635
15893
|
}
|
|
15894
|
+
} catch {
|
|
15895
|
+
ctx.send({ type: "info", message: `Cannot read: ${commandsDir}` });
|
|
15636
15896
|
}
|
|
15637
15897
|
}
|
|
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
15898
|
|
|
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.` });
|
|
15899
|
+
// src/web/commands/review-commands.ts
|
|
15900
|
+
async function handleReview(args, ctx) {
|
|
15901
|
+
const gitCtx = getGitContext();
|
|
15902
|
+
if (!gitCtx) {
|
|
15903
|
+
ctx.send({ type: "error", message: "Not a git repository." });
|
|
15904
|
+
return;
|
|
15661
15905
|
}
|
|
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;
|
|
15906
|
+
const staged = args.includes("--staged");
|
|
15907
|
+
const detailed = args.includes("--detailed");
|
|
15908
|
+
let diff;
|
|
15695
15909
|
try {
|
|
15696
|
-
|
|
15910
|
+
diff = readGitDiff({ staged });
|
|
15697
15911
|
} catch {
|
|
15698
|
-
|
|
15912
|
+
ctx.send({ type: "error", message: "Failed to run git diff." });
|
|
15913
|
+
return;
|
|
15699
15914
|
}
|
|
15700
|
-
|
|
15701
|
-
|
|
15702
|
-
|
|
15703
|
-
|
|
15704
|
-
|
|
15705
|
-
|
|
15706
|
-
|
|
15707
|
-
|
|
15708
|
-
|
|
15709
|
-
|
|
15710
|
-
|
|
15711
|
-
|
|
15712
|
-
|
|
15713
|
-
|
|
15714
|
-
|
|
15715
|
-
|
|
15716
|
-
|
|
15717
|
-
|
|
15718
|
-
|
|
15915
|
+
if (!diff) {
|
|
15916
|
+
ctx.send({ type: "info", message: "No changes to review." + (staged ? "" : " Try --staged for staged changes.") });
|
|
15917
|
+
return;
|
|
15918
|
+
}
|
|
15919
|
+
const MAX_DIFF = 8e3;
|
|
15920
|
+
let truncated = false;
|
|
15921
|
+
if (diff.length > MAX_DIFF) {
|
|
15922
|
+
const head = diff.slice(0, Math.floor(MAX_DIFF * 0.7));
|
|
15923
|
+
const tail = diff.slice(diff.length - Math.floor(MAX_DIFF * 0.2));
|
|
15924
|
+
diff = head + "\n\n... [diff truncated, " + diff.length + " chars total] ...\n\n" + tail;
|
|
15925
|
+
truncated = true;
|
|
15926
|
+
}
|
|
15927
|
+
const reviewPrompt = buildReviewPrompt(diff, formatGitContextForPrompt(gitCtx), detailed);
|
|
15928
|
+
ctx.send({ type: "info", message: "\u{1F50D} Analyzing changes..." });
|
|
15929
|
+
try {
|
|
15930
|
+
const review = await ctx.chatOnce(reviewPrompt, { temperature: 0.3, maxTokens: 8192 });
|
|
15931
|
+
const msg = truncated ? review + "\n\n\u26A0 Diff was truncated. Consider reviewing smaller changesets." : review;
|
|
15932
|
+
ctx.send({ type: "info", message: msg });
|
|
15933
|
+
} catch (err) {
|
|
15934
|
+
ctx.send({ type: "error", message: `Review failed: ${err.message}` });
|
|
15719
15935
|
}
|
|
15720
|
-
files.sort((a, b) => a.timestamp < b.timestamp ? 1 : -1);
|
|
15721
|
-
return files.slice(0, limit);
|
|
15722
15936
|
}
|
|
15723
|
-
function
|
|
15724
|
-
const
|
|
15725
|
-
if (!
|
|
15726
|
-
|
|
15727
|
-
|
|
15728
|
-
|
|
15937
|
+
async function handleSecurityReview(args, ctx) {
|
|
15938
|
+
const gitCtx = getGitContext();
|
|
15939
|
+
if (!gitCtx) {
|
|
15940
|
+
ctx.send({ type: "error", message: "Not a git repository." });
|
|
15941
|
+
return;
|
|
15942
|
+
}
|
|
15943
|
+
const secStaged = args.includes("--staged");
|
|
15944
|
+
let secDiff;
|
|
15729
15945
|
try {
|
|
15730
|
-
|
|
15946
|
+
secDiff = readGitDiff({ staged: secStaged });
|
|
15731
15947
|
} catch {
|
|
15732
|
-
|
|
15948
|
+
ctx.send({ type: "error", message: "Failed to run git diff." });
|
|
15949
|
+
return;
|
|
15733
15950
|
}
|
|
15734
|
-
|
|
15735
|
-
|
|
15736
|
-
|
|
15737
|
-
|
|
15738
|
-
|
|
15951
|
+
if (!secDiff) {
|
|
15952
|
+
ctx.send({ type: "info", message: "No changes to review." + (secStaged ? "" : " Try --staged for staged changes.") });
|
|
15953
|
+
return;
|
|
15954
|
+
}
|
|
15955
|
+
const SEC_MAX_DIFF = 8e3;
|
|
15956
|
+
let secTruncated = false;
|
|
15957
|
+
if (secDiff.length > SEC_MAX_DIFF) {
|
|
15958
|
+
const head = secDiff.slice(0, Math.floor(SEC_MAX_DIFF * 0.7));
|
|
15959
|
+
const tail = secDiff.slice(secDiff.length - Math.floor(SEC_MAX_DIFF * 0.2));
|
|
15960
|
+
secDiff = head + "\n\n... [diff truncated, " + secDiff.length + " chars total] ...\n\n" + tail;
|
|
15961
|
+
secTruncated = true;
|
|
15962
|
+
}
|
|
15963
|
+
const secPrompt = buildSecurityReviewPrompt(secDiff, formatGitContextForPrompt(gitCtx));
|
|
15964
|
+
ctx.send({ type: "info", message: "\u{1F512} Scanning for security vulnerabilities..." });
|
|
15965
|
+
try {
|
|
15966
|
+
const secReview = await ctx.chatOnce(secPrompt, { temperature: 0.2, maxTokens: 8192 });
|
|
15967
|
+
const secMsg = secTruncated ? secReview + "\n\n\u26A0 Diff was truncated. Consider reviewing smaller changesets." : secReview;
|
|
15968
|
+
ctx.send({ type: "info", message: secMsg });
|
|
15969
|
+
} catch (err) {
|
|
15970
|
+
ctx.send({ type: "error", message: `Security review failed: ${err.message}` });
|
|
15739
15971
|
}
|
|
15740
|
-
entries.sort((a, b) => b.bytes - a.bytes);
|
|
15741
|
-
return { totalBytes: total, entries };
|
|
15742
15972
|
}
|
|
15743
|
-
function
|
|
15973
|
+
async function handleTest(args, ctx) {
|
|
15974
|
+
ctx.send({ type: "info", message: "\u{1F9EA} Running tests..." });
|
|
15744
15975
|
try {
|
|
15745
|
-
const
|
|
15746
|
-
|
|
15747
|
-
|
|
15748
|
-
|
|
15749
|
-
|
|
15750
|
-
|
|
15751
|
-
}
|
|
15752
|
-
|
|
15753
|
-
|
|
15754
|
-
|
|
15976
|
+
const { executeTests } = await import("./run-tests-S36TE2W2.js");
|
|
15977
|
+
const argStr = args.join(" ").trim();
|
|
15978
|
+
let testArgs = {};
|
|
15979
|
+
if (argStr) {
|
|
15980
|
+
const isCommand = argStr.includes(" ") || /^(mvn|gradle|npm|pytest|cargo|go)\b/.test(argStr);
|
|
15981
|
+
testArgs = isCommand ? { command: argStr } : { filter: argStr };
|
|
15982
|
+
}
|
|
15983
|
+
const report = await executeTests(testArgs);
|
|
15984
|
+
ctx.send({ type: "info", message: report });
|
|
15985
|
+
} catch (err) {
|
|
15986
|
+
ctx.send({ type: "error", message: `Test failed: ${err.message}` });
|
|
15755
15987
|
}
|
|
15756
15988
|
}
|
|
15757
|
-
|
|
15758
|
-
|
|
15759
|
-
|
|
15760
|
-
|
|
15761
|
-
|
|
15762
|
-
|
|
15763
|
-
|
|
15764
|
-
|
|
15765
|
-
|
|
15989
|
+
async function handleDiff(args, ctx) {
|
|
15990
|
+
const history = undoStack.getHistory();
|
|
15991
|
+
if (history.length === 0) {
|
|
15992
|
+
ctx.send({ type: "info", message: "No file modifications in this session." });
|
|
15993
|
+
return;
|
|
15994
|
+
}
|
|
15995
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
15996
|
+
for (const entry of history) {
|
|
15997
|
+
if (!fileMap.has(entry.filePath)) {
|
|
15998
|
+
fileMap.set(entry.filePath, { earliest: entry.previousContent });
|
|
15766
15999
|
}
|
|
15767
16000
|
}
|
|
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)
|
|
16001
|
+
const statsOnly = args[0]?.toLowerCase() === "--stats";
|
|
16002
|
+
let newFiles = 0;
|
|
16003
|
+
let modifiedFiles = 0;
|
|
16004
|
+
const diffLines2 = [];
|
|
16005
|
+
for (const [filePath, { earliest }] of fileMap) {
|
|
16006
|
+
const currentContent = existsSync29(filePath) ? (() => {
|
|
16007
|
+
try {
|
|
16008
|
+
return readFileSync22(filePath, "utf-8");
|
|
16009
|
+
} catch {
|
|
16010
|
+
return null;
|
|
16011
|
+
}
|
|
16012
|
+
})() : null;
|
|
16013
|
+
const isNew = earliest === null;
|
|
16014
|
+
if (isNew) newFiles++;
|
|
16015
|
+
else modifiedFiles++;
|
|
16016
|
+
if (statsOnly) continue;
|
|
16017
|
+
if (isNew) {
|
|
16018
|
+
diffLines2.push(`
|
|
16019
|
+
\u{1F4C4} [NEW] ${filePath}`);
|
|
16020
|
+
if (currentContent !== null) {
|
|
16021
|
+
diffLines2.push(` ${currentContent.split("\n").length} lines`);
|
|
16022
|
+
} else {
|
|
16023
|
+
diffLines2.push(" (file was deleted after creation)");
|
|
16024
|
+
}
|
|
16025
|
+
} else {
|
|
16026
|
+
diffLines2.push(`
|
|
16027
|
+
\u{1F4DD} [MODIFIED] ${filePath}`);
|
|
16028
|
+
const oldText = earliest ?? "";
|
|
16029
|
+
const newText = currentContent ?? "";
|
|
16030
|
+
if (oldText === newText) {
|
|
16031
|
+
diffLines2.push(" (no net change)");
|
|
16032
|
+
} else {
|
|
16033
|
+
diffLines2.push(renderDiff(oldText, newText, { filePath, maxLines: 80 }));
|
|
16034
|
+
}
|
|
15868
16035
|
}
|
|
15869
|
-
}
|
|
16036
|
+
}
|
|
16037
|
+
diffLines2.push(`
|
|
16038
|
+
**Summary:** ${fileMap.size} file(s) \u2014 ${newFiles} new, ${modifiedFiles} modified`);
|
|
16039
|
+
ctx.send({ type: "info", message: diffLines2.join("\n") });
|
|
16040
|
+
}
|
|
16041
|
+
async function handleInit(args, ctx) {
|
|
16042
|
+
const cwd = process.cwd();
|
|
16043
|
+
const targetPath = join24(cwd, "AICLI.md");
|
|
16044
|
+
const force = args.includes("--force");
|
|
16045
|
+
if (existsSync29(targetPath) && !force) {
|
|
16046
|
+
ctx.send({ type: "info", message: `AICLI.md already exists at ${targetPath}
|
|
16047
|
+
Use /init --force to overwrite.` });
|
|
16048
|
+
return;
|
|
16049
|
+
}
|
|
16050
|
+
ctx.send({ type: "info", message: "\u{1F4DD} Scanning project structure..." });
|
|
16051
|
+
try {
|
|
16052
|
+
const projectInfo = scanProject(cwd);
|
|
16053
|
+
const prompt = buildInitPrompt(projectInfo, cwd);
|
|
16054
|
+
const content = await ctx.chatOnce(prompt, { temperature: 0.3, maxTokens: 4096 });
|
|
16055
|
+
writeFileSync3(targetPath, content, "utf-8");
|
|
16056
|
+
ctx.send({ type: "info", message: `\u2713 Generated: ${targetPath} (${content.length} chars)
|
|
16057
|
+
Use /context reload to load it.` });
|
|
16058
|
+
} catch (err) {
|
|
16059
|
+
ctx.send({ type: "error", message: `Failed to generate AICLI.md: ${err.message}` });
|
|
16060
|
+
}
|
|
15870
16061
|
}
|
|
16062
|
+
async function handleScaffold(args, ctx) {
|
|
16063
|
+
const description = args.join(" ").trim();
|
|
16064
|
+
if (!description) {
|
|
16065
|
+
ctx.send({ type: "error", message: "Usage: /scaffold <project description>" });
|
|
16066
|
+
return;
|
|
16067
|
+
}
|
|
16068
|
+
ctx.send({ type: "info", message: "\u{1F3D7}\uFE0F Generating project scaffold..." });
|
|
16069
|
+
const scaffoldPrompt = `Please scaffold a project based on this description: ${description}
|
|
15871
16070
|
|
|
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`;
|
|
16071
|
+
Create the necessary files and directory structure. Use write_file and bash tools to set up the project.`;
|
|
16072
|
+
await ctx.handleChat(scaffoldPrompt);
|
|
15878
16073
|
}
|
|
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")}`);
|
|
16074
|
+
|
|
16075
|
+
// src/web/commands/memory-commands.ts
|
|
16076
|
+
async function handleSystem(args, ctx) {
|
|
16077
|
+
const text = args.join(" ").trim();
|
|
16078
|
+
if (!text) {
|
|
16079
|
+
const current = ctx.activeSystemPrompt;
|
|
16080
|
+
if (current) {
|
|
16081
|
+
const preview = current.length > 500 ? current.slice(0, 500) + "..." : current;
|
|
16082
|
+
ctx.send({ type: "info", message: `\u{1F4CB} Current system prompt (${current.length} chars):
|
|
16083
|
+
|
|
16084
|
+
${preview}` });
|
|
16085
|
+
} else {
|
|
16086
|
+
ctx.send({ type: "info", message: "No custom system prompt set. Using default.\nUsage: /system <prompt>" });
|
|
16087
|
+
}
|
|
16088
|
+
return;
|
|
15905
16089
|
}
|
|
15906
|
-
|
|
15907
|
-
|
|
15908
|
-
|
|
15909
|
-
|
|
15910
|
-
|
|
15911
|
-
|
|
16090
|
+
if (text === "clear" || text === "reset") {
|
|
16091
|
+
ctx.activeSystemPrompt = ctx.loadContextFiles();
|
|
16092
|
+
ctx.send({ type: "info", message: "\u2713 System prompt reset to default (context files)." });
|
|
16093
|
+
} else {
|
|
16094
|
+
ctx.activeSystemPrompt = text;
|
|
16095
|
+
ctx.send({ type: "info", message: `\u2713 System prompt set (${text.length} chars).` });
|
|
15912
16096
|
}
|
|
15913
|
-
|
|
15914
|
-
|
|
15915
|
-
|
|
15916
|
-
|
|
16097
|
+
}
|
|
16098
|
+
async function handleContext(_args, ctx) {
|
|
16099
|
+
const sub = _args[0];
|
|
16100
|
+
if (sub === "reload") {
|
|
16101
|
+
ctx.activeSystemPrompt = ctx.loadContextFiles();
|
|
16102
|
+
ctx.send({ type: "info", message: ctx.activeSystemPrompt ? `\u2713 Context reloaded (${ctx.activeSystemPrompt.length} chars).` : "\u2713 No context files found." });
|
|
16103
|
+
return;
|
|
16104
|
+
}
|
|
16105
|
+
const result = ctx.contextLoadResult;
|
|
16106
|
+
const layers = ["\u{1F4DA} **Context Layers:**", ""];
|
|
16107
|
+
if (result && result.layers.length > 0) {
|
|
16108
|
+
const labels = { global: "Global ", project: "Project", plugin: "Plugin ", local: "Local " };
|
|
16109
|
+
for (const layer of result.layers) {
|
|
16110
|
+
layers.push(` \u2713 ${labels[layer.level] ?? layer.level}: ${layer.displayPath} (${layer.charCount} chars${layer.truncated ? ", truncated" : ""})`);
|
|
15917
16111
|
}
|
|
16112
|
+
layers.push(` Total: ${result.totalChars} chars (${result.layers.length} layer${result.layers.length > 1 ? "s" : ""})`);
|
|
16113
|
+
} else {
|
|
16114
|
+
layers.push(" No context files loaded.");
|
|
16115
|
+
layers.push(" Search order per layer: AICLI.override.md \u2192 AGENTS.override.md \u2192 AICLI.md \u2192 CLAUDE.md \u2192 AGENTS.md");
|
|
15918
16116
|
}
|
|
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)}`);
|
|
16117
|
+
if (result && result.skipped.length > 0) {
|
|
16118
|
+
layers.push("");
|
|
16119
|
+
layers.push("Skipped:");
|
|
16120
|
+
for (const skipped of result.skipped.slice(0, 5)) {
|
|
16121
|
+
layers.push(` - ${skipped.displayPath}: ${skipped.reason}${skipped.message ? ` (${skipped.message})` : ""}`);
|
|
15935
16122
|
}
|
|
15936
16123
|
}
|
|
15937
|
-
|
|
15938
|
-
|
|
15939
|
-
|
|
15940
|
-
|
|
15941
|
-
|
|
15942
|
-
|
|
15943
|
-
|
|
16124
|
+
layers.push("");
|
|
16125
|
+
layers.push(`Total prompt length: ${ctx.activeSystemPrompt?.length ?? 0} chars`);
|
|
16126
|
+
layers.push("Use /context reload to refresh.");
|
|
16127
|
+
ctx.send({ type: "info", message: layers.join("\n") });
|
|
16128
|
+
}
|
|
16129
|
+
async function handleIndex(args, ctx) {
|
|
16130
|
+
const sub = (args[0] ?? "status").toLowerCase();
|
|
16131
|
+
const root = process.cwd();
|
|
16132
|
+
const { loadIndex: loadIndex2, clearIndex } = await import("./store-VO37H6LS.js");
|
|
16133
|
+
const { indexProject: indexProject2 } = await import("./indexer-4WWS3VIL.js");
|
|
16134
|
+
const { loadVectorStore, clearVectorStore } = await import("./vector-store-JBAE6PS4.js");
|
|
16135
|
+
if (sub === "status") {
|
|
16136
|
+
const idx = loadIndex2(root);
|
|
16137
|
+
const vec = loadVectorStore(root);
|
|
16138
|
+
if (!idx) {
|
|
16139
|
+
ctx.send({ type: "info", message: `No symbol index for ${root}. Run /index rebuild.` });
|
|
16140
|
+
return;
|
|
16141
|
+
}
|
|
16142
|
+
const semantic = vec ? `${vec.count} vectors \xD7 ${vec.dim}-dim` : "not built \u2014 run /index semantic-rebuild to enable search_code";
|
|
16143
|
+
ctx.send({
|
|
16144
|
+
type: "info",
|
|
16145
|
+
message: [
|
|
16146
|
+
"\u{1F50E} Symbol index:",
|
|
16147
|
+
` Root: ${idx.root}`,
|
|
16148
|
+
` Generated: ${idx.generated}`,
|
|
16149
|
+
` Files: ${idx.fileCount}`,
|
|
16150
|
+
` Symbols: ${idx.symbolCount}`,
|
|
16151
|
+
` Semantic: ${semantic}`
|
|
16152
|
+
].join("\n")
|
|
16153
|
+
});
|
|
16154
|
+
} else if (sub === "rebuild") {
|
|
16155
|
+
ctx.send({ type: "info", message: `Indexing ${root}\u2026` });
|
|
16156
|
+
const { stats } = await indexProject2(root, { force: true });
|
|
16157
|
+
ctx.send({
|
|
16158
|
+
type: "info",
|
|
16159
|
+
message: `\u2713 Indexed ${stats.filesParsed} files (${stats.symbols} symbols) in ${stats.durationMs}ms`
|
|
16160
|
+
});
|
|
16161
|
+
} else if (sub === "clear") {
|
|
16162
|
+
clearIndex(root);
|
|
16163
|
+
clearVectorStore(root);
|
|
16164
|
+
ctx.send({ type: "info", message: `\u2713 Cleared symbol + semantic index for ${root}` });
|
|
16165
|
+
} else if (sub === "semantic-rebuild") {
|
|
16166
|
+
const idx = loadIndex2(root);
|
|
16167
|
+
if (!idx) {
|
|
16168
|
+
ctx.send({ type: "error", message: "No symbol index yet. Run /index rebuild first." });
|
|
16169
|
+
return;
|
|
16170
|
+
}
|
|
16171
|
+
if (idx.symbolCount === 0) {
|
|
16172
|
+
ctx.send({
|
|
16173
|
+
type: "error",
|
|
16174
|
+
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."
|
|
16175
|
+
});
|
|
16176
|
+
return;
|
|
16177
|
+
}
|
|
16178
|
+
ctx.send({
|
|
16179
|
+
type: "info",
|
|
16180
|
+
message: `Building semantic index for ${idx.symbolCount} symbols\u2026 (first run downloads ~117 MB model)`
|
|
16181
|
+
});
|
|
16182
|
+
try {
|
|
16183
|
+
const { rebuildSemanticIndex } = await import("./semantic-FF6DDJI6.js");
|
|
16184
|
+
const stats = await rebuildSemanticIndex(root);
|
|
16185
|
+
const first = stats.modelFirstLoadMs ? ` (model load+first batch ${stats.modelFirstLoadMs}ms)` : "";
|
|
16186
|
+
ctx.send({
|
|
16187
|
+
type: "info",
|
|
16188
|
+
message: `\u2713 Embedded ${stats.symbolsEmbedded} symbols in ${stats.durationMs}ms${first}`
|
|
16189
|
+
});
|
|
16190
|
+
} catch (err) {
|
|
16191
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
16192
|
+
ctx.send({ type: "error", message: `Semantic rebuild failed: ${msg}` });
|
|
16193
|
+
}
|
|
16194
|
+
} else if (sub === "semantic-clear") {
|
|
16195
|
+
clearVectorStore(root);
|
|
16196
|
+
ctx.send({ type: "info", message: "\u2713 Cleared semantic index (symbol index preserved)" });
|
|
16197
|
+
} else {
|
|
16198
|
+
ctx.send({
|
|
16199
|
+
type: "error",
|
|
16200
|
+
message: `Unknown subcommand: ${sub}. Use status/rebuild/clear/semantic-rebuild/semantic-clear.`
|
|
16201
|
+
});
|
|
15944
16202
|
}
|
|
15945
|
-
|
|
15946
|
-
|
|
15947
|
-
|
|
15948
|
-
|
|
15949
|
-
|
|
16203
|
+
}
|
|
16204
|
+
async function handleMemory(args, ctx) {
|
|
16205
|
+
const sub = args[0];
|
|
16206
|
+
if (sub === "add" && args.length > 1) {
|
|
16207
|
+
ctx.memoryAdd(args.slice(1).join(" "));
|
|
16208
|
+
} else if (sub === "clear") {
|
|
16209
|
+
ctx.memoryClear();
|
|
16210
|
+
} else {
|
|
16211
|
+
ctx.memoryShow();
|
|
15950
16212
|
}
|
|
15951
|
-
|
|
15952
|
-
|
|
15953
|
-
|
|
15954
|
-
|
|
15955
|
-
|
|
15956
|
-
|
|
15957
|
-
|
|
16213
|
+
}
|
|
16214
|
+
async function handleAddDir(args, ctx) {
|
|
16215
|
+
const sub = args[0]?.trim();
|
|
16216
|
+
if (!sub) {
|
|
16217
|
+
ctx.send({ type: "info", message: ctx.addedDirs.size > 0 ? `\u{1F4C1} Added directories:
|
|
16218
|
+
${[...ctx.addedDirs].map((d) => ` \u2022 ${d}`).join("\n")}
|
|
16219
|
+
|
|
16220
|
+
Use /add-dir remove to clear.` : "No directories added.\nUsage: /add-dir <path> | /add-dir remove" });
|
|
16221
|
+
return;
|
|
15958
16222
|
}
|
|
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`);
|
|
16223
|
+
if (sub === "remove" || sub === "clear") {
|
|
16224
|
+
ctx.addedDirs.clear();
|
|
16225
|
+
ctx.send({ type: "info", message: "\u2713 All added directories removed from context." });
|
|
16226
|
+
return;
|
|
15966
16227
|
}
|
|
15967
|
-
|
|
15968
|
-
|
|
15969
|
-
|
|
16228
|
+
const dirPath = resolve10(sub);
|
|
16229
|
+
if (!existsSync29(dirPath)) {
|
|
16230
|
+
ctx.send({ type: "error", message: `Directory not found: ${dirPath}` });
|
|
16231
|
+
return;
|
|
15970
16232
|
}
|
|
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}`)}` : ""}`);
|
|
16233
|
+
if (!statSync12(dirPath).isDirectory()) {
|
|
16234
|
+
ctx.send({ type: "error", message: `Not a directory: ${dirPath}` });
|
|
16235
|
+
return;
|
|
15985
16236
|
}
|
|
15986
|
-
|
|
15987
|
-
|
|
15988
|
-
|
|
15989
|
-
return out.join("\n");
|
|
16237
|
+
ctx.addedDirs.add(dirPath);
|
|
16238
|
+
ctx.send({ type: "info", message: `\u2713 Added directory: ${dirPath}
|
|
16239
|
+
It will be included in AI context for subsequent messages.` });
|
|
15990
16240
|
}
|
|
15991
16241
|
|
|
15992
16242
|
// src/web/commands/security-commands.ts
|
|
15993
16243
|
async function handleConfig(args, ctx) {
|
|
15994
|
-
|
|
15995
|
-
|
|
15996
|
-
|
|
15997
|
-
ctx.send({ type: "info", message: `\u2699\uFE0F Configuration:
|
|
16244
|
+
const sub = args[0];
|
|
16245
|
+
if (sub === "show" || !sub) {
|
|
16246
|
+
ctx.send({ type: "info", message: `\u2699\uFE0F Configuration:
|
|
15998
16247
|
\`\`\`json
|
|
15999
16248
|
${ctx.config.toFormattedJSON()}
|
|
16000
16249
|
\`\`\`` });
|
|
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>" });
|
|
16250
|
+
} else if (sub === "get" && args[1]) {
|
|
16251
|
+
const val = ctx.config.getByPath(args[1]);
|
|
16252
|
+
const display = typeof val === "object" ? JSON.stringify(val, null, 2) : String(val ?? "undefined");
|
|
16253
|
+
ctx.send({ type: "info", message: `${args[1]} = ${display}` });
|
|
16254
|
+
} else if (sub === "set" && args[1] && args[2] !== void 0) {
|
|
16255
|
+
try {
|
|
16256
|
+
ctx.config.setByPath(args[1], args.slice(2).join(" "));
|
|
16257
|
+
ctx.send({ type: "info", message: `\u2713 ${args[1]} = ${args.slice(2).join(" ")}` });
|
|
16258
|
+
} catch (err) {
|
|
16259
|
+
ctx.send({ type: "error", message: `Config error: ${err.message}` });
|
|
16014
16260
|
}
|
|
16261
|
+
} else {
|
|
16262
|
+
ctx.send({ type: "info", message: "Usage: /config [show] | /config get <key> | /config set <key> <value>" });
|
|
16015
16263
|
}
|
|
16016
16264
|
}
|
|
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
|
-
}
|
|
16265
|
+
async function handleDoctor(_args, ctx) {
|
|
16266
|
+
const report = buildDoctorReport({
|
|
16267
|
+
config: ctx.config,
|
|
16268
|
+
providers: ctx.providers,
|
|
16269
|
+
cwd: process.cwd(),
|
|
16270
|
+
mcpStatuses: ctx.mcpManager?.getStatus() ?? []
|
|
16271
|
+
});
|
|
16272
|
+
ctx.send({ type: "info", message: formatDoctorReport(report, false) });
|
|
16273
|
+
}
|
|
16274
|
+
async function handleBug(_args, ctx) {
|
|
16275
|
+
const session = ctx.sessions.current;
|
|
16276
|
+
const lines = [
|
|
16277
|
+
"\u{1F41B} **Bug Report Template**",
|
|
16278
|
+
"",
|
|
16279
|
+
"**Environment:**",
|
|
16280
|
+
` ai-cli version: ${VERSION}`,
|
|
16281
|
+
` Node.js: ${process.version}`,
|
|
16282
|
+
` OS: ${process.platform} ${process.arch}`,
|
|
16283
|
+
` Provider: ${ctx.currentProvider}`,
|
|
16284
|
+
` Model: ${ctx.currentModel}`,
|
|
16285
|
+
` Session: ${session?.id.slice(0, 8) ?? "none"} (${session?.messages.length ?? 0} messages)`,
|
|
16286
|
+
"",
|
|
16287
|
+
"**Description:**",
|
|
16288
|
+
"(Describe the bug)",
|
|
16289
|
+
"",
|
|
16290
|
+
"**Steps to reproduce:**",
|
|
16291
|
+
"1. ...",
|
|
16292
|
+
"",
|
|
16293
|
+
"**Expected behavior:**",
|
|
16294
|
+
"...",
|
|
16295
|
+
"",
|
|
16296
|
+
"**Actual behavior:**",
|
|
16297
|
+
"...",
|
|
16298
|
+
"",
|
|
16299
|
+
"Report at: https://github.com/jinzhengdong/ai-cli/issues"
|
|
16300
|
+
];
|
|
16301
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
16058
16302
|
}
|
|
16059
16303
|
|
|
16060
16304
|
// src/web/commands/agent-commands.ts
|
|
16061
16305
|
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"}
|
|
16306
|
+
const sub = args[0]?.toLowerCase() ?? "status";
|
|
16307
|
+
if (sub === "on") {
|
|
16308
|
+
ctx.toolExecutor.sessionAutoMode = true;
|
|
16309
|
+
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." });
|
|
16310
|
+
} else if (sub === "off") {
|
|
16311
|
+
ctx.toolExecutor.sessionAutoMode = false;
|
|
16312
|
+
ctx.send({ type: "info", message: "Auto Mode disabled." });
|
|
16313
|
+
} else if (sub === "status") {
|
|
16314
|
+
ctx.send({ type: "info", message: `Auto Mode: ${ctx.toolExecutor.sessionAutoMode ? "on" : "off"}
|
|
16072
16315
|
Scope: current session only
|
|
16073
16316
|
Denied: /permissions recently-denied` });
|
|
16074
|
-
|
|
16075
|
-
|
|
16076
|
-
}
|
|
16077
|
-
ctx.sendStatus();
|
|
16317
|
+
} else {
|
|
16318
|
+
ctx.send({ type: "error", message: "Usage: /auto [on|off|status]" });
|
|
16078
16319
|
}
|
|
16320
|
+
ctx.sendStatus();
|
|
16079
16321
|
}
|
|
16080
16322
|
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." });
|
|
16323
|
+
const sub = args[0]?.toLowerCase() ?? "recently-denied";
|
|
16324
|
+
if (sub === "recently-denied") {
|
|
16325
|
+
const rows = getRecentlyDeniedAutoActions();
|
|
16326
|
+
if (rows.length === 0) {
|
|
16327
|
+
ctx.send({ type: "info", message: "No recently denied Auto Mode actions." });
|
|
16099
16328
|
} else {
|
|
16100
|
-
|
|
16329
|
+
const lines = [`Recently denied Auto Mode actions (${rows.length})`, ""];
|
|
16330
|
+
for (const r of rows.slice(0, 20)) {
|
|
16331
|
+
lines.push(`- ${r.timestamp} ${r.tool} ${r.ruleId}`);
|
|
16332
|
+
lines.push(` ${r.reason}`);
|
|
16333
|
+
lines.push(` ${r.argsPreview}`);
|
|
16334
|
+
}
|
|
16335
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
16101
16336
|
}
|
|
16337
|
+
} else if (sub === "clear-denied") {
|
|
16338
|
+
clearRecentlyDeniedAutoActions();
|
|
16339
|
+
ctx.send({ type: "info", message: "Cleared recently denied Auto Mode actions." });
|
|
16340
|
+
} else {
|
|
16341
|
+
ctx.send({ type: "error", message: "Usage: /permissions recently-denied|clear-denied" });
|
|
16102
16342
|
}
|
|
16103
16343
|
}
|
|
16104
16344
|
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
|
-
}
|
|
16345
|
+
const sub = args[0]?.toLowerCase();
|
|
16346
|
+
if (sub === "off") {
|
|
16347
|
+
ctx.toolExecutor.sessionAutoApprove = false;
|
|
16348
|
+
ctx.send({ type: "info", message: "\u{1F512} Auto-approve disabled \u2014 confirmations restored for this session." });
|
|
16349
|
+
} else {
|
|
16350
|
+
ctx.toolExecutor.sessionAutoApprove = true;
|
|
16351
|
+
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
16352
|
}
|
|
16115
16353
|
}
|
|
16116
16354
|
async function handleAgent(args, ctx) {
|
|
16117
|
-
|
|
16118
|
-
|
|
16119
|
-
|
|
16120
|
-
|
|
16121
|
-
|
|
16122
|
-
|
|
16123
|
-
|
|
16124
|
-
|
|
16125
|
-
|
|
16126
|
-
|
|
16127
|
-
|
|
16128
|
-
|
|
16129
|
-
|
|
16355
|
+
const sub = args[0]?.toLowerCase() ?? "summary";
|
|
16356
|
+
if (sub === "list") {
|
|
16357
|
+
const agents = listAgentConfigs(ctx.config.getConfigDir());
|
|
16358
|
+
const lines = [`Agents (${agents.length})`, ""];
|
|
16359
|
+
for (const agent of agents) {
|
|
16360
|
+
const active = agent.name === getPreferredAgentName() ? " *" : " ";
|
|
16361
|
+
const model = [agent.provider, agent.model].filter(Boolean).join("/") || "inherit";
|
|
16362
|
+
lines.push(`${active} ${agent.name} [${agent.source ?? "builtin"}] \u2014 ${agent.description ?? ""}`);
|
|
16363
|
+
lines.push(` model: ${model} \xB7 permission: ${agent.permissionProfile ?? "workspace-write"} \xB7 maxRounds: ${agent.maxToolRounds ?? "inherit"}`);
|
|
16364
|
+
}
|
|
16365
|
+
lines.push("", "Config dirs: ~/.aicli/agents/ and .aicli/agents/");
|
|
16366
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
16367
|
+
return;
|
|
16368
|
+
}
|
|
16369
|
+
if (sub === "switch") {
|
|
16370
|
+
const name = args[1];
|
|
16371
|
+
if (!name) {
|
|
16372
|
+
ctx.send({ type: "error", message: "Usage: /agent switch <name>" });
|
|
16373
|
+
return;
|
|
16130
16374
|
}
|
|
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}
|
|
16375
|
+
const agent = resolveAgentConfig(name, ctx.config.getConfigDir());
|
|
16376
|
+
if (!agent) {
|
|
16377
|
+
const available = listAgentConfigs(ctx.config.getConfigDir()).map((a) => a.name).join(", ");
|
|
16378
|
+
ctx.send({ type: "error", message: `Unknown agent: ${name}
|
|
16140
16379
|
Available: ${available}` });
|
|
16141
|
-
|
|
16142
|
-
setPreferredAgentName(agent.name);
|
|
16143
|
-
ctx.send({ type: "info", message: `Preferred spawn_agent role: ${agent.name}` });
|
|
16144
|
-
ctx.sendStatus();
|
|
16380
|
+
return;
|
|
16145
16381
|
}
|
|
16146
|
-
|
|
16147
|
-
|
|
16148
|
-
|
|
16149
|
-
|
|
16150
|
-
|
|
16382
|
+
setPreferredAgentName(agent.name);
|
|
16383
|
+
ctx.send({ type: "info", message: `Preferred spawn_agent role: ${agent.name}` });
|
|
16384
|
+
ctx.sendStatus();
|
|
16385
|
+
return;
|
|
16386
|
+
}
|
|
16387
|
+
if (sub === "stop") {
|
|
16388
|
+
const target = args[1] ?? "all";
|
|
16389
|
+
const count = requestAgentStop(target);
|
|
16390
|
+
ctx.send({ type: "info", message: count > 0 ? `Stop requested for ${count} running agent(s).` : "No matching running agents." });
|
|
16391
|
+
ctx.sendStatus();
|
|
16392
|
+
return;
|
|
16393
|
+
}
|
|
16394
|
+
if (sub === "summary") {
|
|
16395
|
+
const runs2 = listAgentRuns();
|
|
16396
|
+
if (runs2.length === 0) {
|
|
16397
|
+
ctx.send({ type: "info", message: "No sub-agent runs yet." });
|
|
16398
|
+
return;
|
|
16151
16399
|
}
|
|
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") });
|
|
16400
|
+
const lines = [`Recent agent runs (${runs2.length})`, ""];
|
|
16401
|
+
for (const run of runs2.slice(0, 20)) {
|
|
16402
|
+
lines.push(`${run.id} ${run.status} ${run.agentName} ${run.startedAt}`);
|
|
16403
|
+
lines.push(` task: ${run.task.slice(0, 120)}${run.task.length > 120 ? "..." : ""}`);
|
|
16404
|
+
if (run.toolNames.length > 0) lines.push(` tools: ${run.toolNames.join(", ")}`);
|
|
16405
|
+
if (run.summary) lines.push(` summary: ${run.summary.slice(0, 180)}${run.summary.length > 180 ? "..." : ""}`);
|
|
16406
|
+
if (run.error) lines.push(` error: ${run.error}`);
|
|
16166
16407
|
}
|
|
16167
|
-
ctx.send({ type: "
|
|
16408
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
16409
|
+
return;
|
|
16168
16410
|
}
|
|
16411
|
+
ctx.send({ type: "error", message: "Usage: /agent list|switch <name>|stop <id|all>|summary" });
|
|
16169
16412
|
}
|
|
16170
16413
|
|
|
16171
16414
|
// src/web/commands/misc-commands.ts
|
|
16172
|
-
async function handleHelp(
|
|
16415
|
+
async function handleHelp(_args, ctx) {
|
|
16173
16416
|
ctx.send({
|
|
16174
16417
|
type: "info",
|
|
16175
16418
|
message: [
|
|
@@ -16227,90 +16470,87 @@ async function handleHelp(args, ctx) {
|
|
|
16227
16470
|
].join("\n")
|
|
16228
16471
|
});
|
|
16229
16472
|
}
|
|
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
|
-
}
|
|
16473
|
+
async function handleAbout(_args, ctx) {
|
|
16474
|
+
const providerList = ctx.providers.listAll();
|
|
16475
|
+
const toolCount = ctx.toolRegistry.getDefinitions().length;
|
|
16476
|
+
const mcpTools = ctx.mcpManager?.getTotalToolCount() ?? 0;
|
|
16477
|
+
const lines = [
|
|
16478
|
+
`\u{1F916} **ai-cli** v${VERSION}`,
|
|
16479
|
+
"",
|
|
16480
|
+
`${DESCRIPTION}`,
|
|
16481
|
+
`Author: ${AUTHOR} <${AUTHOR_EMAIL}>`,
|
|
16482
|
+
"",
|
|
16483
|
+
`**Providers:** ${providerList.length} (${providerList.map((p) => p.id).join(", ")})`,
|
|
16484
|
+
`**Tools:** ${toolCount + mcpTools} (${toolCount} built-in${mcpTools > 0 ? ` + ${mcpTools} MCP` : ""})`,
|
|
16485
|
+
`**REPL Commands:** 37`,
|
|
16486
|
+
"",
|
|
16487
|
+
`GitHub: https://github.com/jinzhengdong/ai-cli`
|
|
16488
|
+
];
|
|
16489
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
16249
16490
|
}
|
|
16250
16491
|
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("");
|
|
16492
|
+
const query = args.join(" ").trim();
|
|
16493
|
+
if (!query) {
|
|
16494
|
+
ctx.send({ type: "error", message: "Usage: /search <keyword>" });
|
|
16495
|
+
return;
|
|
16496
|
+
}
|
|
16497
|
+
const results = ctx.sessions.searchMessages(query);
|
|
16498
|
+
if (results.length === 0) {
|
|
16499
|
+
ctx.send({ type: "info", message: `No sessions found containing "${query}".` });
|
|
16500
|
+
return;
|
|
16501
|
+
}
|
|
16502
|
+
const lines = [`\u{1F50D} Found ${results.length} session(s) containing "${query}"`, ""];
|
|
16503
|
+
for (const r of results) {
|
|
16504
|
+
const { sessionMeta, matches } = r;
|
|
16505
|
+
const dateStr = sessionMeta.updated instanceof Date ? sessionMeta.updated.toLocaleDateString() : String(sessionMeta.updated);
|
|
16506
|
+
lines.push(`**${sessionMeta.id.slice(0, 8)}** [${dateStr}] ${sessionMeta.provider} / ${sessionMeta.model}`);
|
|
16507
|
+
if (sessionMeta.title) lines.push(` ${sessionMeta.title}`);
|
|
16508
|
+
for (const m of matches) {
|
|
16509
|
+
const icon = m.role === "user" ? "\u{1F464}" : "\u{1F916}";
|
|
16510
|
+
lines.push(` ${icon} ${m.snippet}`);
|
|
16271
16511
|
}
|
|
16272
|
-
lines.push("
|
|
16273
|
-
ctx.send({ type: "info", message: lines.join("\n") });
|
|
16512
|
+
lines.push("");
|
|
16274
16513
|
}
|
|
16514
|
+
lines.push("Use /session load <id> to resume a session.");
|
|
16515
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
16275
16516
|
}
|
|
16276
16517
|
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);
|
|
16518
|
+
const sub = args.join(" ").trim();
|
|
16519
|
+
if (sub === "list") {
|
|
16520
|
+
const history = undoStack.getHistory();
|
|
16521
|
+
if (history.length === 0) {
|
|
16522
|
+
ctx.send({ type: "info", message: "Undo stack is empty." });
|
|
16523
|
+
return;
|
|
16307
16524
|
}
|
|
16308
|
-
|
|
16309
|
-
|
|
16310
|
-
|
|
16311
|
-
|
|
16525
|
+
const lines = [`\u{1F4CB} Undo Stack (${history.length} entries, newest last):`, ""];
|
|
16526
|
+
history.forEach((entry, i) => {
|
|
16527
|
+
const timeStr = entry.timestamp.toLocaleTimeString();
|
|
16528
|
+
const typeTag = entry.isDirectory ? "[dir]" : entry.previousContent === null ? "[new]" : "[mod]";
|
|
16529
|
+
lines.push(` ${String(i + 1).padStart(3)} ${typeTag} ${entry.description} ${timeStr}`);
|
|
16530
|
+
});
|
|
16531
|
+
ctx.send({ type: "info", message: lines.join("\n") });
|
|
16532
|
+
return;
|
|
16533
|
+
}
|
|
16534
|
+
const n = sub ? parseInt(sub, 10) : 1;
|
|
16535
|
+
if (isNaN(n) || n < 1) {
|
|
16536
|
+
ctx.send({ type: "info", message: "Usage: /undo [list | <n>]" });
|
|
16537
|
+
return;
|
|
16538
|
+
}
|
|
16539
|
+
if (undoStack.depth === 0) {
|
|
16540
|
+
ctx.send({ type: "info", message: "Nothing to undo." });
|
|
16541
|
+
return;
|
|
16542
|
+
}
|
|
16543
|
+
const count = Math.min(n, undoStack.depth);
|
|
16544
|
+
const undoResults = [];
|
|
16545
|
+
for (let i = 0; i < count; i++) {
|
|
16546
|
+
const undoResult = undoStack.undo();
|
|
16547
|
+
if (undoResult) undoResults.push(undoResult.result);
|
|
16548
|
+
}
|
|
16549
|
+
if (undoResults.length === 0) {
|
|
16550
|
+
ctx.send({ type: "info", message: "Nothing to undo." });
|
|
16551
|
+
} else {
|
|
16552
|
+
ctx.send({ type: "info", message: `\u2713 ${undoResults.length} operation(s) undone:
|
|
16312
16553
|
${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
16313
|
-
}
|
|
16314
16554
|
}
|
|
16315
16555
|
}
|
|
16316
16556
|
|
|
@@ -16359,20 +16599,6 @@ var webCommandHandlers = {
|
|
|
16359
16599
|
search: handleSearch,
|
|
16360
16600
|
undo: handleUndo
|
|
16361
16601
|
};
|
|
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
16602
|
var SessionHandler = class {
|
|
16377
16603
|
ws;
|
|
16378
16604
|
config;
|
|
@@ -16415,6 +16641,8 @@ var SessionHandler = class {
|
|
|
16415
16641
|
addedDirs = /* @__PURE__ */ new Set();
|
|
16416
16642
|
/** Track MCP tool names used across this session (for MCP budget prioritization) */
|
|
16417
16643
|
usedMcpToolNames = /* @__PURE__ */ new Set();
|
|
16644
|
+
/** P2-PERF-05: bash session key for cleanup on disconnect */
|
|
16645
|
+
_bashSessionKey = "";
|
|
16418
16646
|
/**
|
|
16419
16647
|
* 未保存会话的内存缓存(per-handler)。
|
|
16420
16648
|
* 当客户端通过 `session new` 创建新会话时,会话仅存在于 SessionManager._current 中,
|
|
@@ -16433,7 +16661,9 @@ var SessionHandler = class {
|
|
|
16433
16661
|
this.mcpManager = shared.mcpManager;
|
|
16434
16662
|
this.skillManager = shared.skillManager;
|
|
16435
16663
|
this.toolExecutor = new ToolExecutorWeb(shared.toolRegistry, ws);
|
|
16436
|
-
|
|
16664
|
+
const bashSessionKey = `web-${Math.random().toString(36).slice(2)}-${Date.now()}`;
|
|
16665
|
+
this.toolExecutor.setSessionKey(bashSessionKey);
|
|
16666
|
+
this._bashSessionKey = bashSessionKey;
|
|
16437
16667
|
this.currentProvider = this.config.get("defaultProvider");
|
|
16438
16668
|
const allDefaultModels = this.config.get("defaultModels");
|
|
16439
16669
|
try {
|
|
@@ -16551,10 +16781,10 @@ var SessionHandler = class {
|
|
|
16551
16781
|
return;
|
|
16552
16782
|
}
|
|
16553
16783
|
case "ask_user_response": {
|
|
16554
|
-
const
|
|
16555
|
-
if (
|
|
16784
|
+
const resolve14 = this.pendingAskUser.get(msg.requestId);
|
|
16785
|
+
if (resolve14) {
|
|
16556
16786
|
this.pendingAskUser.delete(msg.requestId);
|
|
16557
|
-
|
|
16787
|
+
resolve14(msg.answer);
|
|
16558
16788
|
}
|
|
16559
16789
|
return;
|
|
16560
16790
|
}
|
|
@@ -16567,10 +16797,10 @@ var SessionHandler = class {
|
|
|
16567
16797
|
case "memory_manage":
|
|
16568
16798
|
return this.handleMemoryManage(msg.action, msg.id, msg.expiresAt);
|
|
16569
16799
|
case "auto_pause_response": {
|
|
16570
|
-
const
|
|
16571
|
-
if (
|
|
16800
|
+
const resolve14 = this.pendingAutoPause.get(msg.requestId);
|
|
16801
|
+
if (resolve14) {
|
|
16572
16802
|
this.pendingAutoPause.delete(msg.requestId);
|
|
16573
|
-
|
|
16803
|
+
resolve14({ action: msg.action, message: msg.message });
|
|
16574
16804
|
}
|
|
16575
16805
|
return;
|
|
16576
16806
|
}
|
|
@@ -16585,10 +16815,10 @@ var SessionHandler = class {
|
|
|
16585
16815
|
this.hubOrchestrator?.abort();
|
|
16586
16816
|
return;
|
|
16587
16817
|
case "hub_steer": {
|
|
16588
|
-
const
|
|
16589
|
-
if (
|
|
16818
|
+
const resolve14 = this.pendingHubReview.get(msg.requestId);
|
|
16819
|
+
if (resolve14) {
|
|
16590
16820
|
this.pendingHubReview.delete(msg.requestId);
|
|
16591
|
-
|
|
16821
|
+
resolve14({ action: msg.action, message: msg.message });
|
|
16592
16822
|
}
|
|
16593
16823
|
return;
|
|
16594
16824
|
}
|
|
@@ -16601,11 +16831,12 @@ var SessionHandler = class {
|
|
|
16601
16831
|
runLifecycleHooks(mergePluginHooks(this.config.get("hooks") ?? void 0, this.config.getConfigDir()), "Stop", { sessionId: this.sessions.current?.id }, { configDir: this.config.getConfigDir() });
|
|
16602
16832
|
this.toolExecutor.cancelAll();
|
|
16603
16833
|
if (this.abortController) this.abortController.abort();
|
|
16604
|
-
for (const
|
|
16834
|
+
for (const resolve14 of this.pendingAskUser.values()) resolve14(null);
|
|
16605
16835
|
this.pendingAskUser.clear();
|
|
16606
|
-
for (const
|
|
16836
|
+
for (const resolve14 of this.pendingAutoPause.values()) resolve14({ action: "stop" });
|
|
16607
16837
|
this.pendingAutoPause.clear();
|
|
16608
16838
|
this.saveIfNeeded();
|
|
16839
|
+
if (this._bashSessionKey) clearBashSession(this._bashSessionKey);
|
|
16609
16840
|
}
|
|
16610
16841
|
/** 根据当前模型 context window 更新工具输出截断上限 */
|
|
16611
16842
|
updateContextWindow() {
|
|
@@ -16737,9 +16968,9 @@ var SessionHandler = class {
|
|
|
16737
16968
|
this.hubOrchestrator = orchestrator;
|
|
16738
16969
|
orchestrator.onEvent = (event) => this.send({ type: "hub_event", event });
|
|
16739
16970
|
if (config.humanSteer) {
|
|
16740
|
-
orchestrator.onRoundReview = ({ round, maxRounds }) => new Promise((
|
|
16971
|
+
orchestrator.onRoundReview = ({ round, maxRounds }) => new Promise((resolve14) => {
|
|
16741
16972
|
const requestId = `hubrev_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
16742
|
-
this.pendingHubReview.set(requestId,
|
|
16973
|
+
this.pendingHubReview.set(requestId, resolve14);
|
|
16743
16974
|
this.send({ type: "hub_review", requestId, round, maxRounds });
|
|
16744
16975
|
});
|
|
16745
16976
|
}
|
|
@@ -17072,7 +17303,7 @@ Try: /compact to reduce context, /clear to reset, or switch to a larger-context
|
|
|
17072
17303
|
this.send({ type: "response_done", content, usage });
|
|
17073
17304
|
if (pendingTeeSave && isCleanDocumentBody(content)) {
|
|
17074
17305
|
try {
|
|
17075
|
-
|
|
17306
|
+
mkdirSync16(dirname10(pendingTeeSave), { recursive: true });
|
|
17076
17307
|
const bodyToSave = stripOuterCodeFence(content);
|
|
17077
17308
|
atomicWriteFileSync(pendingTeeSave, bodyToSave);
|
|
17078
17309
|
undoStack.push(pendingTeeSave, `save_last_response (deferred): ${pendingTeeSave}`);
|
|
@@ -17097,8 +17328,8 @@ Try: /compact to reduce context, /clear to reset, or switch to a larger-context
|
|
|
17097
17328
|
onMcpToolUsed: (name) => this.usedMcpToolNames.add(name),
|
|
17098
17329
|
requestAutoPause: async ({ effectiveRound, maxToolRounds: totalRounds, toolSummary }) => {
|
|
17099
17330
|
const requestId = `pause_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
17100
|
-
const pauseResp = await new Promise((
|
|
17101
|
-
this.pendingAutoPause.set(requestId,
|
|
17331
|
+
const pauseResp = await new Promise((resolve14) => {
|
|
17332
|
+
this.pendingAutoPause.set(requestId, resolve14);
|
|
17102
17333
|
this.send({
|
|
17103
17334
|
type: "auto_pause_request",
|
|
17104
17335
|
requestId,
|
|
@@ -17248,8 +17479,8 @@ ${summaryContent}`,
|
|
|
17248
17479
|
let isError = false;
|
|
17249
17480
|
let summary;
|
|
17250
17481
|
try {
|
|
17251
|
-
|
|
17252
|
-
fileStream =
|
|
17482
|
+
mkdirSync16(dirname10(saveToFile), { recursive: true });
|
|
17483
|
+
fileStream = createWriteStream3(saveToFile);
|
|
17253
17484
|
const teeSystemPrompt = stripToolCallReminder(systemPrompt ?? "") + CONTENT_ONLY_STREAM_REMINDER;
|
|
17254
17485
|
const teeExtraMessages = extraMessages.length > 0 ? [...extraMessages, { role: "user", content: TEE_FINAL_USER_NUDGE }] : [{ role: "user", content: TEE_FINAL_USER_NUDGE }];
|
|
17255
17486
|
const chatRequest = {
|
|
@@ -17277,8 +17508,8 @@ ${summaryContent}`,
|
|
|
17277
17508
|
}
|
|
17278
17509
|
if (chunk.done) break;
|
|
17279
17510
|
}
|
|
17280
|
-
await new Promise((
|
|
17281
|
-
fileStream.end((err) => err ? reject(err) :
|
|
17511
|
+
await new Promise((resolve14, reject) => {
|
|
17512
|
+
fileStream.end((err) => err ? reject(err) : resolve14());
|
|
17282
17513
|
});
|
|
17283
17514
|
const verdict = evaluateTeeContent(fullContent, saveToFile, priorContent);
|
|
17284
17515
|
if (verdict.kind === "reject") {
|
|
@@ -17304,7 +17535,7 @@ ${summaryContent}`,
|
|
|
17304
17535
|
} catch (err) {
|
|
17305
17536
|
if (fileStream) {
|
|
17306
17537
|
try {
|
|
17307
|
-
await new Promise((
|
|
17538
|
+
await new Promise((resolve14) => fileStream.end(() => resolve14()));
|
|
17308
17539
|
} catch {
|
|
17309
17540
|
}
|
|
17310
17541
|
}
|
|
@@ -17541,7 +17772,7 @@ ${entry.content}`;
|
|
|
17541
17772
|
const configDir = this.config.getConfigDir();
|
|
17542
17773
|
try {
|
|
17543
17774
|
atomicWriteFileSync(memoryStorePath(configDir), "");
|
|
17544
|
-
atomicWriteFileSync(
|
|
17775
|
+
atomicWriteFileSync(join26(configDir, MEMORY_FILE_NAME), "");
|
|
17545
17776
|
this.send({ type: "info", message: "\u{1F5D1}\uFE0F Persistent memory cleared." });
|
|
17546
17777
|
this.sendMemoryEntries();
|
|
17547
17778
|
} catch (err) {
|
|
@@ -17607,7 +17838,7 @@ ${entry.content}`;
|
|
|
17607
17838
|
return;
|
|
17608
17839
|
}
|
|
17609
17840
|
try {
|
|
17610
|
-
const { searchChatMemory: searchChatMemory2, loadChatIndex: loadChatIndex2 } = await import("./chat-index-
|
|
17841
|
+
const { searchChatMemory: searchChatMemory2, loadChatIndex: loadChatIndex2 } = await import("./chat-index-WSI7ULRQ.js");
|
|
17611
17842
|
const loaded = loadChatIndex2();
|
|
17612
17843
|
if (!loaded || loaded.idx.chunks.length === 0) {
|
|
17613
17844
|
this.send({ type: "memory_hits", query: q, hits: [], indexMissing: true });
|
|
@@ -17643,7 +17874,7 @@ ${entry.content}`;
|
|
|
17643
17874
|
}
|
|
17644
17875
|
async handleMemoryStatus() {
|
|
17645
17876
|
try {
|
|
17646
|
-
const { getChatIndexStatus } = await import("./chat-index-
|
|
17877
|
+
const { getChatIndexStatus } = await import("./chat-index-WSI7ULRQ.js");
|
|
17647
17878
|
const s = getChatIndexStatus();
|
|
17648
17879
|
this.send({
|
|
17649
17880
|
type: "memory_status",
|
|
@@ -17668,7 +17899,7 @@ ${entry.content}`;
|
|
|
17668
17899
|
type: "info",
|
|
17669
17900
|
message: full ? "\u{1F9E0} Rebuilding chat memory index (this may take a while on first run \u2014 ~117 MB embedder)." : "\u{1F9E0} Refreshing chat memory index (incremental)\u2026"
|
|
17670
17901
|
});
|
|
17671
|
-
const { buildChatIndex } = await import("./chat-index-
|
|
17902
|
+
const { buildChatIndex } = await import("./chat-index-WSI7ULRQ.js");
|
|
17672
17903
|
const stats = await buildChatIndex({
|
|
17673
17904
|
full,
|
|
17674
17905
|
onProgress: (p) => {
|
|
@@ -17823,8 +18054,8 @@ async function setupProxy(configProxy) {
|
|
|
17823
18054
|
}
|
|
17824
18055
|
|
|
17825
18056
|
// src/web/auth.ts
|
|
17826
|
-
import { existsSync as
|
|
17827
|
-
import { join as
|
|
18057
|
+
import { existsSync as existsSync32, readFileSync as readFileSync25, writeFileSync as writeFileSync6, mkdirSync as mkdirSync17, readdirSync as readdirSync16, copyFileSync } from "fs";
|
|
18058
|
+
import { join as join27 } from "path";
|
|
17828
18059
|
import { createHmac, randomBytes, timingSafeEqual, pbkdf2Sync } from "crypto";
|
|
17829
18060
|
var USERS_FILE = "users.json";
|
|
17830
18061
|
var TOKEN_EXPIRY_HOURS = 24;
|
|
@@ -17839,7 +18070,7 @@ var AuthManager = class {
|
|
|
17839
18070
|
db;
|
|
17840
18071
|
constructor(baseDir) {
|
|
17841
18072
|
this.baseDir = baseDir;
|
|
17842
|
-
this.usersFile =
|
|
18073
|
+
this.usersFile = join27(baseDir, USERS_FILE);
|
|
17843
18074
|
this.db = this.loadOrCreate();
|
|
17844
18075
|
}
|
|
17845
18076
|
// ── Public API ─────────────────────────────────────────────────
|
|
@@ -17868,9 +18099,9 @@ var AuthManager = class {
|
|
|
17868
18099
|
}
|
|
17869
18100
|
const salt = randomBytes(16).toString("hex");
|
|
17870
18101
|
const passwordHash = this.hashPassword(password, salt);
|
|
17871
|
-
const dataDir =
|
|
17872
|
-
const fullDataDir =
|
|
17873
|
-
|
|
18102
|
+
const dataDir = join27(USERS_DIR, username);
|
|
18103
|
+
const fullDataDir = join27(this.baseDir, dataDir);
|
|
18104
|
+
mkdirSync17(join27(fullDataDir, "history"), { recursive: true });
|
|
17874
18105
|
const user = {
|
|
17875
18106
|
username,
|
|
17876
18107
|
passwordHash,
|
|
@@ -17985,7 +18216,7 @@ var AuthManager = class {
|
|
|
17985
18216
|
getUserDataDir(username) {
|
|
17986
18217
|
const user = this.db.users.find((u) => u.username === username);
|
|
17987
18218
|
if (!user) throw new Error(`User not found: ${username}`);
|
|
17988
|
-
return
|
|
18219
|
+
return join27(this.baseDir, user.dataDir);
|
|
17989
18220
|
}
|
|
17990
18221
|
/** List all usernames */
|
|
17991
18222
|
listUsers() {
|
|
@@ -18021,30 +18252,30 @@ var AuthManager = class {
|
|
|
18021
18252
|
const err = this.register(username, password);
|
|
18022
18253
|
if (err) return err;
|
|
18023
18254
|
const userDir = this.getUserDataDir(username);
|
|
18024
|
-
const globalConfig =
|
|
18025
|
-
if (
|
|
18255
|
+
const globalConfig = join27(this.baseDir, "config.json");
|
|
18256
|
+
if (existsSync32(globalConfig)) {
|
|
18026
18257
|
try {
|
|
18027
|
-
const content =
|
|
18028
|
-
|
|
18258
|
+
const content = readFileSync25(globalConfig, "utf-8");
|
|
18259
|
+
writeFileSync6(join27(userDir, "config.json"), content, "utf-8");
|
|
18029
18260
|
} catch {
|
|
18030
18261
|
}
|
|
18031
18262
|
}
|
|
18032
|
-
const globalMemory =
|
|
18033
|
-
if (
|
|
18263
|
+
const globalMemory = join27(this.baseDir, "memory.md");
|
|
18264
|
+
if (existsSync32(globalMemory)) {
|
|
18034
18265
|
try {
|
|
18035
|
-
const content =
|
|
18036
|
-
|
|
18266
|
+
const content = readFileSync25(globalMemory, "utf-8");
|
|
18267
|
+
writeFileSync6(join27(userDir, "memory.md"), content, "utf-8");
|
|
18037
18268
|
} catch {
|
|
18038
18269
|
}
|
|
18039
18270
|
}
|
|
18040
|
-
const globalHistory =
|
|
18041
|
-
if (
|
|
18271
|
+
const globalHistory = join27(this.baseDir, "history");
|
|
18272
|
+
if (existsSync32(globalHistory)) {
|
|
18042
18273
|
try {
|
|
18043
|
-
const files =
|
|
18044
|
-
const userHistory =
|
|
18274
|
+
const files = readdirSync16(globalHistory).filter((f) => f.endsWith(".json"));
|
|
18275
|
+
const userHistory = join27(userDir, "history");
|
|
18045
18276
|
for (const f of files) {
|
|
18046
18277
|
try {
|
|
18047
|
-
copyFileSync(
|
|
18278
|
+
copyFileSync(join27(globalHistory, f), join27(userHistory, f));
|
|
18048
18279
|
} catch {
|
|
18049
18280
|
}
|
|
18050
18281
|
}
|
|
@@ -18055,9 +18286,9 @@ var AuthManager = class {
|
|
|
18055
18286
|
}
|
|
18056
18287
|
// ── Private methods ────────────────────────────────────────────
|
|
18057
18288
|
loadOrCreate() {
|
|
18058
|
-
if (
|
|
18289
|
+
if (existsSync32(this.usersFile)) {
|
|
18059
18290
|
try {
|
|
18060
|
-
return JSON.parse(
|
|
18291
|
+
return JSON.parse(readFileSync25(this.usersFile, "utf-8"));
|
|
18061
18292
|
} catch {
|
|
18062
18293
|
}
|
|
18063
18294
|
}
|
|
@@ -18073,7 +18304,7 @@ var AuthManager = class {
|
|
|
18073
18304
|
this.saveDB(this.db);
|
|
18074
18305
|
}
|
|
18075
18306
|
saveDB(db) {
|
|
18076
|
-
|
|
18307
|
+
mkdirSync17(this.baseDir, { recursive: true });
|
|
18077
18308
|
atomicWriteFileSync(this.usersFile, JSON.stringify(db, null, 2));
|
|
18078
18309
|
}
|
|
18079
18310
|
/** Legacy hash — kept only for migrating old users (v0.2.x) */
|
|
@@ -18105,11 +18336,11 @@ function getModuleDir() {
|
|
|
18105
18336
|
if (typeof import.meta?.url === "string") {
|
|
18106
18337
|
const url = new URL(import.meta.url);
|
|
18107
18338
|
const filePath = url.pathname.replace(/^\/([A-Z]:)/i, "$1");
|
|
18108
|
-
return
|
|
18339
|
+
return dirname11(filePath);
|
|
18109
18340
|
}
|
|
18110
18341
|
} catch {
|
|
18111
18342
|
}
|
|
18112
|
-
return
|
|
18343
|
+
return resolve13(".");
|
|
18113
18344
|
}
|
|
18114
18345
|
async function startWebServer(options = {}) {
|
|
18115
18346
|
const port = options.port ?? 3e3;
|
|
@@ -18177,8 +18408,8 @@ async function startWebServer(options = {}) {
|
|
|
18177
18408
|
}
|
|
18178
18409
|
}
|
|
18179
18410
|
let skillManager = null;
|
|
18180
|
-
const skillsDir =
|
|
18181
|
-
if (
|
|
18411
|
+
const skillsDir = join28(config.getConfigDir(), SKILLS_DIR_NAME);
|
|
18412
|
+
if (existsSync33(skillsDir)) {
|
|
18182
18413
|
skillManager = new SkillManager(skillsDir, config.get("ui").skillSizeWarn);
|
|
18183
18414
|
skillManager.loadSkills(pluginAssets.skillDirs);
|
|
18184
18415
|
const count = skillManager.listSkills().length;
|
|
@@ -18249,18 +18480,18 @@ async function startWebServer(options = {}) {
|
|
|
18249
18480
|
next();
|
|
18250
18481
|
};
|
|
18251
18482
|
const moduleDir = getModuleDir();
|
|
18252
|
-
let clientDir =
|
|
18253
|
-
if (!
|
|
18254
|
-
clientDir =
|
|
18483
|
+
let clientDir = join28(moduleDir, "web", "client");
|
|
18484
|
+
if (!existsSync33(clientDir)) {
|
|
18485
|
+
clientDir = join28(moduleDir, "client");
|
|
18255
18486
|
}
|
|
18256
|
-
if (!
|
|
18257
|
-
clientDir =
|
|
18487
|
+
if (!existsSync33(clientDir)) {
|
|
18488
|
+
clientDir = join28(moduleDir, "..", "..", "src", "web", "client");
|
|
18258
18489
|
}
|
|
18259
|
-
if (!
|
|
18260
|
-
clientDir =
|
|
18490
|
+
if (!existsSync33(clientDir)) {
|
|
18491
|
+
clientDir = join28(process.cwd(), "src", "web", "client");
|
|
18261
18492
|
}
|
|
18262
18493
|
console.log(` Static files: ${clientDir}`);
|
|
18263
|
-
app.use("/vendor", express.static(
|
|
18494
|
+
app.use("/vendor", express.static(join28(clientDir, "vendor"), { maxAge: "1y", immutable: true }));
|
|
18264
18495
|
app.use(express.static(clientDir));
|
|
18265
18496
|
app.get("/api/status", (_req, res) => {
|
|
18266
18497
|
res.json({
|
|
@@ -18327,10 +18558,10 @@ async function startWebServer(options = {}) {
|
|
|
18327
18558
|
app.get("/api/files", requireAuth, (req, res) => {
|
|
18328
18559
|
const cwd = process.cwd();
|
|
18329
18560
|
const prefix = req.query.prefix || "";
|
|
18330
|
-
const targetDir =
|
|
18561
|
+
const targetDir = join28(cwd, prefix);
|
|
18331
18562
|
try {
|
|
18332
|
-
const canonicalTarget = realpathSync(
|
|
18333
|
-
const canonicalCwd = realpathSync(
|
|
18563
|
+
const canonicalTarget = realpathSync(resolve13(targetDir));
|
|
18564
|
+
const canonicalCwd = realpathSync(resolve13(cwd));
|
|
18334
18565
|
if (!canonicalTarget.startsWith(canonicalCwd + sep3) && canonicalTarget !== canonicalCwd) {
|
|
18335
18566
|
res.json({ files: [] });
|
|
18336
18567
|
return;
|
|
@@ -18341,10 +18572,10 @@ async function startWebServer(options = {}) {
|
|
|
18341
18572
|
}
|
|
18342
18573
|
try {
|
|
18343
18574
|
const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "dist-cjs", "release", "__pycache__", ".next", ".nuxt", "coverage", ".cache"]);
|
|
18344
|
-
const entries =
|
|
18575
|
+
const entries = readdirSync17(targetDir, { withFileTypes: true });
|
|
18345
18576
|
const files = entries.filter((e) => !SKIP.has(e.name) && !e.name.startsWith(".")).slice(0, 50).map((e) => ({
|
|
18346
18577
|
name: e.name,
|
|
18347
|
-
path: relative4(cwd,
|
|
18578
|
+
path: relative4(cwd, join28(targetDir, e.name)).replace(/\\/g, "/"),
|
|
18348
18579
|
isDir: e.isDirectory()
|
|
18349
18580
|
}));
|
|
18350
18581
|
res.json({ files });
|
|
@@ -18384,8 +18615,8 @@ async function startWebServer(options = {}) {
|
|
|
18384
18615
|
try {
|
|
18385
18616
|
const authUser = req._authUser;
|
|
18386
18617
|
const histDir = authUser ? getUserShared(authUser).config.getHistoryDir() : config.getHistoryDir();
|
|
18387
|
-
const filePath =
|
|
18388
|
-
if (!
|
|
18618
|
+
const filePath = join28(histDir, `${id}.json`);
|
|
18619
|
+
if (!existsSync33(filePath)) {
|
|
18389
18620
|
res.status(404).json({ error: "Session not found" });
|
|
18390
18621
|
return;
|
|
18391
18622
|
}
|
|
@@ -18400,7 +18631,7 @@ async function startWebServer(options = {}) {
|
|
|
18400
18631
|
res.status(404).json({ error: "Session not found" });
|
|
18401
18632
|
return;
|
|
18402
18633
|
}
|
|
18403
|
-
const data = JSON.parse(
|
|
18634
|
+
const data = JSON.parse(readFileSync26(filePath, "utf-8"));
|
|
18404
18635
|
res.json({ session: data });
|
|
18405
18636
|
} catch (err) {
|
|
18406
18637
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
@@ -18413,10 +18644,10 @@ async function startWebServer(options = {}) {
|
|
|
18413
18644
|
return;
|
|
18414
18645
|
}
|
|
18415
18646
|
const cwd = process.cwd();
|
|
18416
|
-
const fullPath =
|
|
18647
|
+
const fullPath = resolve13(join28(cwd, filePath));
|
|
18417
18648
|
try {
|
|
18418
18649
|
const canonicalFull = realpathSync(fullPath);
|
|
18419
|
-
const canonicalCwd = realpathSync(
|
|
18650
|
+
const canonicalCwd = realpathSync(resolve13(cwd));
|
|
18420
18651
|
if (!canonicalFull.startsWith(canonicalCwd + sep3) && canonicalFull !== canonicalCwd) {
|
|
18421
18652
|
res.json({ error: "Access denied" });
|
|
18422
18653
|
return;
|
|
@@ -18426,12 +18657,12 @@ async function startWebServer(options = {}) {
|
|
|
18426
18657
|
return;
|
|
18427
18658
|
}
|
|
18428
18659
|
try {
|
|
18429
|
-
const stat =
|
|
18660
|
+
const stat = statSync15(fullPath);
|
|
18430
18661
|
if (stat.size > 512 * 1024) {
|
|
18431
18662
|
res.json({ error: `File too large (${(stat.size / 1024).toFixed(0)} KB, max 512 KB)` });
|
|
18432
18663
|
return;
|
|
18433
18664
|
}
|
|
18434
|
-
const content =
|
|
18665
|
+
const content = readFileSync26(fullPath, "utf-8");
|
|
18435
18666
|
res.json({ content, size: stat.size });
|
|
18436
18667
|
} catch {
|
|
18437
18668
|
res.json({ error: "Cannot read file" });
|
|
@@ -18635,7 +18866,7 @@ async function startWebServer(options = {}) {
|
|
|
18635
18866
|
});
|
|
18636
18867
|
const MAX_PORT_ATTEMPTS = 10;
|
|
18637
18868
|
let actualPort = port;
|
|
18638
|
-
const result = await new Promise((
|
|
18869
|
+
const result = await new Promise((resolve14, reject) => {
|
|
18639
18870
|
const tryListen = (attempt) => {
|
|
18640
18871
|
server.once("error", (err) => {
|
|
18641
18872
|
if (err.code === "EADDRINUSE" && attempt < MAX_PORT_ATTEMPTS) {
|
|
@@ -18666,7 +18897,7 @@ async function startWebServer(options = {}) {
|
|
|
18666
18897
|
}
|
|
18667
18898
|
console.log(` Press Ctrl+C to stop
|
|
18668
18899
|
`);
|
|
18669
|
-
|
|
18900
|
+
resolve14({ port: actualPort, host, url });
|
|
18670
18901
|
});
|
|
18671
18902
|
};
|
|
18672
18903
|
tryListen(1);
|
|
@@ -18686,17 +18917,17 @@ function resolveProjectMcpPath() {
|
|
|
18686
18917
|
const cwd = process.cwd();
|
|
18687
18918
|
const gitRoot = getGitRoot(cwd);
|
|
18688
18919
|
const projectRoot = gitRoot ?? cwd;
|
|
18689
|
-
const configPath =
|
|
18690
|
-
return
|
|
18920
|
+
const configPath = join28(projectRoot, MCP_PROJECT_CONFIG_NAME);
|
|
18921
|
+
return existsSync33(configPath) ? configPath : null;
|
|
18691
18922
|
}
|
|
18692
18923
|
function loadProjectMcpConfig() {
|
|
18693
18924
|
const cwd = process.cwd();
|
|
18694
18925
|
const gitRoot = getGitRoot(cwd);
|
|
18695
18926
|
const projectRoot = gitRoot ?? cwd;
|
|
18696
|
-
const configPath =
|
|
18697
|
-
if (!
|
|
18927
|
+
const configPath = join28(projectRoot, MCP_PROJECT_CONFIG_NAME);
|
|
18928
|
+
if (!existsSync33(configPath)) return null;
|
|
18698
18929
|
try {
|
|
18699
|
-
const raw = JSON.parse(
|
|
18930
|
+
const raw = JSON.parse(readFileSync26(configPath, "utf-8"));
|
|
18700
18931
|
return raw.mcpServers ?? raw;
|
|
18701
18932
|
} catch {
|
|
18702
18933
|
return null;
|
|
@@ -18733,13 +18964,13 @@ function parseCookie(cookie) {
|
|
|
18733
18964
|
}
|
|
18734
18965
|
async function openBrowser(url) {
|
|
18735
18966
|
try {
|
|
18736
|
-
const { spawn:
|
|
18967
|
+
const { spawn: spawn6 } = await import("child_process");
|
|
18737
18968
|
if (process.platform === "win32") {
|
|
18738
|
-
|
|
18969
|
+
spawn6("cmd", ["/c", "start", '""', url], { detached: true, stdio: "ignore" }).unref();
|
|
18739
18970
|
} else if (process.platform === "darwin") {
|
|
18740
|
-
|
|
18971
|
+
spawn6("open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
18741
18972
|
} else {
|
|
18742
|
-
|
|
18973
|
+
spawn6("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
|
|
18743
18974
|
}
|
|
18744
18975
|
} catch {
|
|
18745
18976
|
}
|