jinzd-ai-cli 0.2.26 → 0.2.28
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/{chunk-BSUW2USA.js → chunk-PVP4QYKG.js} +91 -91
- package/dist/chunk-RMEUZON4.js +468 -0
- package/dist/{chunk-3PBMD3H6.js → chunk-STOS2HXS.js} +1 -1
- package/dist/index.js +5 -4
- package/dist/{run-tests-GPPAKSCT.js → run-tests-6XMNZEGY.js} +1 -1
- package/dist/run-tests-IICMDEPJ.js +8 -0
- package/dist/{server-HSGK6BPN.js → server-AVYHN2F5.js} +609 -9
- package/package.json +1 -1
|
@@ -11,8 +11,10 @@ import {
|
|
|
11
11
|
askUserContext,
|
|
12
12
|
checkPermission,
|
|
13
13
|
detectsHallucinatedFileOp,
|
|
14
|
+
formatGitContextForPrompt,
|
|
14
15
|
getContentText,
|
|
15
16
|
getDangerLevel,
|
|
17
|
+
getGitContext,
|
|
16
18
|
getGitRoot,
|
|
17
19
|
googleSearchContext,
|
|
18
20
|
hadPreviousWriteToolCalls,
|
|
@@ -23,8 +25,9 @@ import {
|
|
|
23
25
|
setContextWindow,
|
|
24
26
|
setupProxy,
|
|
25
27
|
spawnAgentContext,
|
|
26
|
-
truncateOutput
|
|
27
|
-
|
|
28
|
+
truncateOutput,
|
|
29
|
+
undoStack
|
|
30
|
+
} from "./chunk-PVP4QYKG.js";
|
|
28
31
|
import {
|
|
29
32
|
AGENTIC_BEHAVIOR_GUIDELINE,
|
|
30
33
|
CONTEXT_FILE_CANDIDATES,
|
|
@@ -36,7 +39,7 @@ import {
|
|
|
36
39
|
PLAN_MODE_SYSTEM_ADDON,
|
|
37
40
|
SKILLS_DIR_NAME,
|
|
38
41
|
VERSION
|
|
39
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-STOS2HXS.js";
|
|
40
43
|
import {
|
|
41
44
|
AuthManager
|
|
42
45
|
} from "./chunk-CPLT6CD3.js";
|
|
@@ -46,7 +49,7 @@ import express from "express";
|
|
|
46
49
|
import { createServer } from "http";
|
|
47
50
|
import { WebSocketServer } from "ws";
|
|
48
51
|
import { join as join3, dirname, resolve as resolve2, relative } from "path";
|
|
49
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync, statSync } from "fs";
|
|
52
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
50
53
|
import { networkInterfaces } from "os";
|
|
51
54
|
|
|
52
55
|
// src/web/tool-executor-web.ts
|
|
@@ -68,6 +71,8 @@ var ToolExecutorWeb = class {
|
|
|
68
71
|
pendingBatchConfirms = /* @__PURE__ */ new Map();
|
|
69
72
|
/** Publicly readable by SessionHandler to check if confirm is active */
|
|
70
73
|
confirming = false;
|
|
74
|
+
/** Session-level auto-approve toggle (/yolo command) */
|
|
75
|
+
sessionAutoApprove = false;
|
|
71
76
|
/** Track tool start times for duration calculation */
|
|
72
77
|
toolStartTimes = /* @__PURE__ */ new Map();
|
|
73
78
|
setRoundInfo(current, total) {
|
|
@@ -238,6 +243,20 @@ var ToolExecutorWeb = class {
|
|
|
238
243
|
}
|
|
239
244
|
}
|
|
240
245
|
this.sendToolCallStart(call);
|
|
246
|
+
if (this.sessionAutoApprove && (dangerLevel === "write" || dangerLevel === "destructive")) {
|
|
247
|
+
try {
|
|
248
|
+
const rawContent = await tool.execute(call.arguments);
|
|
249
|
+
const content = truncateOutput(rawContent, call.name);
|
|
250
|
+
this.sendToolCallResult(call, rawContent, false);
|
|
251
|
+
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
|
|
252
|
+
return { callId: call.id, content, isError: false };
|
|
253
|
+
} catch (err) {
|
|
254
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
255
|
+
this.sendToolCallResult(call, message, true);
|
|
256
|
+
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
|
|
257
|
+
return { callId: call.id, content: message, isError: true };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
241
260
|
if (dangerLevel === "write" || dangerLevel === "destructive") {
|
|
242
261
|
const confirmed = await this.confirm(call, dangerLevel);
|
|
243
262
|
if (!confirmed) {
|
|
@@ -299,7 +318,7 @@ var ToolExecutorWeb = class {
|
|
|
299
318
|
}
|
|
300
319
|
async executeBatchFileWrites(items) {
|
|
301
320
|
const calls = items.map((i) => i.call);
|
|
302
|
-
const decision = await this.batchConfirm(calls);
|
|
321
|
+
const decision = this.sessionAutoApprove ? "all" : await this.batchConfirm(calls);
|
|
303
322
|
const results = [];
|
|
304
323
|
for (let i = 0; i < calls.length; i++) {
|
|
305
324
|
const call = calls[i];
|
|
@@ -403,12 +422,13 @@ function loadMemoryContent(configDir) {
|
|
|
403
422
|
}
|
|
404
423
|
|
|
405
424
|
// src/web/session-handler.ts
|
|
406
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3, appendFileSync, writeFileSync, mkdirSync } from "fs";
|
|
425
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, appendFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
407
426
|
import { join as join2, resolve } from "path";
|
|
427
|
+
import { execSync } from "child_process";
|
|
408
428
|
var MAX_TOOL_ROUNDS = 25;
|
|
409
429
|
var FREE_ROUND_TOOLS = /* @__PURE__ */ new Set(["write_todos"]);
|
|
410
430
|
var MAX_CONSECUTIVE_FREE_ROUNDS = 5;
|
|
411
|
-
var SessionHandler = class {
|
|
431
|
+
var SessionHandler = class _SessionHandler {
|
|
412
432
|
ws;
|
|
413
433
|
config;
|
|
414
434
|
providers;
|
|
@@ -1044,6 +1064,16 @@ Tokens: in=${this.sessionTokenUsage.inputTokens} out=${this.sessionTokenUsage.ou
|
|
|
1044
1064
|
" /memory \u2014 Show persistent memory contents",
|
|
1045
1065
|
" /memory add <text> \u2014 Add entry to persistent memory",
|
|
1046
1066
|
" /memory clear \u2014 Clear persistent memory",
|
|
1067
|
+
" /yolo [on|off] \u2014 Toggle session auto-approve (skip confirmations)",
|
|
1068
|
+
" /search <keyword> \u2014 Search across all session histories",
|
|
1069
|
+
" /undo [list|<n>] \u2014 Undo file operations",
|
|
1070
|
+
" /diff [--stats] \u2014 Show file modifications in this session",
|
|
1071
|
+
" /checkpoint [save|restore|delete] <name> \u2014 Session checkpoints",
|
|
1072
|
+
" /fork [checkpoint] \u2014 Fork session from checkpoint or current",
|
|
1073
|
+
" /review [--staged] \u2014 AI code review from git diff",
|
|
1074
|
+
" /test [command] \u2014 Run project tests",
|
|
1075
|
+
" /init [--force] \u2014 Generate AICLI.md by scanning project",
|
|
1076
|
+
" /doctor \u2014 Health check (API keys, config, MCP)",
|
|
1047
1077
|
" /help \u2014 Show this help message",
|
|
1048
1078
|
"",
|
|
1049
1079
|
"\u{1F4A1} Tips:",
|
|
@@ -1132,6 +1162,370 @@ ${activated.meta.description || ""}` });
|
|
|
1132
1162
|
}
|
|
1133
1163
|
break;
|
|
1134
1164
|
}
|
|
1165
|
+
// ── /yolo ──────────────────────────────────────────────────────
|
|
1166
|
+
case "yolo": {
|
|
1167
|
+
const sub = args[0]?.toLowerCase();
|
|
1168
|
+
if (sub === "off") {
|
|
1169
|
+
this.toolExecutor.sessionAutoApprove = false;
|
|
1170
|
+
this.send({ type: "info", message: "\u{1F512} Auto-approve disabled \u2014 confirmations restored for this session." });
|
|
1171
|
+
} else {
|
|
1172
|
+
this.toolExecutor.sessionAutoApprove = true;
|
|
1173
|
+
this.send({ type: "info", message: "\u26A1 YOLO mode ON \u2014 all write/destructive tools auto-approved for this session.\nUse /yolo off to re-enable confirmations." });
|
|
1174
|
+
}
|
|
1175
|
+
break;
|
|
1176
|
+
}
|
|
1177
|
+
// ── /search ─────────────────────────────────────────────────────
|
|
1178
|
+
case "search": {
|
|
1179
|
+
const query = args.join(" ").trim();
|
|
1180
|
+
if (!query) {
|
|
1181
|
+
this.send({ type: "error", message: "Usage: /search <keyword>" });
|
|
1182
|
+
break;
|
|
1183
|
+
}
|
|
1184
|
+
const results = this.sessions.searchMessages(query);
|
|
1185
|
+
if (results.length === 0) {
|
|
1186
|
+
this.send({ type: "info", message: `No sessions found containing "${query}".` });
|
|
1187
|
+
break;
|
|
1188
|
+
}
|
|
1189
|
+
const lines = [`\u{1F50D} Found ${results.length} session(s) containing "${query}"`, ""];
|
|
1190
|
+
for (const r of results) {
|
|
1191
|
+
const { sessionMeta, matches } = r;
|
|
1192
|
+
const dateStr = sessionMeta.updated instanceof Date ? sessionMeta.updated.toLocaleDateString() : String(sessionMeta.updated);
|
|
1193
|
+
lines.push(`**${sessionMeta.id.slice(0, 8)}** [${dateStr}] ${sessionMeta.provider} / ${sessionMeta.model}`);
|
|
1194
|
+
if (sessionMeta.title) lines.push(` ${sessionMeta.title}`);
|
|
1195
|
+
for (const m of matches) {
|
|
1196
|
+
const icon = m.role === "user" ? "\u{1F464}" : "\u{1F916}";
|
|
1197
|
+
lines.push(` ${icon} ${m.snippet}`);
|
|
1198
|
+
}
|
|
1199
|
+
lines.push("");
|
|
1200
|
+
}
|
|
1201
|
+
lines.push("Use /session load <id> to resume a session.");
|
|
1202
|
+
this.send({ type: "info", message: lines.join("\n") });
|
|
1203
|
+
break;
|
|
1204
|
+
}
|
|
1205
|
+
// ── /undo ───────────────────────────────────────────────────────
|
|
1206
|
+
case "undo": {
|
|
1207
|
+
const sub = args.join(" ").trim();
|
|
1208
|
+
if (sub === "list") {
|
|
1209
|
+
const history = undoStack.getHistory();
|
|
1210
|
+
if (history.length === 0) {
|
|
1211
|
+
this.send({ type: "info", message: "Undo stack is empty." });
|
|
1212
|
+
break;
|
|
1213
|
+
}
|
|
1214
|
+
const lines = [`\u{1F4CB} Undo Stack (${history.length} entries, newest last):`, ""];
|
|
1215
|
+
history.forEach((entry, i) => {
|
|
1216
|
+
const timeStr = entry.timestamp.toLocaleTimeString();
|
|
1217
|
+
const typeTag = entry.isDirectory ? "[dir]" : entry.previousContent === null ? "[new]" : "[mod]";
|
|
1218
|
+
lines.push(` ${String(i + 1).padStart(3)} ${typeTag} ${entry.description} ${timeStr}`);
|
|
1219
|
+
});
|
|
1220
|
+
this.send({ type: "info", message: lines.join("\n") });
|
|
1221
|
+
break;
|
|
1222
|
+
}
|
|
1223
|
+
const n = sub ? parseInt(sub, 10) : 1;
|
|
1224
|
+
if (isNaN(n) || n < 1) {
|
|
1225
|
+
this.send({ type: "info", message: "Usage: /undo [list | <n>]" });
|
|
1226
|
+
break;
|
|
1227
|
+
}
|
|
1228
|
+
if (undoStack.depth === 0) {
|
|
1229
|
+
this.send({ type: "info", message: "Nothing to undo." });
|
|
1230
|
+
break;
|
|
1231
|
+
}
|
|
1232
|
+
const count = Math.min(n, undoStack.depth);
|
|
1233
|
+
const undoResults = [];
|
|
1234
|
+
for (let i = 0; i < count; i++) {
|
|
1235
|
+
const top = undoStack.peek();
|
|
1236
|
+
if (!top) break;
|
|
1237
|
+
const undoResult = undoStack.undo();
|
|
1238
|
+
if (undoResult) undoResults.push(undoResult.result);
|
|
1239
|
+
}
|
|
1240
|
+
if (undoResults.length === 0) {
|
|
1241
|
+
this.send({ type: "info", message: "Nothing to undo." });
|
|
1242
|
+
} else {
|
|
1243
|
+
this.send({ type: "info", message: `\u2713 ${undoResults.length} operation(s) undone:
|
|
1244
|
+
${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
1245
|
+
}
|
|
1246
|
+
break;
|
|
1247
|
+
}
|
|
1248
|
+
// ── /diff ───────────────────────────────────────────────────────
|
|
1249
|
+
case "diff": {
|
|
1250
|
+
const history = undoStack.getHistory();
|
|
1251
|
+
if (history.length === 0) {
|
|
1252
|
+
this.send({ type: "info", message: "No file modifications in this session." });
|
|
1253
|
+
break;
|
|
1254
|
+
}
|
|
1255
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
1256
|
+
for (const entry of history) {
|
|
1257
|
+
if (!fileMap.has(entry.filePath)) {
|
|
1258
|
+
fileMap.set(entry.filePath, { earliest: entry.previousContent });
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
const statsOnly = args[0]?.toLowerCase() === "--stats";
|
|
1262
|
+
let newFiles = 0;
|
|
1263
|
+
let modifiedFiles = 0;
|
|
1264
|
+
const diffLines = [];
|
|
1265
|
+
for (const [filePath, { earliest }] of fileMap) {
|
|
1266
|
+
const currentContent = existsSync3(filePath) ? (() => {
|
|
1267
|
+
try {
|
|
1268
|
+
return readFileSync3(filePath, "utf-8");
|
|
1269
|
+
} catch {
|
|
1270
|
+
return null;
|
|
1271
|
+
}
|
|
1272
|
+
})() : null;
|
|
1273
|
+
const isNew = earliest === null;
|
|
1274
|
+
if (isNew) newFiles++;
|
|
1275
|
+
else modifiedFiles++;
|
|
1276
|
+
if (statsOnly) continue;
|
|
1277
|
+
if (isNew) {
|
|
1278
|
+
diffLines.push(`
|
|
1279
|
+
\u{1F4C4} [NEW] ${filePath}`);
|
|
1280
|
+
if (currentContent !== null) {
|
|
1281
|
+
diffLines.push(` ${currentContent.split("\n").length} lines`);
|
|
1282
|
+
} else {
|
|
1283
|
+
diffLines.push(" (file was deleted after creation)");
|
|
1284
|
+
}
|
|
1285
|
+
} else {
|
|
1286
|
+
diffLines.push(`
|
|
1287
|
+
\u{1F4DD} [MODIFIED] ${filePath}`);
|
|
1288
|
+
const oldText = earliest ?? "";
|
|
1289
|
+
const newText = currentContent ?? "";
|
|
1290
|
+
if (oldText === newText) {
|
|
1291
|
+
diffLines.push(" (no net change)");
|
|
1292
|
+
} else {
|
|
1293
|
+
diffLines.push(renderDiff(oldText, newText, { filePath, maxLines: 80 }));
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
diffLines.push(`
|
|
1298
|
+
**Summary:** ${fileMap.size} file(s) \u2014 ${newFiles} new, ${modifiedFiles} modified`);
|
|
1299
|
+
this.send({ type: "info", message: diffLines.join("\n") });
|
|
1300
|
+
break;
|
|
1301
|
+
}
|
|
1302
|
+
// ── /checkpoint ─────────────────────────────────────────────────
|
|
1303
|
+
case "checkpoint": {
|
|
1304
|
+
const session = this.sessions.current;
|
|
1305
|
+
if (!session) {
|
|
1306
|
+
this.send({ type: "error", message: "No active session." });
|
|
1307
|
+
break;
|
|
1308
|
+
}
|
|
1309
|
+
const sub = args[0]?.toLowerCase();
|
|
1310
|
+
if (!sub || sub === "list") {
|
|
1311
|
+
const cps = session.checkpoints;
|
|
1312
|
+
if (cps.length === 0) {
|
|
1313
|
+
this.send({ type: "info", message: "No checkpoints. Use /checkpoint save <name> to create one." });
|
|
1314
|
+
} else {
|
|
1315
|
+
const lines = [`\u{1F4CC} Checkpoints (${cps.length}):`, ""];
|
|
1316
|
+
for (const cp of cps) {
|
|
1317
|
+
const ts = cp.timestamp.toLocaleString();
|
|
1318
|
+
lines.push(` ${cp.name.padEnd(20)} msg #${cp.messageIndex} ${ts}`);
|
|
1319
|
+
}
|
|
1320
|
+
this.send({ type: "info", message: lines.join("\n") });
|
|
1321
|
+
}
|
|
1322
|
+
break;
|
|
1323
|
+
}
|
|
1324
|
+
const cpName = args.slice(1).join(" ").trim();
|
|
1325
|
+
if (sub === "save") {
|
|
1326
|
+
if (!cpName) {
|
|
1327
|
+
this.send({ type: "error", message: "Usage: /checkpoint save <name>" });
|
|
1328
|
+
break;
|
|
1329
|
+
}
|
|
1330
|
+
session.createCheckpoint(cpName);
|
|
1331
|
+
this.send({ type: "info", message: `\u2713 Checkpoint "${cpName}" saved at message #${session.messages.length}` });
|
|
1332
|
+
} else if (sub === "restore") {
|
|
1333
|
+
if (!cpName) {
|
|
1334
|
+
this.send({ type: "error", message: "Usage: /checkpoint restore <name>" });
|
|
1335
|
+
break;
|
|
1336
|
+
}
|
|
1337
|
+
const ok = session.restoreCheckpoint(cpName);
|
|
1338
|
+
if (ok) {
|
|
1339
|
+
this.send({ type: "info", message: `\u2713 Restored to checkpoint "${cpName}" (${session.messages.length} messages)` });
|
|
1340
|
+
this.sendSessionMessages();
|
|
1341
|
+
this.sendStatus();
|
|
1342
|
+
} else {
|
|
1343
|
+
this.send({ type: "error", message: `Checkpoint "${cpName}" not found.` });
|
|
1344
|
+
}
|
|
1345
|
+
} else if (sub === "delete") {
|
|
1346
|
+
if (!cpName) {
|
|
1347
|
+
this.send({ type: "error", message: "Usage: /checkpoint delete <name>" });
|
|
1348
|
+
break;
|
|
1349
|
+
}
|
|
1350
|
+
const ok = session.deleteCheckpoint(cpName);
|
|
1351
|
+
if (ok) {
|
|
1352
|
+
this.send({ type: "info", message: `\u2713 Deleted checkpoint "${cpName}"` });
|
|
1353
|
+
} else {
|
|
1354
|
+
this.send({ type: "error", message: `Checkpoint "${cpName}" not found.` });
|
|
1355
|
+
}
|
|
1356
|
+
} else {
|
|
1357
|
+
this.send({ type: "error", message: `Unknown subcommand: ${sub}. Use save/restore/list/delete.` });
|
|
1358
|
+
}
|
|
1359
|
+
break;
|
|
1360
|
+
}
|
|
1361
|
+
// ── /fork ───────────────────────────────────────────────────────
|
|
1362
|
+
case "fork": {
|
|
1363
|
+
const session = this.sessions.current;
|
|
1364
|
+
if (!session) {
|
|
1365
|
+
this.send({ type: "info", message: "No active session to fork." });
|
|
1366
|
+
break;
|
|
1367
|
+
}
|
|
1368
|
+
const sub = args.join(" ").trim();
|
|
1369
|
+
let messageCount = session.messages.length;
|
|
1370
|
+
let fromLabel = "current position";
|
|
1371
|
+
if (sub) {
|
|
1372
|
+
const cp = session.checkpoints.find((c) => c.name === sub);
|
|
1373
|
+
if (!cp) {
|
|
1374
|
+
const available = session.checkpoints.map((c) => c.name);
|
|
1375
|
+
this.send({ type: "error", message: available.length > 0 ? `Checkpoint "${sub}" not found. Available: ${available.join(", ")}` : `Checkpoint "${sub}" not found. No checkpoints saved.` });
|
|
1376
|
+
break;
|
|
1377
|
+
}
|
|
1378
|
+
messageCount = cp.messageIndex;
|
|
1379
|
+
fromLabel = `checkpoint "${cp.name}" (${cp.messageIndex} messages)`;
|
|
1380
|
+
}
|
|
1381
|
+
try {
|
|
1382
|
+
const originalId = session.id.slice(0, 8);
|
|
1383
|
+
const forked = await this.sessions.forkSession(messageCount);
|
|
1384
|
+
this.send({ type: "info", message: `\u{1F500} Session Forked
|
|
1385
|
+
Original: ${originalId}
|
|
1386
|
+
Forked: ${forked.id.slice(0, 8)} "${forked.title ?? "(untitled)"}"
|
|
1387
|
+
From: ${fromLabel}
|
|
1388
|
+
Messages: ${forked.messages.length} copied` });
|
|
1389
|
+
this.sendSessionMessages();
|
|
1390
|
+
this.sendStatus();
|
|
1391
|
+
this.sendSessionList();
|
|
1392
|
+
} catch (err) {
|
|
1393
|
+
this.send({ type: "error", message: `Fork failed: ${err.message}` });
|
|
1394
|
+
}
|
|
1395
|
+
break;
|
|
1396
|
+
}
|
|
1397
|
+
// ── /review ─────────────────────────────────────────────────────
|
|
1398
|
+
case "review": {
|
|
1399
|
+
const gitCtx = getGitContext();
|
|
1400
|
+
if (!gitCtx) {
|
|
1401
|
+
this.send({ type: "error", message: "Not a git repository." });
|
|
1402
|
+
break;
|
|
1403
|
+
}
|
|
1404
|
+
const staged = args.includes("--staged");
|
|
1405
|
+
const detailed = args.includes("--detailed");
|
|
1406
|
+
let diff;
|
|
1407
|
+
try {
|
|
1408
|
+
const cmd = staged ? "git diff --staged" : "git diff";
|
|
1409
|
+
diff = execSync(cmd, { encoding: "utf-8", timeout: 1e4 }).trim();
|
|
1410
|
+
} catch {
|
|
1411
|
+
this.send({ type: "error", message: "Failed to run git diff." });
|
|
1412
|
+
break;
|
|
1413
|
+
}
|
|
1414
|
+
if (!diff) {
|
|
1415
|
+
this.send({ type: "info", message: "No changes to review." + (staged ? "" : " Try --staged for staged changes.") });
|
|
1416
|
+
break;
|
|
1417
|
+
}
|
|
1418
|
+
const MAX_DIFF = 8e3;
|
|
1419
|
+
let truncated = false;
|
|
1420
|
+
if (diff.length > MAX_DIFF) {
|
|
1421
|
+
const head = diff.slice(0, Math.floor(MAX_DIFF * 0.7));
|
|
1422
|
+
const tail = diff.slice(diff.length - Math.floor(MAX_DIFF * 0.2));
|
|
1423
|
+
diff = head + "\n\n... [diff truncated, " + diff.length + " chars total] ...\n\n" + tail;
|
|
1424
|
+
truncated = true;
|
|
1425
|
+
}
|
|
1426
|
+
const reviewPrompt = this.buildReviewPrompt(diff, formatGitContextForPrompt(gitCtx), detailed);
|
|
1427
|
+
this.send({ type: "info", message: "\u{1F50D} Analyzing changes..." });
|
|
1428
|
+
try {
|
|
1429
|
+
const review = await this.chatOnce(reviewPrompt, { temperature: 0.3, maxTokens: 8192 });
|
|
1430
|
+
const msg = truncated ? review + "\n\n\u26A0 Diff was truncated. Consider reviewing smaller changesets." : review;
|
|
1431
|
+
this.send({ type: "info", message: msg });
|
|
1432
|
+
} catch (err) {
|
|
1433
|
+
this.send({ type: "error", message: `Review failed: ${err.message}` });
|
|
1434
|
+
}
|
|
1435
|
+
break;
|
|
1436
|
+
}
|
|
1437
|
+
// ── /test ───────────────────────────────────────────────────────
|
|
1438
|
+
case "test": {
|
|
1439
|
+
this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
|
|
1440
|
+
try {
|
|
1441
|
+
const { executeTests } = await import("./run-tests-6XMNZEGY.js");
|
|
1442
|
+
const argStr = args.join(" ").trim();
|
|
1443
|
+
let testArgs = {};
|
|
1444
|
+
if (argStr) {
|
|
1445
|
+
const isCommand = argStr.includes(" ") || /^(mvn|gradle|npm|pytest|cargo|go)\b/.test(argStr);
|
|
1446
|
+
testArgs = isCommand ? { command: argStr } : { filter: argStr };
|
|
1447
|
+
}
|
|
1448
|
+
const report = await executeTests(testArgs);
|
|
1449
|
+
this.send({ type: "info", message: report });
|
|
1450
|
+
} catch (err) {
|
|
1451
|
+
this.send({ type: "error", message: `Test failed: ${err.message}` });
|
|
1452
|
+
}
|
|
1453
|
+
break;
|
|
1454
|
+
}
|
|
1455
|
+
// ── /init ───────────────────────────────────────────────────────
|
|
1456
|
+
case "init": {
|
|
1457
|
+
const cwd = process.cwd();
|
|
1458
|
+
const targetPath = join2(cwd, "AICLI.md");
|
|
1459
|
+
const force = args.includes("--force");
|
|
1460
|
+
if (existsSync3(targetPath) && !force) {
|
|
1461
|
+
this.send({ type: "info", message: `AICLI.md already exists at ${targetPath}
|
|
1462
|
+
Use /init --force to overwrite.` });
|
|
1463
|
+
break;
|
|
1464
|
+
}
|
|
1465
|
+
this.send({ type: "info", message: "\u{1F4DD} Scanning project structure..." });
|
|
1466
|
+
try {
|
|
1467
|
+
const projectInfo = this.scanProject(cwd);
|
|
1468
|
+
const prompt = this.buildInitPrompt(projectInfo, cwd);
|
|
1469
|
+
const content = await this.chatOnce(prompt, { temperature: 0.3, maxTokens: 4096 });
|
|
1470
|
+
writeFileSync(targetPath, content, "utf-8");
|
|
1471
|
+
this.send({ type: "info", message: `\u2713 Generated: ${targetPath} (${content.length} chars)
|
|
1472
|
+
Use /context reload to load it.` });
|
|
1473
|
+
} catch (err) {
|
|
1474
|
+
this.send({ type: "error", message: `Failed to generate AICLI.md: ${err.message}` });
|
|
1475
|
+
}
|
|
1476
|
+
break;
|
|
1477
|
+
}
|
|
1478
|
+
// ── /doctor ─────────────────────────────────────────────────────
|
|
1479
|
+
case "doctor": {
|
|
1480
|
+
const lines = ["\u{1FA7A} **AI-CLI Health Check**", ""];
|
|
1481
|
+
lines.push("**API Keys:**");
|
|
1482
|
+
const providersList = this.providers.listAll();
|
|
1483
|
+
for (const p of providersList) {
|
|
1484
|
+
const icon = p.configured ? "\u2713" : "\u25CB";
|
|
1485
|
+
const status = p.configured ? "configured" : "not configured";
|
|
1486
|
+
lines.push(` ${icon} ${p.id.padEnd(14)} ${status}`);
|
|
1487
|
+
}
|
|
1488
|
+
lines.push("");
|
|
1489
|
+
const configDir = this.config.getConfigDir();
|
|
1490
|
+
lines.push("**Config Files:**");
|
|
1491
|
+
lines.push(` Dir: ${configDir}`);
|
|
1492
|
+
const checkFile = (label, filePath) => {
|
|
1493
|
+
const exists = existsSync3(filePath);
|
|
1494
|
+
let extra = "";
|
|
1495
|
+
if (exists) {
|
|
1496
|
+
try {
|
|
1497
|
+
extra = ` (${statSync(filePath).size} bytes)`;
|
|
1498
|
+
} catch {
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
lines.push(` ${exists ? "\u2713" : "\u2013"} ${label.padEnd(14)} ${exists ? filePath + extra : "(not found)"}`);
|
|
1502
|
+
};
|
|
1503
|
+
checkFile("config.json", join2(configDir, "config.json"));
|
|
1504
|
+
checkFile("memory.md", join2(configDir, MEMORY_FILE_NAME));
|
|
1505
|
+
checkFile("dev-state.md", join2(configDir, "dev-state.md"));
|
|
1506
|
+
lines.push("");
|
|
1507
|
+
if (this.mcpManager) {
|
|
1508
|
+
lines.push("**MCP Servers:**");
|
|
1509
|
+
const statuses = this.mcpManager.getStatus();
|
|
1510
|
+
if (statuses.length === 0) {
|
|
1511
|
+
lines.push(" (no servers configured)");
|
|
1512
|
+
} else {
|
|
1513
|
+
for (const s of statuses) {
|
|
1514
|
+
const state = s.connected ? `connected \xB7 ${s.serverName} \xB7 ${s.toolCount} tools` : `disconnected${s.error ? ` \xB7 ${s.error}` : ""}`;
|
|
1515
|
+
lines.push(` ${s.connected ? "\u2713" : "\u2717"} ${s.serverId.padEnd(16)} ${state}`);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
lines.push("");
|
|
1519
|
+
}
|
|
1520
|
+
lines.push("**Current Session:**");
|
|
1521
|
+
lines.push(` Provider: ${this.currentProvider}`);
|
|
1522
|
+
lines.push(` Model: ${this.currentModel}`);
|
|
1523
|
+
lines.push(` Version: ${VERSION}`);
|
|
1524
|
+
lines.push("");
|
|
1525
|
+
lines.push("\u2713 Health check complete");
|
|
1526
|
+
this.send({ type: "info", message: lines.join("\n") });
|
|
1527
|
+
break;
|
|
1528
|
+
}
|
|
1135
1529
|
default:
|
|
1136
1530
|
this.send({ type: "error", message: `Unknown command: /${name}. Type /help for available commands.` });
|
|
1137
1531
|
}
|
|
@@ -1313,6 +1707,21 @@ ${activated.meta.description || ""}` });
|
|
|
1313
1707
|
});
|
|
1314
1708
|
}
|
|
1315
1709
|
// ── Helpers ──────────────────────────────────────────────────────
|
|
1710
|
+
/**
|
|
1711
|
+
* One-shot AI call (for /init, /review). Returns plain text content.
|
|
1712
|
+
*/
|
|
1713
|
+
async chatOnce(prompt, opts) {
|
|
1714
|
+
const provider = this.providers.get(this.currentProvider);
|
|
1715
|
+
if (!provider) throw new Error("No provider available");
|
|
1716
|
+
const response = await provider.chat({
|
|
1717
|
+
messages: [{ role: "user", content: prompt, timestamp: /* @__PURE__ */ new Date() }],
|
|
1718
|
+
model: this.currentModel,
|
|
1719
|
+
stream: false,
|
|
1720
|
+
temperature: opts?.temperature ?? 0.3,
|
|
1721
|
+
maxTokens: opts?.maxTokens ?? 4096
|
|
1722
|
+
});
|
|
1723
|
+
return response.content;
|
|
1724
|
+
}
|
|
1316
1725
|
buildSystemPrompt() {
|
|
1317
1726
|
const skillContent = this.skillManager?.getActivePromptContent();
|
|
1318
1727
|
const activeSkill = skillContent && this.skillManager?.getActive() ? { name: this.skillManager.getActive().meta.name, content: skillContent } : void 0;
|
|
@@ -1367,6 +1776,197 @@ ${activated.meta.description || ""}` });
|
|
|
1367
1776
|
* 2. Project: <git-root>/AICLI.md or CLAUDE.md
|
|
1368
1777
|
* 3. Subdir: <cwd>/AICLI.md or CLAUDE.md (only if cwd ≠ project root)
|
|
1369
1778
|
*/
|
|
1779
|
+
// ── /init helpers ─────────────────────────────────────────────────
|
|
1780
|
+
static SCAN_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1781
|
+
"node_modules",
|
|
1782
|
+
".git",
|
|
1783
|
+
"dist",
|
|
1784
|
+
"build",
|
|
1785
|
+
"out",
|
|
1786
|
+
"target",
|
|
1787
|
+
".next",
|
|
1788
|
+
".nuxt",
|
|
1789
|
+
"__pycache__",
|
|
1790
|
+
".venv",
|
|
1791
|
+
"venv",
|
|
1792
|
+
".tox",
|
|
1793
|
+
".mypy_cache",
|
|
1794
|
+
".pytest_cache",
|
|
1795
|
+
".gradle",
|
|
1796
|
+
".idea",
|
|
1797
|
+
".vscode",
|
|
1798
|
+
".vs",
|
|
1799
|
+
"coverage",
|
|
1800
|
+
".cache",
|
|
1801
|
+
".parcel-cache",
|
|
1802
|
+
"dist-cjs",
|
|
1803
|
+
"release",
|
|
1804
|
+
".output",
|
|
1805
|
+
".turbo",
|
|
1806
|
+
"vendor"
|
|
1807
|
+
]);
|
|
1808
|
+
scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
|
|
1809
|
+
const lines = [];
|
|
1810
|
+
let count = 0;
|
|
1811
|
+
const walk = (d, prefix, depth) => {
|
|
1812
|
+
if (depth > maxDepth || count >= maxEntries) return;
|
|
1813
|
+
let entries;
|
|
1814
|
+
try {
|
|
1815
|
+
entries = readdirSync(d);
|
|
1816
|
+
} catch {
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
const filtered = entries.filter((e) => !e.startsWith(".") && !_SessionHandler.SCAN_SKIP_DIRS.has(e));
|
|
1820
|
+
const sorted = filtered.sort((a, b) => {
|
|
1821
|
+
let aIsDir = false, bIsDir = false;
|
|
1822
|
+
try {
|
|
1823
|
+
aIsDir = statSync(join2(d, a)).isDirectory();
|
|
1824
|
+
} catch {
|
|
1825
|
+
}
|
|
1826
|
+
try {
|
|
1827
|
+
bIsDir = statSync(join2(d, b)).isDirectory();
|
|
1828
|
+
} catch {
|
|
1829
|
+
}
|
|
1830
|
+
if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
|
|
1831
|
+
return a.localeCompare(b);
|
|
1832
|
+
});
|
|
1833
|
+
for (let i = 0; i < sorted.length && count < maxEntries; i++) {
|
|
1834
|
+
const name = sorted[i];
|
|
1835
|
+
const fullPath = join2(d, name);
|
|
1836
|
+
const isLast = i === sorted.length - 1;
|
|
1837
|
+
let isDir;
|
|
1838
|
+
try {
|
|
1839
|
+
isDir = statSync(fullPath).isDirectory();
|
|
1840
|
+
} catch {
|
|
1841
|
+
continue;
|
|
1842
|
+
}
|
|
1843
|
+
lines.push(prefix + (isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ") + name + (isDir ? "/" : ""));
|
|
1844
|
+
count++;
|
|
1845
|
+
if (isDir) walk(fullPath, prefix + (isLast ? " " : "\u2502 "), depth + 1);
|
|
1846
|
+
}
|
|
1847
|
+
};
|
|
1848
|
+
walk(dir, "", 0);
|
|
1849
|
+
if (count >= maxEntries) lines.push("... (truncated)");
|
|
1850
|
+
return lines.join("\n");
|
|
1851
|
+
}
|
|
1852
|
+
scanProject(cwd) {
|
|
1853
|
+
const info = { type: "unknown", language: "unknown", configFiles: [], directoryStructure: "" };
|
|
1854
|
+
const check = (file) => existsSync3(join2(cwd, file));
|
|
1855
|
+
const configCandidates = [
|
|
1856
|
+
"package.json",
|
|
1857
|
+
"tsconfig.json",
|
|
1858
|
+
"Cargo.toml",
|
|
1859
|
+
"pyproject.toml",
|
|
1860
|
+
"setup.py",
|
|
1861
|
+
"requirements.txt",
|
|
1862
|
+
"go.mod",
|
|
1863
|
+
"pom.xml",
|
|
1864
|
+
"build.gradle",
|
|
1865
|
+
"build.gradle.kts",
|
|
1866
|
+
"CMakeLists.txt",
|
|
1867
|
+
"Makefile",
|
|
1868
|
+
".csproj",
|
|
1869
|
+
".sln",
|
|
1870
|
+
"composer.json",
|
|
1871
|
+
"Gemfile",
|
|
1872
|
+
"mix.exs",
|
|
1873
|
+
"deno.json",
|
|
1874
|
+
"bun.lockb"
|
|
1875
|
+
];
|
|
1876
|
+
info.configFiles = configCandidates.filter(check);
|
|
1877
|
+
if (check("package.json")) {
|
|
1878
|
+
info.type = "node";
|
|
1879
|
+
info.language = check("tsconfig.json") ? "TypeScript" : "JavaScript";
|
|
1880
|
+
try {
|
|
1881
|
+
const pkg = JSON.parse(readFileSync3(join2(cwd, "package.json"), "utf-8"));
|
|
1882
|
+
const scripts = pkg.scripts ?? {};
|
|
1883
|
+
info.buildCommand = scripts.build ? "npm run build" : void 0;
|
|
1884
|
+
info.testCommand = scripts.test ? "npm test" : void 0;
|
|
1885
|
+
info.devCommand = scripts.dev ? "npm run dev" : void 0;
|
|
1886
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1887
|
+
if (allDeps["react"]) info.framework = "React";
|
|
1888
|
+
else if (allDeps["vue"]) info.framework = "Vue";
|
|
1889
|
+
else if (allDeps["@angular/core"]) info.framework = "Angular";
|
|
1890
|
+
else if (allDeps["next"]) info.framework = "Next.js";
|
|
1891
|
+
else if (allDeps["express"]) info.framework = "Express";
|
|
1892
|
+
} catch {
|
|
1893
|
+
}
|
|
1894
|
+
} else if (check("Cargo.toml")) {
|
|
1895
|
+
info.type = "rust";
|
|
1896
|
+
info.language = "Rust";
|
|
1897
|
+
info.buildCommand = "cargo build";
|
|
1898
|
+
info.testCommand = "cargo test";
|
|
1899
|
+
} else if (check("pyproject.toml") || check("setup.py") || check("requirements.txt")) {
|
|
1900
|
+
info.type = "python";
|
|
1901
|
+
info.language = "Python";
|
|
1902
|
+
info.testCommand = "pytest";
|
|
1903
|
+
} else if (check("go.mod")) {
|
|
1904
|
+
info.type = "go";
|
|
1905
|
+
info.language = "Go";
|
|
1906
|
+
info.buildCommand = "go build ./...";
|
|
1907
|
+
info.testCommand = "go test ./...";
|
|
1908
|
+
} else if (check("pom.xml")) {
|
|
1909
|
+
info.type = "java";
|
|
1910
|
+
info.language = "Java";
|
|
1911
|
+
info.buildCommand = "mvn package";
|
|
1912
|
+
info.testCommand = "mvn test";
|
|
1913
|
+
} else if (check("build.gradle") || check("build.gradle.kts")) {
|
|
1914
|
+
info.type = "java";
|
|
1915
|
+
info.language = "Java/Kotlin";
|
|
1916
|
+
info.buildCommand = "./gradlew build";
|
|
1917
|
+
info.testCommand = "./gradlew test";
|
|
1918
|
+
}
|
|
1919
|
+
info.directoryStructure = this.scanDirTree(cwd);
|
|
1920
|
+
return info;
|
|
1921
|
+
}
|
|
1922
|
+
buildInitPrompt(info, cwd) {
|
|
1923
|
+
const parts = [
|
|
1924
|
+
"Please generate an AICLI.md context file (Markdown format) for the following project.",
|
|
1925
|
+
"\n## Project Info\n",
|
|
1926
|
+
`- Working directory: ${cwd}`,
|
|
1927
|
+
`- Type: ${info.type}`,
|
|
1928
|
+
`- Language: ${info.language}`
|
|
1929
|
+
];
|
|
1930
|
+
if (info.framework) parts.push(`- Framework: ${info.framework}`);
|
|
1931
|
+
if (info.buildCommand) parts.push(`- Build command: ${info.buildCommand}`);
|
|
1932
|
+
if (info.testCommand) parts.push(`- Test command: ${info.testCommand}`);
|
|
1933
|
+
if (info.devCommand) parts.push(`- Dev command: ${info.devCommand}`);
|
|
1934
|
+
parts.push(`
|
|
1935
|
+
## Detected Config Files
|
|
1936
|
+
${info.configFiles.map((f) => `- ${f}`).join("\n")}`);
|
|
1937
|
+
parts.push(`
|
|
1938
|
+
## Directory Structure
|
|
1939
|
+
\`\`\`
|
|
1940
|
+
${info.directoryStructure}
|
|
1941
|
+
\`\`\``);
|
|
1942
|
+
parts.push(`
|
|
1943
|
+
## Requirements
|
|
1944
|
+
Generate a structured Markdown file with: project overview, tech stack, project structure, common commands, code style. Keep it concise, within 200 lines.`);
|
|
1945
|
+
return parts.join("\n");
|
|
1946
|
+
}
|
|
1947
|
+
// ── /review helper ──────────────────────────────────────────────────
|
|
1948
|
+
buildReviewPrompt(diff, gitContextStr, detailed) {
|
|
1949
|
+
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.";
|
|
1950
|
+
return `# Code Review Request
|
|
1951
|
+
|
|
1952
|
+
${level}
|
|
1953
|
+
|
|
1954
|
+
## Git Status
|
|
1955
|
+
${gitContextStr}
|
|
1956
|
+
|
|
1957
|
+
## Code Changes (diff)
|
|
1958
|
+
\`\`\`diff
|
|
1959
|
+
${diff}
|
|
1960
|
+
\`\`\`
|
|
1961
|
+
|
|
1962
|
+
## Output Format
|
|
1963
|
+
1. **Overall Assessment**: One-sentence summary
|
|
1964
|
+
2. **Issues** (if any): [Severity] file:line \u2014 description + fix
|
|
1965
|
+
3. **Improvement Suggestions** (if any)
|
|
1966
|
+
4. **Highlights** (if any)
|
|
1967
|
+
|
|
1968
|
+
Severity: \u{1F534} Critical / \u{1F7E1} Warning / \u{1F535} Info`;
|
|
1969
|
+
}
|
|
1370
1970
|
loadContextFiles() {
|
|
1371
1971
|
const parts = [];
|
|
1372
1972
|
const cwd = process.cwd();
|
|
@@ -1515,7 +2115,7 @@ async function startWebServer(options = {}) {
|
|
|
1515
2115
|
}
|
|
1516
2116
|
try {
|
|
1517
2117
|
const SKIP = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "dist-cjs", "release", "__pycache__", ".next", ".nuxt", "coverage", ".cache"]);
|
|
1518
|
-
const entries =
|
|
2118
|
+
const entries = readdirSync2(targetDir, { withFileTypes: true });
|
|
1519
2119
|
const files = entries.filter((e) => !SKIP.has(e.name) && !e.name.startsWith(".")).slice(0, 50).map((e) => ({
|
|
1520
2120
|
name: e.name,
|
|
1521
2121
|
path: relative(cwd, join3(targetDir, e.name)).replace(/\\/g, "/"),
|
|
@@ -1556,7 +2156,7 @@ async function startWebServer(options = {}) {
|
|
|
1556
2156
|
return;
|
|
1557
2157
|
}
|
|
1558
2158
|
try {
|
|
1559
|
-
const stat =
|
|
2159
|
+
const stat = statSync2(fullPath);
|
|
1560
2160
|
if (stat.size > 512 * 1024) {
|
|
1561
2161
|
res.json({ error: `File too large (${(stat.size / 1024).toFixed(0)} KB, max 512 KB)` });
|
|
1562
2162
|
return;
|