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.
Files changed (27) hide show
  1. package/README.md +30 -0
  2. package/README.zh-CN.md +30 -0
  3. package/dist/{batch-DE4RXKZD.js → batch-PKY63TME.js} +2 -2
  4. package/dist/{chat-index-UBCWHBLR.js → chat-index-R2E27VXN.js} +1 -1
  5. package/dist/{chunk-JBWA73GK.js → chunk-7NTFMLJJ.js} +1 -1
  6. package/dist/{chunk-C3OU2OPF.js → chunk-FGUKBIYO.js} +2 -2
  7. package/dist/{chunk-BE6ERF7M.js → chunk-G3PW3AOB.js} +1 -1
  8. package/dist/{chunk-E5XCM4A6.js → chunk-IWLVH32D.js} +1 -1
  9. package/dist/{chunk-ZY2N2N6T.js → chunk-J35V3IAH.js} +1 -1
  10. package/dist/{chunk-UOROWTGG.js → chunk-MRGWPCFQ.js} +26 -3
  11. package/dist/{chunk-W7UKO3PS.js → chunk-OQGVGPEK.js} +5 -0
  12. package/dist/{chunk-ZOPYREL5.js → chunk-THNECAAY.js} +464 -67
  13. package/dist/{chunk-6NS6643Y.js → chunk-UDMU4FUF.js} +1 -1
  14. package/dist/{ci-XMUEX526.js → ci-HBZ6CIW5.js} +2 -2
  15. package/dist/{constants-AWTIQIWG.js → constants-CAUBTSFW.js} +1 -1
  16. package/dist/{doctor-cli-SSI6ETFT.js → doctor-cli-5Z5UWYW6.js} +4 -4
  17. package/dist/electron-server.js +794 -241
  18. package/dist/{hub-INUJND2G.js → hub-ZRADBR76.js} +1 -1
  19. package/dist/index.js +205 -19
  20. package/dist/{run-tests-PACN4UYX.js → run-tests-7J3PNWVS.js} +1 -1
  21. package/dist/{run-tests-3MHWUF43.js → run-tests-DSOJBJWA.js} +2 -2
  22. package/dist/{server-7USZJJAH.js → server-PWBWHVYB.js} +4 -4
  23. package/dist/{server-BTSKOPQI.js → server-U4I7RI42.js} +175 -10
  24. package/dist/{task-orchestrator-OV4O25MX.js → task-orchestrator-RUXWRMQV.js} +4 -4
  25. package/dist/{usage-T2P6FTE7.js → usage-2P6TIBTN.js} +2 -2
  26. package/dist/web/client/app.js +1 -1
  27. 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-6NS6643Y.js";
8
+ } from "./chunk-UDMU4FUF.js";
9
9
  import {
10
10
  runTool
11
- } from "./chunk-JBWA73GK.js";
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-E5XCM4A6.js";
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 existsSync4, readFileSync as readFileSync3 } from "fs";
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: 5e3,
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((resolve6) => {
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
- resolve6(result);
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 (existsSync4(filePath)) {
2492
+ if (existsSync6(filePath)) {
2123
2493
  let oldContent;
2124
2494
  try {
2125
- oldContent = readFileSync3(filePath, "utf-8");
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 || !existsSync4(filePath)) return;
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 = readFileSync3(filePath, "utf-8");
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((resolve6) => {
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
- resolve6(answer === "y");
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 resolve4, sep as sep2, basename as basename2 } from "path";
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 = resolve4(p);
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
- mkdirSync(dirname2(filePath), { recursive: true });
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 readFileSync4, existsSync as existsSync5 } from "fs";
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 (!existsSync5(filePath)) throw new ToolError("edit_file", `File not found: ${filePath}`);
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 = readFileSync4(filePath, encoding);
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 existsSync6 } from "fs";
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 (!existsSync6(dirPath)) {
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(join(basePath, entry.name), indent + " ", true, lines);
3408
+ listRecursive(join3(basePath, entry.name), indent + " ", true, lines);
3039
3409
  }
3040
3410
  } else {
3041
3411
  try {
3042
- const stat = statSync3(join(basePath, entry.name));
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 readFileSync5, statSync as statSync4, existsSync as existsSync7 } from "fs";
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 join2, relative } from "path";
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 (!existsSync7(rootPath)) throw new ToolError("grep_files", `Path not found: ${rootPath}`);
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(join2(dirPath, entry.name));
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 = join2(dirPath, entry.name);
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 = readFileSync5(fullPath, "utf-8");
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 existsSync8 } from "fs";
3296
- import { join as join3, relative as relative2, basename as basename4 } from "path";
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 (!existsSync8(rootPath)) throw new ToolError("glob_files", `Path not found: ${rootPath}`);
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 = join3(dirPath, entry.name);
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((resolve6) => {
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
- resolve6(`${prefixWarnings}[Timeout after ${timeout}ms]
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
- resolve6(`${prefixWarnings}Exit code ${code}:
3898
+ resolve7(`${prefixWarnings}Exit code ${code}:
3529
3899
  ${output}`);
3530
3900
  } else {
3531
- resolve6(`${prefixWarnings}${output || "(no output)"}`);
3901
+ resolve7(`${prefixWarnings}${output || "(no output)"}`);
3532
3902
  }
3533
3903
  });
3534
3904
  child.on("error", (err) => {
3535
3905
  clearTimeout(timer);
3536
- resolve6(
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 mkdirSync2, unlinkSync as unlinkSync2, rmdirSync as rmdirSync2 } from "fs";
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
- mkdirSync2(dirname3(filePath), { recursive: true });
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 existsSync9, readFileSync as readFileSync6, statSync as statSync6, mkdirSync as mkdirSync3 } from "fs";
4205
- import { join as join4 } from "path";
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 join4(homedir3(), CONFIG_DIR_NAME, MEMORY_FILE_NAME);
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 = join4(homedir3(), CONFIG_DIR_NAME);
4233
- if (!existsSync9(configDir)) {
4234
- mkdirSync3(configDir, { recursive: true });
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 = existsSync9(memoryPath) ? readFileSync6(memoryPath, "utf-8") : "";
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((resolve6) => {
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
- resolve6(answer);
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 existsSync10 } from "fs";
5036
- import { join as join5 } from "path";
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 (existsSync10(join5(dir, ".git"))) return;
5042
- const parent = join5(dir, "..");
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 readFileSync7, existsSync as existsSync11 } from "fs";
5695
+ import { readFileSync as readFileSync9, existsSync as existsSync13 } from "fs";
5308
5696
  import { writeFile } from "fs/promises";
5309
- import { resolve as resolve5, extname as extname2 } from "path";
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 = resolve5(filePath);
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 (!existsSync11(absPath)) {
5750
+ if (!existsSync13(absPath)) {
5363
5751
  throw new ToolError("notebook_edit", `Notebook not found: ${filePath}`);
5364
5752
  }
5365
- const raw = readFileSync7(absPath, "utf-8");
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 existsSync12, mkdirSync as mkdirSync4, readdirSync as readdirSync6 } from "fs";
5783
- import { join as join6 } from "path";
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 (!existsSync12(pluginsDir)) {
6332
+ if (!existsSync14(pluginsDir)) {
5945
6333
  try {
5946
- mkdirSync4(pluginsDir, { recursive: true });
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) => ` + ${join6(pluginsDir, f)}`).join("\n") + "\n\n"
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(join6(pluginsDir, file)).href;
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,