jinzd-ai-cli 0.4.211 → 0.4.213
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/README.md +30 -0
- package/README.zh-CN.md +30 -0
- package/dist/{batch-DE4RXKZD.js → batch-PKY63TME.js} +2 -2
- package/dist/{chat-index-UBCWHBLR.js → chat-index-R2E27VXN.js} +1 -1
- package/dist/{chunk-JBWA73GK.js → chunk-7NTFMLJJ.js} +1 -1
- package/dist/{chunk-C3OU2OPF.js → chunk-FGUKBIYO.js} +2 -2
- package/dist/{chunk-BE6ERF7M.js → chunk-G3PW3AOB.js} +1 -1
- package/dist/{chunk-E5XCM4A6.js → chunk-IWLVH32D.js} +1 -1
- package/dist/{chunk-ZY2N2N6T.js → chunk-J35V3IAH.js} +1 -1
- package/dist/{chunk-UOROWTGG.js → chunk-MRGWPCFQ.js} +26 -3
- package/dist/{chunk-W7UKO3PS.js → chunk-OQGVGPEK.js} +5 -0
- package/dist/{chunk-ZOPYREL5.js → chunk-THNECAAY.js} +464 -67
- package/dist/{chunk-6NS6643Y.js → chunk-UDMU4FUF.js} +1 -1
- package/dist/{ci-XMUEX526.js → ci-HBZ6CIW5.js} +2 -2
- package/dist/{constants-AWTIQIWG.js → constants-CAUBTSFW.js} +1 -1
- package/dist/{doctor-cli-SSI6ETFT.js → doctor-cli-5Z5UWYW6.js} +4 -4
- package/dist/electron-server.js +794 -241
- package/dist/{hub-INUJND2G.js → hub-ZRADBR76.js} +1 -1
- package/dist/index.js +205 -19
- package/dist/{run-tests-PACN4UYX.js → run-tests-7J3PNWVS.js} +1 -1
- package/dist/{run-tests-3MHWUF43.js → run-tests-DSOJBJWA.js} +2 -2
- package/dist/{server-7USZJJAH.js → server-PWBWHVYB.js} +4 -4
- package/dist/{server-BTSKOPQI.js → server-U4I7RI42.js} +175 -10
- package/dist/{task-orchestrator-OV4O25MX.js → task-orchestrator-RUXWRMQV.js} +4 -4
- package/dist/{usage-T2P6FTE7.js → usage-2P6TIBTN.js} +2 -2
- package/dist/web/client/app.js +1 -1
- package/package.json +1 -1
|
@@ -5,10 +5,10 @@ import {
|
|
|
5
5
|
} from "./chunk-T2NL5ZIA.js";
|
|
6
6
|
import {
|
|
7
7
|
runTestsTool
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-UDMU4FUF.js";
|
|
9
9
|
import {
|
|
10
10
|
runTool
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-7NTFMLJJ.js";
|
|
12
12
|
import {
|
|
13
13
|
getDangerLevel,
|
|
14
14
|
isFileWriteTool,
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
SUBAGENT_ALLOWED_TOOLS,
|
|
27
27
|
SUBAGENT_DEFAULT_MAX_ROUNDS,
|
|
28
28
|
SUBAGENT_MAX_ROUNDS_LIMIT
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-IWLVH32D.js";
|
|
30
30
|
import {
|
|
31
31
|
fileCheckpoints
|
|
32
32
|
} from "./chunk-4BKXL7SM.js";
|
|
@@ -1050,12 +1050,12 @@ ${content}`;
|
|
|
1050
1050
|
};
|
|
1051
1051
|
|
|
1052
1052
|
// src/tools/builtin/write-file.ts
|
|
1053
|
-
import { appendFileSync, mkdirSync } from "fs";
|
|
1053
|
+
import { appendFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
1054
1054
|
import { dirname as dirname2 } from "path";
|
|
1055
1055
|
|
|
1056
1056
|
// src/tools/executor.ts
|
|
1057
1057
|
import chalk3 from "chalk";
|
|
1058
|
-
import { existsSync as
|
|
1058
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
1059
1059
|
import { tmpdir } from "os";
|
|
1060
1060
|
|
|
1061
1061
|
// src/core/readline-internal.ts
|
|
@@ -1263,6 +1263,155 @@ function simpleDiff(oldLines, newLines) {
|
|
|
1263
1263
|
|
|
1264
1264
|
// src/tools/hooks.ts
|
|
1265
1265
|
import { execSync } from "child_process";
|
|
1266
|
+
import { createHash } from "crypto";
|
|
1267
|
+
import { existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3 } from "fs";
|
|
1268
|
+
import { join } from "path";
|
|
1269
|
+
var TRUST_FILE = "hooks-trust.json";
|
|
1270
|
+
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
1271
|
+
function asArray(entry) {
|
|
1272
|
+
if (!entry) return [];
|
|
1273
|
+
const raw = Array.isArray(entry) ? entry : [entry];
|
|
1274
|
+
return raw.map((item) => typeof item === "string" ? { command: item } : item);
|
|
1275
|
+
}
|
|
1276
|
+
function hookTrustPath(configDir) {
|
|
1277
|
+
return join(configDir, TRUST_FILE);
|
|
1278
|
+
}
|
|
1279
|
+
function loadHookTrustStore(configDir) {
|
|
1280
|
+
const file = hookTrustPath(configDir);
|
|
1281
|
+
if (!existsSync4(file)) return { records: [] };
|
|
1282
|
+
try {
|
|
1283
|
+
const parsed = JSON.parse(readFileSync3(file, "utf-8"));
|
|
1284
|
+
return { records: Array.isArray(parsed.records) ? parsed.records : [] };
|
|
1285
|
+
} catch {
|
|
1286
|
+
return { records: [] };
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
function saveHookTrustStore(configDir, store) {
|
|
1290
|
+
mkdirSync(configDir, { recursive: true });
|
|
1291
|
+
atomicWriteFileSync(hookTrustPath(configDir), JSON.stringify(store, null, 2));
|
|
1292
|
+
}
|
|
1293
|
+
function hashHook(event, command) {
|
|
1294
|
+
return createHash("sha256").update(`${event}
|
|
1295
|
+
${command}`, "utf-8").digest("hex");
|
|
1296
|
+
}
|
|
1297
|
+
function isTrusted(source, event, hash, store) {
|
|
1298
|
+
if (source === "user" || source === "managed") return true;
|
|
1299
|
+
return store.records.some((r) => r.source === source && r.event === event && r.hash === hash);
|
|
1300
|
+
}
|
|
1301
|
+
function listHooks(config, configDir) {
|
|
1302
|
+
if (!config || config.enabled === false) return [];
|
|
1303
|
+
const store = loadHookTrustStore(configDir);
|
|
1304
|
+
const out = [];
|
|
1305
|
+
const push = (event, hook, index) => {
|
|
1306
|
+
if (!hook.command) return;
|
|
1307
|
+
const source = hook.source ?? "user";
|
|
1308
|
+
const hash = hashHook(event, hook.command);
|
|
1309
|
+
out.push({
|
|
1310
|
+
id: `${event}:${index}`,
|
|
1311
|
+
event,
|
|
1312
|
+
command: hook.command,
|
|
1313
|
+
source,
|
|
1314
|
+
hash,
|
|
1315
|
+
trusted: isTrusted(source, event, hash, store),
|
|
1316
|
+
disabled: hook.disabled === true,
|
|
1317
|
+
description: hook.description
|
|
1318
|
+
});
|
|
1319
|
+
};
|
|
1320
|
+
if (config.preToolExecution) push("PreToolUse", { command: config.preToolExecution }, 0);
|
|
1321
|
+
if (config.postToolExecution) push("PostToolUse", { command: config.postToolExecution }, 0);
|
|
1322
|
+
for (const [event, entry] of Object.entries(config.events ?? {})) {
|
|
1323
|
+
asArray(entry).forEach((hook, i) => push(event, hook, i));
|
|
1324
|
+
}
|
|
1325
|
+
return out;
|
|
1326
|
+
}
|
|
1327
|
+
function trustHook(configDir, hook) {
|
|
1328
|
+
const store = loadHookTrustStore(configDir);
|
|
1329
|
+
const next = store.records.filter((r) => !(r.source === hook.source && r.event === hook.event && r.id === hook.id));
|
|
1330
|
+
next.push({ id: hook.id, event: hook.event, source: hook.source, hash: hook.hash, trustedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1331
|
+
saveHookTrustStore(configDir, { records: next });
|
|
1332
|
+
}
|
|
1333
|
+
function untrustHook(configDir, hook) {
|
|
1334
|
+
const store = loadHookTrustStore(configDir);
|
|
1335
|
+
saveHookTrustStore(configDir, {
|
|
1336
|
+
records: store.records.filter((r) => !(r.source === hook.source && r.event === hook.event && r.hash === hook.hash))
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
function getPendingHookTrust(config, configDir) {
|
|
1340
|
+
return listHooks(config, configDir).filter((h) => h.source === "project" && !h.disabled && !h.trusted);
|
|
1341
|
+
}
|
|
1342
|
+
function normalizeDecision(value, hook) {
|
|
1343
|
+
if (!value || typeof value !== "object") return null;
|
|
1344
|
+
const obj = value;
|
|
1345
|
+
const action = obj.action;
|
|
1346
|
+
if (action !== "allow" && action !== "deny" && action !== "ask") return null;
|
|
1347
|
+
return {
|
|
1348
|
+
action,
|
|
1349
|
+
reason: typeof obj.reason === "string" ? obj.reason : void 0,
|
|
1350
|
+
prompt: typeof obj.prompt === "string" ? obj.prompt : void 0,
|
|
1351
|
+
contextAppend: typeof obj.contextAppend === "string" ? obj.contextAppend : void 0,
|
|
1352
|
+
warning: typeof obj.warning === "string" ? obj.warning : void 0,
|
|
1353
|
+
warnings: Array.isArray(obj.warnings) ? obj.warnings.filter((x) => typeof x === "string") : void 0,
|
|
1354
|
+
hook
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
function runStructuredHook(hook, descriptor, payload) {
|
|
1358
|
+
if (hook.disabled === true) return null;
|
|
1359
|
+
const timeout = Math.max(100, Math.min(hook.timeoutMs ?? DEFAULT_TIMEOUT_MS, 3e4));
|
|
1360
|
+
try {
|
|
1361
|
+
const stdout = execSync(hook.command, {
|
|
1362
|
+
timeout,
|
|
1363
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1364
|
+
encoding: "utf-8",
|
|
1365
|
+
env: {
|
|
1366
|
+
...process.env,
|
|
1367
|
+
AICLI_HOOK_EVENT: payload.event,
|
|
1368
|
+
AICLI_HOOK_EVENT_JSON: JSON.stringify(payload)
|
|
1369
|
+
}
|
|
1370
|
+
}).trim();
|
|
1371
|
+
if (!stdout) return null;
|
|
1372
|
+
return normalizeDecision(JSON.parse(stdout), descriptor);
|
|
1373
|
+
} catch (err) {
|
|
1374
|
+
process.stderr.write(`\u26A0 Hook failed: ${hook.command.slice(0, 100)}
|
|
1375
|
+
`);
|
|
1376
|
+
if (hook.required) {
|
|
1377
|
+
return { action: "deny", reason: err instanceof Error ? err.message : String(err), hook: descriptor };
|
|
1378
|
+
}
|
|
1379
|
+
return null;
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
function runLifecycleHooks(config, event, payload, opts) {
|
|
1383
|
+
if (!config || config.enabled === false) return [];
|
|
1384
|
+
const options = typeof opts === "string" ? { configDir: opts } : opts;
|
|
1385
|
+
const store = loadHookTrustStore(options.configDir);
|
|
1386
|
+
const hooks = asArray(config.events?.[event]);
|
|
1387
|
+
const decisions = [];
|
|
1388
|
+
for (let i = 0; i < hooks.length; i++) {
|
|
1389
|
+
const hook = hooks[i];
|
|
1390
|
+
if (!hook.command || hook.disabled) continue;
|
|
1391
|
+
const source = hook.source ?? "user";
|
|
1392
|
+
const hash = hashHook(event, hook.command);
|
|
1393
|
+
const descriptor = {
|
|
1394
|
+
id: `${event}:${i}`,
|
|
1395
|
+
event,
|
|
1396
|
+
command: hook.command,
|
|
1397
|
+
source,
|
|
1398
|
+
hash,
|
|
1399
|
+
trusted: isTrusted(source, event, hash, store),
|
|
1400
|
+
disabled: false,
|
|
1401
|
+
description: hook.description
|
|
1402
|
+
};
|
|
1403
|
+
if (!descriptor.trusted) {
|
|
1404
|
+
options.onSummary?.(`hook skipped: ${event} project hook requires trust (${hash.slice(0, 12)})`);
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1407
|
+
const decision = runStructuredHook(hook, descriptor, { event, ...payload });
|
|
1408
|
+
if (decision) {
|
|
1409
|
+
decisions.push(decision);
|
|
1410
|
+
options.onSummary?.(`hook ${event}: ${decision.action}${decision.reason ? ` (${decision.reason})` : ""}`);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
return decisions;
|
|
1414
|
+
}
|
|
1266
1415
|
function rewriteTemplate(template, isWindows) {
|
|
1267
1416
|
const ref = (name) => isWindows ? `%${name}%` : `$${name}`;
|
|
1268
1417
|
return template.replace(/\{tool\}/g, ref("AICLI_HOOK_TOOL")).replace(/\{dangerLevel\}/g, ref("AICLI_HOOK_DANGER_LEVEL")).replace(/\{args\}/g, ref("AICLI_HOOK_ARGS")).replace(/\{status\}/g, ref("AICLI_HOOK_STATUS"));
|
|
@@ -1273,7 +1422,7 @@ function runHook(template, vars) {
|
|
|
1273
1422
|
const cmd = rewriteTemplate(template, isWindows);
|
|
1274
1423
|
try {
|
|
1275
1424
|
execSync(cmd, {
|
|
1276
|
-
timeout:
|
|
1425
|
+
timeout: DEFAULT_TIMEOUT_MS,
|
|
1277
1426
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1278
1427
|
encoding: "utf-8",
|
|
1279
1428
|
env: {
|
|
@@ -1711,6 +1860,157 @@ var theme = new Proxy(DARK_THEME, {
|
|
|
1711
1860
|
}
|
|
1712
1861
|
});
|
|
1713
1862
|
|
|
1863
|
+
// src/tools/action-classifier.ts
|
|
1864
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
1865
|
+
import { isAbsolute as isAbsolute2, join as join2, resolve as resolve4 } from "path";
|
|
1866
|
+
var RECENT_DENIED_LIMIT = 50;
|
|
1867
|
+
var recentlyDenied = [];
|
|
1868
|
+
function recordRecentlyDeniedAutoAction(call, classification) {
|
|
1869
|
+
recentlyDenied.unshift({
|
|
1870
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1871
|
+
tool: call.name,
|
|
1872
|
+
reason: classification.reason,
|
|
1873
|
+
ruleId: classification.ruleId,
|
|
1874
|
+
argsPreview: JSON.stringify(call.arguments).slice(0, 240)
|
|
1875
|
+
});
|
|
1876
|
+
recentlyDenied.splice(RECENT_DENIED_LIMIT);
|
|
1877
|
+
}
|
|
1878
|
+
function getRecentlyDeniedAutoActions() {
|
|
1879
|
+
return [...recentlyDenied];
|
|
1880
|
+
}
|
|
1881
|
+
function clearRecentlyDeniedAutoActions() {
|
|
1882
|
+
recentlyDenied.splice(0, recentlyDenied.length);
|
|
1883
|
+
}
|
|
1884
|
+
function normalizePath(value) {
|
|
1885
|
+
return value.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
1886
|
+
}
|
|
1887
|
+
function isInside(root, target) {
|
|
1888
|
+
const absoluteRoot = resolve4(root);
|
|
1889
|
+
const absoluteTarget = isAbsolute2(target) ? target : resolve4(root, target);
|
|
1890
|
+
const base = normalizePath(absoluteRoot);
|
|
1891
|
+
const t = normalizePath(absoluteTarget);
|
|
1892
|
+
return t === base || t.startsWith(base + "/");
|
|
1893
|
+
}
|
|
1894
|
+
function getPathArg(call) {
|
|
1895
|
+
const raw = call.arguments.path ?? call.arguments.filePath ?? call.arguments.outputPath ?? call.arguments.file;
|
|
1896
|
+
return typeof raw === "string" && raw.trim() ? raw.trim() : null;
|
|
1897
|
+
}
|
|
1898
|
+
function splitShellWords(command) {
|
|
1899
|
+
const out = [];
|
|
1900
|
+
const re = /"([^"]*)"|'([^']*)'|([^\s]+)/g;
|
|
1901
|
+
let m;
|
|
1902
|
+
while (m = re.exec(command)) out.push(m[1] ?? m[2] ?? m[3] ?? "");
|
|
1903
|
+
return out.filter(Boolean);
|
|
1904
|
+
}
|
|
1905
|
+
function readPackageManifest(root) {
|
|
1906
|
+
const p = join2(root, "package.json");
|
|
1907
|
+
if (!existsSync5(p)) return null;
|
|
1908
|
+
try {
|
|
1909
|
+
return JSON.parse(readFileSync4(p, "utf-8"));
|
|
1910
|
+
} catch {
|
|
1911
|
+
return null;
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
function manifestHasDependency(root, name) {
|
|
1915
|
+
const manifest = readPackageManifest(root);
|
|
1916
|
+
if (!manifest) return false;
|
|
1917
|
+
for (const key of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
|
|
1918
|
+
const deps = manifest[key];
|
|
1919
|
+
if (deps && typeof deps === "object" && Object.prototype.hasOwnProperty.call(deps, name)) return true;
|
|
1920
|
+
}
|
|
1921
|
+
return false;
|
|
1922
|
+
}
|
|
1923
|
+
function lockfileMentions(root, name) {
|
|
1924
|
+
for (const file of ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"]) {
|
|
1925
|
+
const p = join2(root, file);
|
|
1926
|
+
if (!existsSync5(p)) continue;
|
|
1927
|
+
try {
|
|
1928
|
+
if (readFileSync4(p, "utf-8").includes(name)) return true;
|
|
1929
|
+
} catch {
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
return false;
|
|
1933
|
+
}
|
|
1934
|
+
function getInstallPackages(command) {
|
|
1935
|
+
const words = splitShellWords(command);
|
|
1936
|
+
const cmd = words[0]?.toLowerCase();
|
|
1937
|
+
const sub = words[1]?.toLowerCase();
|
|
1938
|
+
if (!cmd || !sub) return [];
|
|
1939
|
+
const isInstall = cmd === "npm" && (sub === "install" || sub === "i" || sub === "add") || (cmd === "pnpm" || cmd === "yarn") && (sub === "add" || sub === "install");
|
|
1940
|
+
if (!isInstall) return [];
|
|
1941
|
+
return words.slice(2).filter((w) => !w.startsWith("-")).filter((w) => w !== "." && w !== "./").map((w) => w.replace(/^@([^/]+)\/([^@]+)(?:@.+)?$/, "@$1/$2").replace(/^([^@]+)@.+$/, "$1"));
|
|
1942
|
+
}
|
|
1943
|
+
function isPlainGitPush(command) {
|
|
1944
|
+
if (!/^\s*git\s+push\b/i.test(command)) return false;
|
|
1945
|
+
if (/--force|-f\b|\+[^\s]+/i.test(command)) return false;
|
|
1946
|
+
return !/[;&|`$<>\n\r]/.test(command);
|
|
1947
|
+
}
|
|
1948
|
+
function hasSecretExfiltrationShape(command) {
|
|
1949
|
+
return /(?:\.env|config\.json|id_rsa|id_ed25519|AWS_SECRET|AICLI_API_KEY|OPENAI_API_KEY|ANTHROPIC_API_KEY)/i.test(command) && /\b(curl|wget|scp|rsync|nc|netcat|Invoke-WebRequest|Invoke-RestMethod)\b/i.test(command);
|
|
1950
|
+
}
|
|
1951
|
+
var RuleBasedActionClassifier = class {
|
|
1952
|
+
classify(call, dangerLevel, ctx) {
|
|
1953
|
+
if (call.name === "bash") return this.classifyShell(call, dangerLevel, ctx);
|
|
1954
|
+
if (call.name === "web_fetch" || call.name === "web_search" || call.name === "google_search") {
|
|
1955
|
+
return { decision: "auto-approve", reason: "read-only HTTP tool", ruleId: "auto.readonly-http", confidence: "high" };
|
|
1956
|
+
}
|
|
1957
|
+
if (dangerLevel === "safe") {
|
|
1958
|
+
return { decision: "auto-approve", reason: "safe read-only tool", ruleId: "auto.safe-tool", confidence: "high" };
|
|
1959
|
+
}
|
|
1960
|
+
if (dangerLevel === "write") {
|
|
1961
|
+
const p = getPathArg(call);
|
|
1962
|
+
if (p && (isInside(ctx.workspaceRoot, p) || (ctx.tempDirs ?? []).some((root) => isInside(root, p)))) {
|
|
1963
|
+
return { decision: "auto-approve", reason: "explicit write inside workspace/temp roots", ruleId: "auto.workspace-write", confidence: "high" };
|
|
1964
|
+
}
|
|
1965
|
+
return { decision: "confirm", reason: "write action is not proven low-risk", ruleId: "auto.write-confirm", confidence: "medium" };
|
|
1966
|
+
}
|
|
1967
|
+
return { decision: "confirm", reason: "destructive actions require confirmation", ruleId: "auto.destructive-confirm", confidence: "high" };
|
|
1968
|
+
}
|
|
1969
|
+
classifyShell(call, dangerLevel, ctx) {
|
|
1970
|
+
const command = String(call.arguments.command ?? "");
|
|
1971
|
+
if (/\bcurl\b[^\n|;&]*\|\s*(?:sudo\s+)?(?:bash|sh|zsh)\b/i.test(command)) {
|
|
1972
|
+
return { decision: "deny", reason: "curl pipe shell execution is blocked in auto mode", ruleId: "deny.curl-pipe-shell", confidence: "high" };
|
|
1973
|
+
}
|
|
1974
|
+
if (/\bgit\s+push\b/i.test(command) && /--force|-f\b|\+[^\s]+/i.test(command)) {
|
|
1975
|
+
return { decision: "deny", reason: "force push is blocked in auto mode", ruleId: "deny.force-push", confidence: "high" };
|
|
1976
|
+
}
|
|
1977
|
+
if (hasSecretExfiltrationShape(command)) {
|
|
1978
|
+
return { decision: "deny", reason: "possible secret exfiltration is blocked in auto mode", ruleId: "deny.secret-exfiltration", confidence: "high" };
|
|
1979
|
+
}
|
|
1980
|
+
if (/\b(terraform|tofu)\s+destroy\b|\bkubectl\s+delete\b|\bcdk\s+destroy\b/i.test(command)) {
|
|
1981
|
+
return { decision: "confirm", reason: "infrastructure destroy requires confirmation", ruleId: "confirm.iac-destroy", confidence: "high" };
|
|
1982
|
+
}
|
|
1983
|
+
if (/\b(prisma|sequelize|knex|typeorm)\b[^\n]*\b(migrate|migration)\b|\bdb:migrate\b/i.test(command)) {
|
|
1984
|
+
return { decision: "confirm", reason: "database migration requires confirmation", ruleId: "confirm.database-migration", confidence: "high" };
|
|
1985
|
+
}
|
|
1986
|
+
if (/\b(vercel|netlify|flyctl|railway|wrangler|serverless|firebase)\b[^\n]*\bdeploy\b|\bdeploy\b[^\n]*\b(prod|production)\b/i.test(command)) {
|
|
1987
|
+
return { decision: "confirm", reason: "production deployment requires confirmation", ruleId: "confirm.production-deploy", confidence: "high" };
|
|
1988
|
+
}
|
|
1989
|
+
if (/\b(iam|policy|token|secret|api[-_]?key|credential|permission|role)\b/i.test(command)) {
|
|
1990
|
+
return { decision: "confirm", reason: "credential or permission change requires confirmation", ruleId: "confirm.credentials-permissions", confidence: "medium" };
|
|
1991
|
+
}
|
|
1992
|
+
if (/\b(npx|pnpm\s+dlx|yarn\s+dlx)\s+(claude|codex|aider|cursor-agent|gemini)\b/i.test(command)) {
|
|
1993
|
+
return { decision: "confirm", reason: "third-party agent loop requires confirmation", ruleId: "confirm.third-party-agent", confidence: "high" };
|
|
1994
|
+
}
|
|
1995
|
+
if (isPlainGitPush(command)) {
|
|
1996
|
+
return { decision: "auto-approve", reason: "plain git push without force", ruleId: "auto.git-push", confidence: "high" };
|
|
1997
|
+
}
|
|
1998
|
+
const pkgs = getInstallPackages(command);
|
|
1999
|
+
if (pkgs.length > 0) {
|
|
2000
|
+
const allDeclared = pkgs.every((p) => manifestHasDependency(ctx.workspaceRoot, p) || lockfileMentions(ctx.workspaceRoot, p));
|
|
2001
|
+
return allDeclared ? { decision: "auto-approve", reason: "installing dependencies already declared in manifest/lockfile", ruleId: "auto.declared-dependency-install", confidence: "medium" } : { decision: "confirm", reason: "dependency install includes undeclared packages", ruleId: "confirm.undeclared-dependency-install", confidence: "medium" };
|
|
2002
|
+
}
|
|
2003
|
+
if (/\b(curl|wget|Invoke-WebRequest|Invoke-RestMethod)\b/i.test(command) && !/[>|;&`$]/.test(command)) {
|
|
2004
|
+
return { decision: "auto-approve", reason: "read-only HTTP shell command", ruleId: "auto.readonly-http-shell", confidence: "medium" };
|
|
2005
|
+
}
|
|
2006
|
+
if (dangerLevel === "safe") {
|
|
2007
|
+
return { decision: "auto-approve", reason: "safe shell action", ruleId: "auto.safe-shell", confidence: "medium" };
|
|
2008
|
+
}
|
|
2009
|
+
return { decision: "confirm", reason: "shell action is not proven low-risk", ruleId: "confirm.shell-default", confidence: "medium" };
|
|
2010
|
+
}
|
|
2011
|
+
};
|
|
2012
|
+
var defaultActionClassifier = new RuleBasedActionClassifier();
|
|
2013
|
+
|
|
1714
2014
|
// src/tools/executor.ts
|
|
1715
2015
|
function buildErrorPreview(content) {
|
|
1716
2016
|
const meaningful = content.split("\n").filter(
|
|
@@ -1750,6 +2050,8 @@ var ToolExecutor = class {
|
|
|
1750
2050
|
* destructive 操作仍然必须逐次确认。
|
|
1751
2051
|
*/
|
|
1752
2052
|
sessionAutoApprove = false;
|
|
2053
|
+
/** Session-level Auto Mode: rule-based low-risk auto approval. */
|
|
2054
|
+
sessionAutoMode = false;
|
|
1753
2055
|
/**
|
|
1754
2056
|
* Logical session key used to scope per-session state in stateful tools
|
|
1755
2057
|
* (currently only `bash`'s persistent cwd). Web mode sets this per-tab so
|
|
@@ -1790,6 +2092,7 @@ var ToolExecutor = class {
|
|
|
1790
2092
|
allowedPermissionProfiles = [];
|
|
1791
2093
|
workspaceRoot = process.cwd();
|
|
1792
2094
|
networkPolicy;
|
|
2095
|
+
hookConfigDir = process.cwd();
|
|
1793
2096
|
/** 注入 hooks 和 permission rules 配置 */
|
|
1794
2097
|
setConfig(opts) {
|
|
1795
2098
|
this.hookConfig = opts.hookConfig;
|
|
@@ -1800,6 +2103,7 @@ var ToolExecutor = class {
|
|
|
1800
2103
|
if (opts.allowedPermissionProfiles) this.allowedPermissionProfiles = opts.allowedPermissionProfiles;
|
|
1801
2104
|
if (opts.workspaceRoot) this.workspaceRoot = opts.workspaceRoot;
|
|
1802
2105
|
if (opts.networkPolicy) this.networkPolicy = opts.networkPolicy;
|
|
2106
|
+
if (opts.hookConfigDir) this.hookConfigDir = opts.hookConfigDir;
|
|
1803
2107
|
}
|
|
1804
2108
|
async execute(call) {
|
|
1805
2109
|
return runWithSessionKey(this.sessionKey, () => this.executeInner(call));
|
|
@@ -1820,6 +2124,24 @@ var ToolExecutor = class {
|
|
|
1820
2124
|
dangerLevel,
|
|
1821
2125
|
args: JSON.stringify(call.arguments).slice(0, 200)
|
|
1822
2126
|
});
|
|
2127
|
+
const preToolDecisions = runLifecycleHooks(
|
|
2128
|
+
this.hookConfig,
|
|
2129
|
+
"PreToolUse",
|
|
2130
|
+
{ tool: call.name, dangerLevel, args: call.arguments },
|
|
2131
|
+
{ configDir: this.hookConfigDir, onSummary: (msg) => console.log(theme.dim(` [hook] ${msg}`)) }
|
|
2132
|
+
);
|
|
2133
|
+
const preDeny = preToolDecisions.find((d) => d.action === "deny");
|
|
2134
|
+
if (preDeny) {
|
|
2135
|
+
return {
|
|
2136
|
+
callId: call.id,
|
|
2137
|
+
content: `[Hook denied] ${preDeny.reason ?? `PreToolUse blocked ${call.name}`}. Do not retry without asking.`,
|
|
2138
|
+
isError: true
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
for (const d of preToolDecisions) {
|
|
2142
|
+
const warnings = [d.warning, ...d.warnings ?? []].filter(Boolean);
|
|
2143
|
+
for (const w of warnings) console.log(theme.warning(` \u26A0 Hook warning: ${w}`));
|
|
2144
|
+
}
|
|
1823
2145
|
const permission = checkPermissionWithProfile(
|
|
1824
2146
|
call.name,
|
|
1825
2147
|
call.arguments,
|
|
@@ -1835,6 +2157,14 @@ var ToolExecutor = class {
|
|
|
1835
2157
|
}
|
|
1836
2158
|
);
|
|
1837
2159
|
const networkPermission = checkNetworkPolicy(call.name, call.arguments, this.networkPolicy);
|
|
2160
|
+
if (networkPermission?.action === "confirm" || permission.action === "confirm") {
|
|
2161
|
+
runLifecycleHooks(
|
|
2162
|
+
this.hookConfig,
|
|
2163
|
+
"PermissionRequest",
|
|
2164
|
+
{ tool: call.name, dangerLevel, args: call.arguments, reason: networkPermission?.reason ?? permission.reason },
|
|
2165
|
+
{ configDir: this.hookConfigDir, onSummary: (msg) => console.log(theme.dim(" [hook] " + msg)) }
|
|
2166
|
+
);
|
|
2167
|
+
}
|
|
1838
2168
|
if (networkPermission?.action === "deny") {
|
|
1839
2169
|
const reason = networkPermission.reason ? ` (${networkPermission.reason})` : "";
|
|
1840
2170
|
return {
|
|
@@ -1859,11 +2189,13 @@ var ToolExecutor = class {
|
|
|
1859
2189
|
const wasTruncated = content !== rawContent;
|
|
1860
2190
|
this.printToolResult(call.name, rawContent, false, wasTruncated);
|
|
1861
2191
|
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
|
|
2192
|
+
runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
|
|
1862
2193
|
return { callId: call.id, content, isError: false };
|
|
1863
2194
|
} catch (err) {
|
|
1864
2195
|
const message = err instanceof Error ? err.message : String(err);
|
|
1865
2196
|
this.printToolResult(call.name, message, true, false);
|
|
1866
2197
|
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
|
|
2198
|
+
runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
|
|
1867
2199
|
return { callId: call.id, content: message, isError: true };
|
|
1868
2200
|
}
|
|
1869
2201
|
}
|
|
@@ -1879,6 +2211,40 @@ var ToolExecutor = class {
|
|
|
1879
2211
|
};
|
|
1880
2212
|
}
|
|
1881
2213
|
}
|
|
2214
|
+
if (this.sessionAutoMode && networkPermission?.action !== "confirm") {
|
|
2215
|
+
const classification = defaultActionClassifier.classify(call, dangerLevel, {
|
|
2216
|
+
workspaceRoot: this.workspaceRoot,
|
|
2217
|
+
tempDirs: [tmpdir()]
|
|
2218
|
+
});
|
|
2219
|
+
if (classification.decision === "deny") {
|
|
2220
|
+
recordRecentlyDeniedAutoAction(call, classification);
|
|
2221
|
+
return {
|
|
2222
|
+
callId: call.id,
|
|
2223
|
+
content: `[Auto denied] ${classification.reason}. Do not retry without asking.`,
|
|
2224
|
+
isError: true
|
|
2225
|
+
};
|
|
2226
|
+
}
|
|
2227
|
+
if (classification.decision === "auto-approve") {
|
|
2228
|
+
this.printToolCall(call);
|
|
2229
|
+
if (dangerLevel === "write") this.printDiffPreview(call);
|
|
2230
|
+
console.log(theme.warning(` \u26A1 Auto-approved (/auto: ${classification.ruleId})`));
|
|
2231
|
+
try {
|
|
2232
|
+
const rawContent = await runTool(tool, call.arguments, call.name);
|
|
2233
|
+
const content = truncateOutput(rawContent, call.name);
|
|
2234
|
+
const wasTruncated = content !== rawContent;
|
|
2235
|
+
this.printToolResult(call.name, rawContent, false, wasTruncated);
|
|
2236
|
+
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
|
|
2237
|
+
runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
|
|
2238
|
+
return { callId: call.id, content, isError: false };
|
|
2239
|
+
} catch (err) {
|
|
2240
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2241
|
+
this.printToolResult(call.name, message, true, false);
|
|
2242
|
+
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
|
|
2243
|
+
runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
|
|
2244
|
+
return { callId: call.id, content: message, isError: true };
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
1882
2248
|
if (this.sessionAutoApprove && dangerLevel === "write") {
|
|
1883
2249
|
this.printToolCall(call);
|
|
1884
2250
|
if (dangerLevel === "write") this.printDiffPreview(call);
|
|
@@ -1889,11 +2255,13 @@ var ToolExecutor = class {
|
|
|
1889
2255
|
const wasTruncated = content !== rawContent;
|
|
1890
2256
|
this.printToolResult(call.name, rawContent, false, wasTruncated);
|
|
1891
2257
|
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
|
|
2258
|
+
runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
|
|
1892
2259
|
return { callId: call.id, content, isError: false };
|
|
1893
2260
|
} catch (err) {
|
|
1894
2261
|
const message = err instanceof Error ? err.message : String(err);
|
|
1895
2262
|
this.printToolResult(call.name, message, true, false);
|
|
1896
2263
|
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
|
|
2264
|
+
runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
|
|
1897
2265
|
return { callId: call.id, content: message, isError: true };
|
|
1898
2266
|
}
|
|
1899
2267
|
}
|
|
@@ -1927,11 +2295,13 @@ var ToolExecutor = class {
|
|
|
1927
2295
|
const wasTruncated = content !== rawContent;
|
|
1928
2296
|
this.printToolResult(call.name, rawContent, false, wasTruncated);
|
|
1929
2297
|
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
|
|
2298
|
+
runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
|
|
1930
2299
|
return { callId: call.id, content, isError: false };
|
|
1931
2300
|
} catch (err) {
|
|
1932
2301
|
const message = err instanceof Error ? err.message : String(err);
|
|
1933
2302
|
this.printToolResult(call.name, message, true, false);
|
|
1934
2303
|
runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
|
|
2304
|
+
runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
|
|
1935
2305
|
return { callId: call.id, content: message, isError: true };
|
|
1936
2306
|
}
|
|
1937
2307
|
}
|
|
@@ -2042,7 +2412,7 @@ var ToolExecutor = class {
|
|
|
2042
2412
|
rl.resume();
|
|
2043
2413
|
process.stdout.write(prompt);
|
|
2044
2414
|
this.confirming = true;
|
|
2045
|
-
return new Promise((
|
|
2415
|
+
return new Promise((resolve7) => {
|
|
2046
2416
|
let completed = false;
|
|
2047
2417
|
const cleanup = (result) => {
|
|
2048
2418
|
if (completed) return;
|
|
@@ -2052,7 +2422,7 @@ var ToolExecutor = class {
|
|
|
2052
2422
|
rl.pause();
|
|
2053
2423
|
rlAny.output = savedOutput;
|
|
2054
2424
|
this.confirming = false;
|
|
2055
|
-
|
|
2425
|
+
resolve7(result);
|
|
2056
2426
|
};
|
|
2057
2427
|
const onLine = (line) => {
|
|
2058
2428
|
const trimmed = line.trim();
|
|
@@ -2119,10 +2489,10 @@ var ToolExecutor = class {
|
|
|
2119
2489
|
const filePath = String(call.arguments["path"] ?? "");
|
|
2120
2490
|
const newContent = String(call.arguments["content"] ?? "");
|
|
2121
2491
|
if (!filePath) return;
|
|
2122
|
-
if (
|
|
2492
|
+
if (existsSync6(filePath)) {
|
|
2123
2493
|
let oldContent;
|
|
2124
2494
|
try {
|
|
2125
|
-
oldContent =
|
|
2495
|
+
oldContent = readFileSync5(filePath, "utf-8");
|
|
2126
2496
|
} catch {
|
|
2127
2497
|
return;
|
|
2128
2498
|
}
|
|
@@ -2145,7 +2515,7 @@ var ToolExecutor = class {
|
|
|
2145
2515
|
}
|
|
2146
2516
|
} else if (call.name === "edit_file") {
|
|
2147
2517
|
const filePath = String(call.arguments["path"] ?? "");
|
|
2148
|
-
if (!filePath || !
|
|
2518
|
+
if (!filePath || !existsSync6(filePath)) return;
|
|
2149
2519
|
const oldStr = call.arguments["old_str"];
|
|
2150
2520
|
const newStr = call.arguments["new_str"];
|
|
2151
2521
|
if (oldStr !== void 0) {
|
|
@@ -2172,7 +2542,7 @@ var ToolExecutor = class {
|
|
|
2172
2542
|
const to = Number(call.arguments["delete_to_line"] ?? from);
|
|
2173
2543
|
let fileContent;
|
|
2174
2544
|
try {
|
|
2175
|
-
fileContent =
|
|
2545
|
+
fileContent = readFileSync5(filePath, "utf-8");
|
|
2176
2546
|
} catch {
|
|
2177
2547
|
return;
|
|
2178
2548
|
}
|
|
@@ -2226,7 +2596,7 @@ var ToolExecutor = class {
|
|
|
2226
2596
|
rl.resume();
|
|
2227
2597
|
process.stdout.write(color("Proceed? [y/N] (type y + Enter to confirm) "));
|
|
2228
2598
|
this.confirming = true;
|
|
2229
|
-
return new Promise((
|
|
2599
|
+
return new Promise((resolve7) => {
|
|
2230
2600
|
let completed = false;
|
|
2231
2601
|
const cleanup = (answer) => {
|
|
2232
2602
|
if (completed) return;
|
|
@@ -2236,7 +2606,7 @@ var ToolExecutor = class {
|
|
|
2236
2606
|
rl.pause();
|
|
2237
2607
|
rlAny.output = savedOutput;
|
|
2238
2608
|
this.confirming = false;
|
|
2239
|
-
|
|
2609
|
+
resolve7(answer === "y");
|
|
2240
2610
|
};
|
|
2241
2611
|
const onLine = (line) => {
|
|
2242
2612
|
const trimmed = line.trim();
|
|
@@ -2264,11 +2634,11 @@ var ToolExecutor = class {
|
|
|
2264
2634
|
};
|
|
2265
2635
|
|
|
2266
2636
|
// src/tools/sensitive-paths.ts
|
|
2267
|
-
import { resolve as
|
|
2637
|
+
import { resolve as resolve5, sep as sep2, basename as basename2 } from "path";
|
|
2268
2638
|
import { homedir as homedir2 } from "os";
|
|
2269
2639
|
var home = homedir2();
|
|
2270
2640
|
function norm(p) {
|
|
2271
|
-
const abs =
|
|
2641
|
+
const abs = resolve5(p);
|
|
2272
2642
|
return process.platform === "win32" ? abs.toLowerCase() : abs;
|
|
2273
2643
|
}
|
|
2274
2644
|
function homeRel(p) {
|
|
@@ -2407,7 +2777,7 @@ Do NOT split a long document into many write_file(append=true) calls. That patte
|
|
|
2407
2777
|
}
|
|
2408
2778
|
undoStack.push(filePath, `write_file${appendMode ? " (append)" : ""}: ${filePath}`);
|
|
2409
2779
|
fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
|
|
2410
|
-
|
|
2780
|
+
mkdirSync2(dirname2(filePath), { recursive: true });
|
|
2411
2781
|
if (appendMode) {
|
|
2412
2782
|
appendFileSync(filePath, content, encoding);
|
|
2413
2783
|
} else {
|
|
@@ -2427,7 +2797,7 @@ Do NOT split a long document into many write_file(append=true) calls. That patte
|
|
|
2427
2797
|
};
|
|
2428
2798
|
|
|
2429
2799
|
// src/tools/builtin/edit-file.ts
|
|
2430
|
-
import { readFileSync as
|
|
2800
|
+
import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
|
|
2431
2801
|
|
|
2432
2802
|
// src/tools/builtin/patch-apply.ts
|
|
2433
2803
|
function parseUnifiedDiff(patch) {
|
|
@@ -2788,7 +3158,7 @@ Note: Path can be absolute or relative to cwd.`,
|
|
|
2788
3158
|
const filePath = String(args["path"] ?? "");
|
|
2789
3159
|
const encoding = args["encoding"] ?? "utf-8";
|
|
2790
3160
|
if (!filePath) throw new ToolError("edit_file", "path is required");
|
|
2791
|
-
if (!
|
|
3161
|
+
if (!existsSync7(filePath)) throw new ToolError("edit_file", `File not found: ${filePath}`);
|
|
2792
3162
|
const verdict = classifyWritePath(filePath);
|
|
2793
3163
|
if (verdict.sensitive && subAgentGuard.active) {
|
|
2794
3164
|
throw new ToolError(
|
|
@@ -2796,7 +3166,7 @@ Note: Path can be absolute or relative to cwd.`,
|
|
|
2796
3166
|
`Refused: sub-agents cannot edit sensitive paths \u2014 ${verdict.reason}. If this is genuinely needed, ask the parent agent to perform the edit so the user can approve it.`
|
|
2797
3167
|
);
|
|
2798
3168
|
}
|
|
2799
|
-
const original =
|
|
3169
|
+
const original = readFileSync6(filePath, encoding);
|
|
2800
3170
|
if (args["patch"] !== void 0) {
|
|
2801
3171
|
const patchText = String(args["patch"] ?? "");
|
|
2802
3172
|
const stopOnError = args["stop_on_error"] !== false;
|
|
@@ -2954,8 +3324,8 @@ function truncatePreview(str, maxLen = 80) {
|
|
|
2954
3324
|
}
|
|
2955
3325
|
|
|
2956
3326
|
// src/tools/builtin/list-dir.ts
|
|
2957
|
-
import { readdirSync as readdirSync3, statSync as statSync3, existsSync as
|
|
2958
|
-
import { join, basename as basename3 } from "path";
|
|
3327
|
+
import { readdirSync as readdirSync3, statSync as statSync3, existsSync as existsSync8 } from "fs";
|
|
3328
|
+
import { join as join3, basename as basename3 } from "path";
|
|
2959
3329
|
var listDirTool = {
|
|
2960
3330
|
definition: {
|
|
2961
3331
|
name: "list_dir",
|
|
@@ -2977,7 +3347,7 @@ var listDirTool = {
|
|
|
2977
3347
|
async execute(args) {
|
|
2978
3348
|
const dirPath = String(args["path"] ?? process.cwd());
|
|
2979
3349
|
const recursive = Boolean(args["recursive"] ?? false);
|
|
2980
|
-
if (!
|
|
3350
|
+
if (!existsSync8(dirPath)) {
|
|
2981
3351
|
const targetName = basename3(dirPath).toLowerCase();
|
|
2982
3352
|
const cwd = process.cwd();
|
|
2983
3353
|
const suggestions = [];
|
|
@@ -3035,11 +3405,11 @@ function listRecursive(basePath, indent, recursive, lines) {
|
|
|
3035
3405
|
if (entry.isDirectory()) {
|
|
3036
3406
|
lines.push(`${indent}\u{1F4C1} ${entry.name}/`);
|
|
3037
3407
|
if (recursive) {
|
|
3038
|
-
listRecursive(
|
|
3408
|
+
listRecursive(join3(basePath, entry.name), indent + " ", true, lines);
|
|
3039
3409
|
}
|
|
3040
3410
|
} else {
|
|
3041
3411
|
try {
|
|
3042
|
-
const stat = statSync3(
|
|
3412
|
+
const stat = statSync3(join3(basePath, entry.name));
|
|
3043
3413
|
const size = formatSize(stat.size);
|
|
3044
3414
|
lines.push(`${indent}\u{1F4C4} ${entry.name} (${size})`);
|
|
3045
3415
|
} catch {
|
|
@@ -3055,9 +3425,9 @@ function formatSize(bytes) {
|
|
|
3055
3425
|
}
|
|
3056
3426
|
|
|
3057
3427
|
// src/tools/builtin/grep-files.ts
|
|
3058
|
-
import { readdirSync as readdirSync4, readFileSync as
|
|
3428
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync7, statSync as statSync4, existsSync as existsSync9 } from "fs";
|
|
3059
3429
|
import { readFile } from "fs/promises";
|
|
3060
|
-
import { join as
|
|
3430
|
+
import { join as join4, relative } from "path";
|
|
3061
3431
|
var grepFilesTool = {
|
|
3062
3432
|
definition: {
|
|
3063
3433
|
name: "grep_files",
|
|
@@ -3108,7 +3478,7 @@ Supports regex. Automatically skips node_modules, dist, .git directories.`,
|
|
|
3108
3478
|
const contextLines = Math.max(0, Number(args["context_lines"] ?? 0));
|
|
3109
3479
|
const maxResults = Math.max(1, Number(args["max_results"] ?? 50));
|
|
3110
3480
|
if (!pattern) throw new ToolError("grep_files", "pattern is required");
|
|
3111
|
-
if (!
|
|
3481
|
+
if (!existsSync9(rootPath)) throw new ToolError("grep_files", `Path not found: ${rootPath}`);
|
|
3112
3482
|
const MAX_PATTERN_LENGTH = 1e3;
|
|
3113
3483
|
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
3114
3484
|
throw new ToolError("grep_files", `Pattern too long (${pattern.length} chars, max ${MAX_PATTERN_LENGTH}). Use a shorter pattern.`);
|
|
@@ -3202,11 +3572,11 @@ function collectFilePaths(rootPath, filePattern, maxFiles) {
|
|
|
3202
3572
|
if (paths.length >= maxFiles) return;
|
|
3203
3573
|
if (entry.isDirectory()) {
|
|
3204
3574
|
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
3205
|
-
walk(
|
|
3575
|
+
walk(join4(dirPath, entry.name));
|
|
3206
3576
|
} else if (entry.isFile()) {
|
|
3207
3577
|
if (isBinary(entry.name)) continue;
|
|
3208
3578
|
if (filePattern && !matchesFilePattern(entry.name, filePattern)) continue;
|
|
3209
|
-
const fullPath =
|
|
3579
|
+
const fullPath = join4(dirPath, entry.name);
|
|
3210
3580
|
try {
|
|
3211
3581
|
if (statSync4(fullPath).size > 1e6) continue;
|
|
3212
3582
|
} catch {
|
|
@@ -3261,7 +3631,7 @@ function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, re
|
|
|
3261
3631
|
}
|
|
3262
3632
|
let content;
|
|
3263
3633
|
try {
|
|
3264
|
-
content =
|
|
3634
|
+
content = readFileSync7(fullPath, "utf-8");
|
|
3265
3635
|
} catch {
|
|
3266
3636
|
return;
|
|
3267
3637
|
}
|
|
@@ -3292,8 +3662,8 @@ function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, re
|
|
|
3292
3662
|
}
|
|
3293
3663
|
|
|
3294
3664
|
// src/tools/builtin/glob-files.ts
|
|
3295
|
-
import { readdirSync as readdirSync5, statSync as statSync5, existsSync as
|
|
3296
|
-
import { join as
|
|
3665
|
+
import { readdirSync as readdirSync5, statSync as statSync5, existsSync as existsSync10 } from "fs";
|
|
3666
|
+
import { join as join5, relative as relative2, basename as basename4 } from "path";
|
|
3297
3667
|
var globFilesTool = {
|
|
3298
3668
|
definition: {
|
|
3299
3669
|
name: "glob_files",
|
|
@@ -3326,7 +3696,7 @@ Results sorted by most recent modification time. Automatically skips node_module
|
|
|
3326
3696
|
const rootPath = String(args["path"] ?? process.cwd());
|
|
3327
3697
|
const maxResults = Math.max(1, Number(args["max_results"] ?? 100));
|
|
3328
3698
|
if (!pattern) throw new ToolError("glob_files", "pattern is required");
|
|
3329
|
-
if (!
|
|
3699
|
+
if (!existsSync10(rootPath)) throw new ToolError("glob_files", `Path not found: ${rootPath}`);
|
|
3330
3700
|
const regex = globToRegex(pattern);
|
|
3331
3701
|
const matches = [];
|
|
3332
3702
|
collectMatchingFiles(rootPath, rootPath, regex, matches, maxResults);
|
|
@@ -3403,7 +3773,7 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
|
|
|
3403
3773
|
}
|
|
3404
3774
|
for (const entry of entries) {
|
|
3405
3775
|
if (results.length >= maxResults) break;
|
|
3406
|
-
const fullPath =
|
|
3776
|
+
const fullPath = join5(dirPath, entry.name);
|
|
3407
3777
|
if (entry.isDirectory()) {
|
|
3408
3778
|
if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
3409
3779
|
collectMatchingFiles(fullPath, rootPath, regex, results, maxResults);
|
|
@@ -3485,7 +3855,7 @@ var runInteractiveTool = {
|
|
|
3485
3855
|
PYTHONDONTWRITEBYTECODE: "1"
|
|
3486
3856
|
};
|
|
3487
3857
|
const prefixWarnings = [argsTypeWarning, stdinTypeWarning].filter(Boolean).join("");
|
|
3488
|
-
return new Promise((
|
|
3858
|
+
return new Promise((resolve7) => {
|
|
3489
3859
|
const child = spawn2(executable, cmdArgs.map(String), {
|
|
3490
3860
|
cwd: process.cwd(),
|
|
3491
3861
|
env,
|
|
@@ -3518,22 +3888,22 @@ var runInteractiveTool = {
|
|
|
3518
3888
|
setTimeout(writeNextLine, 400);
|
|
3519
3889
|
const timer = setTimeout(() => {
|
|
3520
3890
|
child.kill();
|
|
3521
|
-
|
|
3891
|
+
resolve7(`${prefixWarnings}[Timeout after ${timeout}ms]
|
|
3522
3892
|
${buildOutput(stdout, stderr)}`);
|
|
3523
3893
|
}, timeout);
|
|
3524
3894
|
child.on("close", (code) => {
|
|
3525
3895
|
clearTimeout(timer);
|
|
3526
3896
|
const output = buildOutput(stdout, stderr);
|
|
3527
3897
|
if (code !== 0 && code !== null) {
|
|
3528
|
-
|
|
3898
|
+
resolve7(`${prefixWarnings}Exit code ${code}:
|
|
3529
3899
|
${output}`);
|
|
3530
3900
|
} else {
|
|
3531
|
-
|
|
3901
|
+
resolve7(`${prefixWarnings}${output || "(no output)"}`);
|
|
3532
3902
|
}
|
|
3533
3903
|
});
|
|
3534
3904
|
child.on("error", (err) => {
|
|
3535
3905
|
clearTimeout(timer);
|
|
3536
|
-
|
|
3906
|
+
resolve7(
|
|
3537
3907
|
`${prefixWarnings}Failed to start process "${executable}": ${err.message}
|
|
3538
3908
|
Hint: On Windows, use the full path to the executable, e.g.:
|
|
3539
3909
|
C:\\Users\\Jinzd\\anaconda3\\envs\\python312\\python.exe`
|
|
@@ -4143,7 +4513,7 @@ ${preamble}`;
|
|
|
4143
4513
|
}
|
|
4144
4514
|
|
|
4145
4515
|
// src/tools/builtin/save-last-response.ts
|
|
4146
|
-
import { mkdirSync as
|
|
4516
|
+
import { mkdirSync as mkdirSync3, unlinkSync as unlinkSync2, rmdirSync as rmdirSync2 } from "fs";
|
|
4147
4517
|
import { dirname as dirname3 } from "path";
|
|
4148
4518
|
var lastResponseStore = { content: "" };
|
|
4149
4519
|
function cleanupRejectedTeeFile(filePath) {
|
|
@@ -4193,7 +4563,7 @@ Any of these triggers means use save_last_response, NOT write_file:
|
|
|
4193
4563
|
throw new ToolError("save_last_response", "No content to save: AI has not produced any response yet, or the last response was empty.");
|
|
4194
4564
|
}
|
|
4195
4565
|
undoStack.push(filePath, `save_last_response: ${filePath}`);
|
|
4196
|
-
|
|
4566
|
+
mkdirSync3(dirname3(filePath), { recursive: true });
|
|
4197
4567
|
atomicWriteFileSync(filePath, content);
|
|
4198
4568
|
const lines = content.split("\n").length;
|
|
4199
4569
|
return `File saved: ${filePath} (${lines} lines, ${content.length} bytes)`;
|
|
@@ -4201,11 +4571,11 @@ Any of these triggers means use save_last_response, NOT write_file:
|
|
|
4201
4571
|
};
|
|
4202
4572
|
|
|
4203
4573
|
// src/tools/builtin/save-memory.ts
|
|
4204
|
-
import { existsSync as
|
|
4205
|
-
import { join as
|
|
4574
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8, statSync as statSync6, mkdirSync as mkdirSync4 } from "fs";
|
|
4575
|
+
import { join as join6 } from "path";
|
|
4206
4576
|
import { homedir as homedir3 } from "os";
|
|
4207
4577
|
function getMemoryFilePath() {
|
|
4208
|
-
return
|
|
4578
|
+
return join6(homedir3(), CONFIG_DIR_NAME, MEMORY_FILE_NAME);
|
|
4209
4579
|
}
|
|
4210
4580
|
function formatTimestamp() {
|
|
4211
4581
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -4229,16 +4599,16 @@ var saveMemoryTool = {
|
|
|
4229
4599
|
const content = String(args["content"] ?? "").trim();
|
|
4230
4600
|
if (!content) throw new ToolError("save_memory", "content is required");
|
|
4231
4601
|
const memoryPath = getMemoryFilePath();
|
|
4232
|
-
const configDir =
|
|
4233
|
-
if (!
|
|
4234
|
-
|
|
4602
|
+
const configDir = join6(homedir3(), CONFIG_DIR_NAME);
|
|
4603
|
+
if (!existsSync11(configDir)) {
|
|
4604
|
+
mkdirSync4(configDir, { recursive: true });
|
|
4235
4605
|
}
|
|
4236
4606
|
const timestamp = formatTimestamp();
|
|
4237
4607
|
const entry = `
|
|
4238
4608
|
## ${timestamp}
|
|
4239
4609
|
${content}
|
|
4240
4610
|
`;
|
|
4241
|
-
const previous =
|
|
4611
|
+
const previous = existsSync11(memoryPath) ? readFileSync8(memoryPath, "utf-8") : "";
|
|
4242
4612
|
atomicWriteFileSync(memoryPath, previous + entry);
|
|
4243
4613
|
const byteSize = statSync6(memoryPath).size;
|
|
4244
4614
|
return `Memory saved successfully. File size: ${byteSize} bytes in ${MEMORY_FILE_NAME}`;
|
|
@@ -4285,7 +4655,7 @@ function promptUser(rl, question) {
|
|
|
4285
4655
|
console.log();
|
|
4286
4656
|
console.log(chalk4.cyan("\u2753 ") + chalk4.bold(question));
|
|
4287
4657
|
process.stdout.write(chalk4.cyan("> "));
|
|
4288
|
-
return new Promise((
|
|
4658
|
+
return new Promise((resolve7) => {
|
|
4289
4659
|
let completed = false;
|
|
4290
4660
|
const cleanup = (answer) => {
|
|
4291
4661
|
if (completed) return;
|
|
@@ -4295,7 +4665,7 @@ function promptUser(rl, question) {
|
|
|
4295
4665
|
rl.pause();
|
|
4296
4666
|
rlAny.output = savedOutput;
|
|
4297
4667
|
askUserContext.prompting = false;
|
|
4298
|
-
|
|
4668
|
+
resolve7(answer);
|
|
4299
4669
|
};
|
|
4300
4670
|
const onLine = (line) => {
|
|
4301
4671
|
cleanup(line);
|
|
@@ -4775,11 +5145,27 @@ var spawnAgentTool = {
|
|
|
4775
5145
|
if (!ctx.provider) {
|
|
4776
5146
|
throw new ToolError("spawn_agent", "provider not initialized (context not injected)");
|
|
4777
5147
|
}
|
|
5148
|
+
const hookConfig = ctx.configManager?.get("hooks") ?? void 0;
|
|
5149
|
+
const hookConfigDir = ctx.configManager?.getConfigDir() ?? process.cwd();
|
|
5150
|
+
const taskCount = singleTask ? 1 : tasksArr.length;
|
|
5151
|
+
const hookPayload = {
|
|
5152
|
+
task: singleTask || void 0,
|
|
5153
|
+
tasks: tasksArr.length > 0 ? tasksArr : void 0,
|
|
5154
|
+
taskCount,
|
|
5155
|
+
maxRounds
|
|
5156
|
+
};
|
|
5157
|
+
const startDecisions = runLifecycleHooks(hookConfig, "SubagentStart", hookPayload, hookConfigDir);
|
|
5158
|
+
const startDeny = startDecisions.find((d) => d.action === "deny");
|
|
5159
|
+
if (startDeny) {
|
|
5160
|
+
throw new ToolError("spawn_agent", "SubagentStart hook denied: " + (startDeny.reason ?? "denied"));
|
|
5161
|
+
}
|
|
5162
|
+
let subagentStatus = "error";
|
|
4778
5163
|
const guardWasActive = subAgentGuard.active;
|
|
4779
5164
|
subAgentGuard.active = true;
|
|
4780
5165
|
try {
|
|
4781
5166
|
if (singleTask) {
|
|
4782
5167
|
const { content, usage } = await runSubAgent(singleTask, maxRounds, null, ctx);
|
|
5168
|
+
subagentStatus = "ok";
|
|
4783
5169
|
return [
|
|
4784
5170
|
"## Sub-Agent Result",
|
|
4785
5171
|
"",
|
|
@@ -4808,8 +5194,10 @@ var spawnAgentTool = {
|
|
|
4808
5194
|
});
|
|
4809
5195
|
lines.push("---");
|
|
4810
5196
|
lines.push(`Total token usage: ${totalIn} input, ${totalOut} output`);
|
|
5197
|
+
subagentStatus = "ok";
|
|
4811
5198
|
return lines.join("\n");
|
|
4812
5199
|
} finally {
|
|
5200
|
+
runLifecycleHooks(hookConfig, "SubagentStop", { ...hookPayload, status: subagentStatus }, hookConfigDir);
|
|
4813
5201
|
subAgentGuard.active = guardWasActive;
|
|
4814
5202
|
}
|
|
4815
5203
|
}
|
|
@@ -5032,14 +5420,14 @@ var taskStopTool = {
|
|
|
5032
5420
|
|
|
5033
5421
|
// src/tools/builtin/git-tools.ts
|
|
5034
5422
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
5035
|
-
import { existsSync as
|
|
5036
|
-
import { join as
|
|
5423
|
+
import { existsSync as existsSync12 } from "fs";
|
|
5424
|
+
import { join as join7 } from "path";
|
|
5037
5425
|
function assertGitRepo(cwd) {
|
|
5038
5426
|
let dir = cwd;
|
|
5039
5427
|
const root = dir.split(/[\\/]/)[0] + (dir.includes("\\") ? "\\" : "/");
|
|
5040
5428
|
while (dir && dir !== root) {
|
|
5041
|
-
if (
|
|
5042
|
-
const parent =
|
|
5429
|
+
if (existsSync12(join7(dir, ".git"))) return;
|
|
5430
|
+
const parent = join7(dir, "..");
|
|
5043
5431
|
if (parent === dir) break;
|
|
5044
5432
|
dir = parent;
|
|
5045
5433
|
}
|
|
@@ -5304,9 +5692,9 @@ ${commitOutput.trim()}`;
|
|
|
5304
5692
|
};
|
|
5305
5693
|
|
|
5306
5694
|
// src/tools/builtin/notebook-edit.ts
|
|
5307
|
-
import { readFileSync as
|
|
5695
|
+
import { readFileSync as readFileSync9, existsSync as existsSync13 } from "fs";
|
|
5308
5696
|
import { writeFile } from "fs/promises";
|
|
5309
|
-
import { resolve as
|
|
5697
|
+
import { resolve as resolve6, extname as extname2 } from "path";
|
|
5310
5698
|
var notebookEditTool = {
|
|
5311
5699
|
definition: {
|
|
5312
5700
|
name: "notebook_edit",
|
|
@@ -5355,14 +5743,14 @@ var notebookEditTool = {
|
|
|
5355
5743
|
if (!Number.isInteger(cellIndexRaw) || cellIndexRaw < 1) {
|
|
5356
5744
|
throw new ToolError("notebook_edit", "cell_index must be a positive integer (1-based)");
|
|
5357
5745
|
}
|
|
5358
|
-
const absPath =
|
|
5746
|
+
const absPath = resolve6(filePath);
|
|
5359
5747
|
if (extname2(absPath).toLowerCase() !== ".ipynb") {
|
|
5360
5748
|
throw new ToolError("notebook_edit", "path must point to a .ipynb file");
|
|
5361
5749
|
}
|
|
5362
|
-
if (!
|
|
5750
|
+
if (!existsSync13(absPath)) {
|
|
5363
5751
|
throw new ToolError("notebook_edit", `Notebook not found: ${filePath}`);
|
|
5364
5752
|
}
|
|
5365
|
-
const raw =
|
|
5753
|
+
const raw = readFileSync9(absPath, "utf-8");
|
|
5366
5754
|
const nb = parseNotebook(raw);
|
|
5367
5755
|
const cellIdx0 = cellIndexRaw - 1;
|
|
5368
5756
|
undoStack.push(absPath, `notebook_edit (${action}): ${filePath}`);
|
|
@@ -5779,8 +6167,8 @@ function estimateToolDefinitionTokens(def) {
|
|
|
5779
6167
|
|
|
5780
6168
|
// src/tools/registry.ts
|
|
5781
6169
|
import { pathToFileURL } from "url";
|
|
5782
|
-
import { existsSync as
|
|
5783
|
-
import { join as
|
|
6170
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync5, readdirSync as readdirSync6 } from "fs";
|
|
6171
|
+
import { join as join8 } from "path";
|
|
5784
6172
|
var ToolRegistry = class {
|
|
5785
6173
|
tools = /* @__PURE__ */ new Map();
|
|
5786
6174
|
pluginToolNames = /* @__PURE__ */ new Set();
|
|
@@ -5941,9 +6329,9 @@ var ToolRegistry = class {
|
|
|
5941
6329
|
* Returns the number of successfully loaded plugins.
|
|
5942
6330
|
*/
|
|
5943
6331
|
async loadPlugins(pluginsDir, allowPlugins = false) {
|
|
5944
|
-
if (!
|
|
6332
|
+
if (!existsSync14(pluginsDir)) {
|
|
5945
6333
|
try {
|
|
5946
|
-
|
|
6334
|
+
mkdirSync5(pluginsDir, { recursive: true });
|
|
5947
6335
|
} catch {
|
|
5948
6336
|
}
|
|
5949
6337
|
return 0;
|
|
@@ -5967,12 +6355,12 @@ var ToolRegistry = class {
|
|
|
5967
6355
|
process.stderr.write(
|
|
5968
6356
|
`
|
|
5969
6357
|
[plugins] \u26A0 Loading ${files.length} plugin(s) with FULL system privileges:
|
|
5970
|
-
` + files.map((f) => ` + ${
|
|
6358
|
+
` + files.map((f) => ` + ${join8(pluginsDir, f)}`).join("\n") + "\n\n"
|
|
5971
6359
|
);
|
|
5972
6360
|
let loaded = 0;
|
|
5973
6361
|
for (const file of files) {
|
|
5974
6362
|
try {
|
|
5975
|
-
const fileUrl = pathToFileURL(
|
|
6363
|
+
const fileUrl = pathToFileURL(join8(pluginsDir, file)).href;
|
|
5976
6364
|
const mod = await import(fileUrl);
|
|
5977
6365
|
const tool = mod.tool ?? mod.default?.tool ?? mod.default;
|
|
5978
6366
|
if (!tool || typeof tool.execute !== "function" || !tool.definition?.name) {
|
|
@@ -6004,6 +6392,16 @@ export {
|
|
|
6004
6392
|
theme,
|
|
6005
6393
|
undoStack,
|
|
6006
6394
|
renderDiff,
|
|
6395
|
+
listHooks,
|
|
6396
|
+
trustHook,
|
|
6397
|
+
untrustHook,
|
|
6398
|
+
getPendingHookTrust,
|
|
6399
|
+
runLifecycleHooks,
|
|
6400
|
+
runHook,
|
|
6401
|
+
recordRecentlyDeniedAutoAction,
|
|
6402
|
+
getRecentlyDeniedAutoActions,
|
|
6403
|
+
clearRecentlyDeniedAutoActions,
|
|
6404
|
+
defaultActionClassifier,
|
|
6007
6405
|
isInterrupted,
|
|
6008
6406
|
requestInterrupt,
|
|
6009
6407
|
resetInterrupt,
|
|
@@ -6011,7 +6409,6 @@ export {
|
|
|
6011
6409
|
rlInternal,
|
|
6012
6410
|
groupCallsByPhase,
|
|
6013
6411
|
runSafePhases,
|
|
6014
|
-
runHook,
|
|
6015
6412
|
checkPermissionWithProfile,
|
|
6016
6413
|
formatPermissionProfileWarning,
|
|
6017
6414
|
checkNetworkPolicy,
|