jinzd-ai-cli 0.4.211 → 0.4.212

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 +23 -0
  2. package/README.zh-CN.md +23 -0
  3. package/dist/{batch-DE4RXKZD.js → batch-WIKFEOAZ.js} +2 -2
  4. package/dist/{chat-index-UBCWHBLR.js → chat-index-R2E27VXN.js} +1 -1
  5. package/dist/{chunk-E5XCM4A6.js → chunk-GBBVCZ4W.js} +1 -1
  6. package/dist/{chunk-C3OU2OPF.js → chunk-HW2ALWQJ.js} +2 -2
  7. package/dist/{chunk-6NS6643Y.js → chunk-HX6N6QEY.js} +1 -1
  8. package/dist/{chunk-JBWA73GK.js → chunk-IFZX26K7.js} +1 -1
  9. package/dist/{chunk-ZY2N2N6T.js → chunk-KE4B3NOQ.js} +1 -1
  10. package/dist/{chunk-BE6ERF7M.js → chunk-LU6FBJQ5.js} +1 -1
  11. package/dist/{chunk-ZOPYREL5.js → chunk-N5LB3PPL.js} +258 -52
  12. package/dist/{chunk-UOROWTGG.js → chunk-NYCBOVNF.js} +26 -3
  13. package/dist/{chunk-W7UKO3PS.js → chunk-OQGVGPEK.js} +5 -0
  14. package/dist/{ci-XMUEX526.js → ci-X3LNUFZV.js} +2 -2
  15. package/dist/{constants-AWTIQIWG.js → constants-KDWG4KR4.js} +1 -1
  16. package/dist/{doctor-cli-SSI6ETFT.js → doctor-cli-TJTWIW7U.js} +4 -4
  17. package/dist/electron-server.js +468 -174
  18. package/dist/{hub-INUJND2G.js → hub-JWSV4MKC.js} +1 -1
  19. package/dist/index.js +142 -17
  20. package/dist/{run-tests-PACN4UYX.js → run-tests-D7YBY4XB.js} +1 -1
  21. package/dist/{run-tests-3MHWUF43.js → run-tests-H4Q2PRX6.js} +2 -2
  22. package/dist/{server-7USZJJAH.js → server-OFKGDRYI.js} +4 -4
  23. package/dist/{server-BTSKOPQI.js → server-VNCN7AY5.js} +99 -10
  24. package/dist/{task-orchestrator-OV4O25MX.js → task-orchestrator-AL2E642M.js} +4 -4
  25. package/dist/{usage-T2P6FTE7.js → usage-LB6REWPO.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-HX6N6QEY.js";
9
9
  import {
10
10
  runTool
11
- } from "./chunk-JBWA73GK.js";
11
+ } from "./chunk-IFZX26K7.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-GBBVCZ4W.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 existsSync5, readFileSync as readFileSync4 } 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: {
@@ -1790,6 +1939,7 @@ var ToolExecutor = class {
1790
1939
  allowedPermissionProfiles = [];
1791
1940
  workspaceRoot = process.cwd();
1792
1941
  networkPolicy;
1942
+ hookConfigDir = process.cwd();
1793
1943
  /** 注入 hooks 和 permission rules 配置 */
1794
1944
  setConfig(opts) {
1795
1945
  this.hookConfig = opts.hookConfig;
@@ -1800,6 +1950,7 @@ var ToolExecutor = class {
1800
1950
  if (opts.allowedPermissionProfiles) this.allowedPermissionProfiles = opts.allowedPermissionProfiles;
1801
1951
  if (opts.workspaceRoot) this.workspaceRoot = opts.workspaceRoot;
1802
1952
  if (opts.networkPolicy) this.networkPolicy = opts.networkPolicy;
1953
+ if (opts.hookConfigDir) this.hookConfigDir = opts.hookConfigDir;
1803
1954
  }
1804
1955
  async execute(call) {
1805
1956
  return runWithSessionKey(this.sessionKey, () => this.executeInner(call));
@@ -1820,6 +1971,24 @@ var ToolExecutor = class {
1820
1971
  dangerLevel,
1821
1972
  args: JSON.stringify(call.arguments).slice(0, 200)
1822
1973
  });
1974
+ const preToolDecisions = runLifecycleHooks(
1975
+ this.hookConfig,
1976
+ "PreToolUse",
1977
+ { tool: call.name, dangerLevel, args: call.arguments },
1978
+ { configDir: this.hookConfigDir, onSummary: (msg) => console.log(theme.dim(` [hook] ${msg}`)) }
1979
+ );
1980
+ const preDeny = preToolDecisions.find((d) => d.action === "deny");
1981
+ if (preDeny) {
1982
+ return {
1983
+ callId: call.id,
1984
+ content: `[Hook denied] ${preDeny.reason ?? `PreToolUse blocked ${call.name}`}. Do not retry without asking.`,
1985
+ isError: true
1986
+ };
1987
+ }
1988
+ for (const d of preToolDecisions) {
1989
+ const warnings = [d.warning, ...d.warnings ?? []].filter(Boolean);
1990
+ for (const w of warnings) console.log(theme.warning(` \u26A0 Hook warning: ${w}`));
1991
+ }
1823
1992
  const permission = checkPermissionWithProfile(
1824
1993
  call.name,
1825
1994
  call.arguments,
@@ -1835,6 +2004,14 @@ var ToolExecutor = class {
1835
2004
  }
1836
2005
  );
1837
2006
  const networkPermission = checkNetworkPolicy(call.name, call.arguments, this.networkPolicy);
2007
+ if (networkPermission?.action === "confirm" || permission.action === "confirm") {
2008
+ runLifecycleHooks(
2009
+ this.hookConfig,
2010
+ "PermissionRequest",
2011
+ { tool: call.name, dangerLevel, args: call.arguments, reason: networkPermission?.reason ?? permission.reason },
2012
+ { configDir: this.hookConfigDir, onSummary: (msg) => console.log(theme.dim(" [hook] " + msg)) }
2013
+ );
2014
+ }
1838
2015
  if (networkPermission?.action === "deny") {
1839
2016
  const reason = networkPermission.reason ? ` (${networkPermission.reason})` : "";
1840
2017
  return {
@@ -1859,11 +2036,13 @@ var ToolExecutor = class {
1859
2036
  const wasTruncated = content !== rawContent;
1860
2037
  this.printToolResult(call.name, rawContent, false, wasTruncated);
1861
2038
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
2039
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
1862
2040
  return { callId: call.id, content, isError: false };
1863
2041
  } catch (err) {
1864
2042
  const message = err instanceof Error ? err.message : String(err);
1865
2043
  this.printToolResult(call.name, message, true, false);
1866
2044
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
2045
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
1867
2046
  return { callId: call.id, content: message, isError: true };
1868
2047
  }
1869
2048
  }
@@ -1889,11 +2068,13 @@ var ToolExecutor = class {
1889
2068
  const wasTruncated = content !== rawContent;
1890
2069
  this.printToolResult(call.name, rawContent, false, wasTruncated);
1891
2070
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
2071
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
1892
2072
  return { callId: call.id, content, isError: false };
1893
2073
  } catch (err) {
1894
2074
  const message = err instanceof Error ? err.message : String(err);
1895
2075
  this.printToolResult(call.name, message, true, false);
1896
2076
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
2077
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
1897
2078
  return { callId: call.id, content: message, isError: true };
1898
2079
  }
1899
2080
  }
@@ -1927,11 +2108,13 @@ var ToolExecutor = class {
1927
2108
  const wasTruncated = content !== rawContent;
1928
2109
  this.printToolResult(call.name, rawContent, false, wasTruncated);
1929
2110
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
2111
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
1930
2112
  return { callId: call.id, content, isError: false };
1931
2113
  } catch (err) {
1932
2114
  const message = err instanceof Error ? err.message : String(err);
1933
2115
  this.printToolResult(call.name, message, true, false);
1934
2116
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
2117
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
1935
2118
  return { callId: call.id, content: message, isError: true };
1936
2119
  }
1937
2120
  }
@@ -2119,10 +2302,10 @@ var ToolExecutor = class {
2119
2302
  const filePath = String(call.arguments["path"] ?? "");
2120
2303
  const newContent = String(call.arguments["content"] ?? "");
2121
2304
  if (!filePath) return;
2122
- if (existsSync4(filePath)) {
2305
+ if (existsSync5(filePath)) {
2123
2306
  let oldContent;
2124
2307
  try {
2125
- oldContent = readFileSync3(filePath, "utf-8");
2308
+ oldContent = readFileSync4(filePath, "utf-8");
2126
2309
  } catch {
2127
2310
  return;
2128
2311
  }
@@ -2145,7 +2328,7 @@ var ToolExecutor = class {
2145
2328
  }
2146
2329
  } else if (call.name === "edit_file") {
2147
2330
  const filePath = String(call.arguments["path"] ?? "");
2148
- if (!filePath || !existsSync4(filePath)) return;
2331
+ if (!filePath || !existsSync5(filePath)) return;
2149
2332
  const oldStr = call.arguments["old_str"];
2150
2333
  const newStr = call.arguments["new_str"];
2151
2334
  if (oldStr !== void 0) {
@@ -2172,7 +2355,7 @@ var ToolExecutor = class {
2172
2355
  const to = Number(call.arguments["delete_to_line"] ?? from);
2173
2356
  let fileContent;
2174
2357
  try {
2175
- fileContent = readFileSync3(filePath, "utf-8");
2358
+ fileContent = readFileSync4(filePath, "utf-8");
2176
2359
  } catch {
2177
2360
  return;
2178
2361
  }
@@ -2407,7 +2590,7 @@ Do NOT split a long document into many write_file(append=true) calls. That patte
2407
2590
  }
2408
2591
  undoStack.push(filePath, `write_file${appendMode ? " (append)" : ""}: ${filePath}`);
2409
2592
  fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
2410
- mkdirSync(dirname2(filePath), { recursive: true });
2593
+ mkdirSync2(dirname2(filePath), { recursive: true });
2411
2594
  if (appendMode) {
2412
2595
  appendFileSync(filePath, content, encoding);
2413
2596
  } else {
@@ -2427,7 +2610,7 @@ Do NOT split a long document into many write_file(append=true) calls. That patte
2427
2610
  };
2428
2611
 
2429
2612
  // src/tools/builtin/edit-file.ts
2430
- import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
2613
+ import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
2431
2614
 
2432
2615
  // src/tools/builtin/patch-apply.ts
2433
2616
  function parseUnifiedDiff(patch) {
@@ -2788,7 +2971,7 @@ Note: Path can be absolute or relative to cwd.`,
2788
2971
  const filePath = String(args["path"] ?? "");
2789
2972
  const encoding = args["encoding"] ?? "utf-8";
2790
2973
  if (!filePath) throw new ToolError("edit_file", "path is required");
2791
- if (!existsSync5(filePath)) throw new ToolError("edit_file", `File not found: ${filePath}`);
2974
+ if (!existsSync6(filePath)) throw new ToolError("edit_file", `File not found: ${filePath}`);
2792
2975
  const verdict = classifyWritePath(filePath);
2793
2976
  if (verdict.sensitive && subAgentGuard.active) {
2794
2977
  throw new ToolError(
@@ -2796,7 +2979,7 @@ Note: Path can be absolute or relative to cwd.`,
2796
2979
  `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
2980
  );
2798
2981
  }
2799
- const original = readFileSync4(filePath, encoding);
2982
+ const original = readFileSync5(filePath, encoding);
2800
2983
  if (args["patch"] !== void 0) {
2801
2984
  const patchText = String(args["patch"] ?? "");
2802
2985
  const stopOnError = args["stop_on_error"] !== false;
@@ -2954,8 +3137,8 @@ function truncatePreview(str, maxLen = 80) {
2954
3137
  }
2955
3138
 
2956
3139
  // 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";
3140
+ import { readdirSync as readdirSync3, statSync as statSync3, existsSync as existsSync7 } from "fs";
3141
+ import { join as join2, basename as basename3 } from "path";
2959
3142
  var listDirTool = {
2960
3143
  definition: {
2961
3144
  name: "list_dir",
@@ -2977,7 +3160,7 @@ var listDirTool = {
2977
3160
  async execute(args) {
2978
3161
  const dirPath = String(args["path"] ?? process.cwd());
2979
3162
  const recursive = Boolean(args["recursive"] ?? false);
2980
- if (!existsSync6(dirPath)) {
3163
+ if (!existsSync7(dirPath)) {
2981
3164
  const targetName = basename3(dirPath).toLowerCase();
2982
3165
  const cwd = process.cwd();
2983
3166
  const suggestions = [];
@@ -3035,11 +3218,11 @@ function listRecursive(basePath, indent, recursive, lines) {
3035
3218
  if (entry.isDirectory()) {
3036
3219
  lines.push(`${indent}\u{1F4C1} ${entry.name}/`);
3037
3220
  if (recursive) {
3038
- listRecursive(join(basePath, entry.name), indent + " ", true, lines);
3221
+ listRecursive(join2(basePath, entry.name), indent + " ", true, lines);
3039
3222
  }
3040
3223
  } else {
3041
3224
  try {
3042
- const stat = statSync3(join(basePath, entry.name));
3225
+ const stat = statSync3(join2(basePath, entry.name));
3043
3226
  const size = formatSize(stat.size);
3044
3227
  lines.push(`${indent}\u{1F4C4} ${entry.name} (${size})`);
3045
3228
  } catch {
@@ -3055,9 +3238,9 @@ function formatSize(bytes) {
3055
3238
  }
3056
3239
 
3057
3240
  // src/tools/builtin/grep-files.ts
3058
- import { readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync4, existsSync as existsSync7 } from "fs";
3241
+ import { readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync4, existsSync as existsSync8 } from "fs";
3059
3242
  import { readFile } from "fs/promises";
3060
- import { join as join2, relative } from "path";
3243
+ import { join as join3, relative } from "path";
3061
3244
  var grepFilesTool = {
3062
3245
  definition: {
3063
3246
  name: "grep_files",
@@ -3108,7 +3291,7 @@ Supports regex. Automatically skips node_modules, dist, .git directories.`,
3108
3291
  const contextLines = Math.max(0, Number(args["context_lines"] ?? 0));
3109
3292
  const maxResults = Math.max(1, Number(args["max_results"] ?? 50));
3110
3293
  if (!pattern) throw new ToolError("grep_files", "pattern is required");
3111
- if (!existsSync7(rootPath)) throw new ToolError("grep_files", `Path not found: ${rootPath}`);
3294
+ if (!existsSync8(rootPath)) throw new ToolError("grep_files", `Path not found: ${rootPath}`);
3112
3295
  const MAX_PATTERN_LENGTH = 1e3;
3113
3296
  if (pattern.length > MAX_PATTERN_LENGTH) {
3114
3297
  throw new ToolError("grep_files", `Pattern too long (${pattern.length} chars, max ${MAX_PATTERN_LENGTH}). Use a shorter pattern.`);
@@ -3202,11 +3385,11 @@ function collectFilePaths(rootPath, filePattern, maxFiles) {
3202
3385
  if (paths.length >= maxFiles) return;
3203
3386
  if (entry.isDirectory()) {
3204
3387
  if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
3205
- walk(join2(dirPath, entry.name));
3388
+ walk(join3(dirPath, entry.name));
3206
3389
  } else if (entry.isFile()) {
3207
3390
  if (isBinary(entry.name)) continue;
3208
3391
  if (filePattern && !matchesFilePattern(entry.name, filePattern)) continue;
3209
- const fullPath = join2(dirPath, entry.name);
3392
+ const fullPath = join3(dirPath, entry.name);
3210
3393
  try {
3211
3394
  if (statSync4(fullPath).size > 1e6) continue;
3212
3395
  } catch {
@@ -3261,7 +3444,7 @@ function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, re
3261
3444
  }
3262
3445
  let content;
3263
3446
  try {
3264
- content = readFileSync5(fullPath, "utf-8");
3447
+ content = readFileSync6(fullPath, "utf-8");
3265
3448
  } catch {
3266
3449
  return;
3267
3450
  }
@@ -3292,8 +3475,8 @@ function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, re
3292
3475
  }
3293
3476
 
3294
3477
  // 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";
3478
+ import { readdirSync as readdirSync5, statSync as statSync5, existsSync as existsSync9 } from "fs";
3479
+ import { join as join4, relative as relative2, basename as basename4 } from "path";
3297
3480
  var globFilesTool = {
3298
3481
  definition: {
3299
3482
  name: "glob_files",
@@ -3326,7 +3509,7 @@ Results sorted by most recent modification time. Automatically skips node_module
3326
3509
  const rootPath = String(args["path"] ?? process.cwd());
3327
3510
  const maxResults = Math.max(1, Number(args["max_results"] ?? 100));
3328
3511
  if (!pattern) throw new ToolError("glob_files", "pattern is required");
3329
- if (!existsSync8(rootPath)) throw new ToolError("glob_files", `Path not found: ${rootPath}`);
3512
+ if (!existsSync9(rootPath)) throw new ToolError("glob_files", `Path not found: ${rootPath}`);
3330
3513
  const regex = globToRegex(pattern);
3331
3514
  const matches = [];
3332
3515
  collectMatchingFiles(rootPath, rootPath, regex, matches, maxResults);
@@ -3403,7 +3586,7 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
3403
3586
  }
3404
3587
  for (const entry of entries) {
3405
3588
  if (results.length >= maxResults) break;
3406
- const fullPath = join3(dirPath, entry.name);
3589
+ const fullPath = join4(dirPath, entry.name);
3407
3590
  if (entry.isDirectory()) {
3408
3591
  if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".")) continue;
3409
3592
  collectMatchingFiles(fullPath, rootPath, regex, results, maxResults);
@@ -4143,7 +4326,7 @@ ${preamble}`;
4143
4326
  }
4144
4327
 
4145
4328
  // src/tools/builtin/save-last-response.ts
4146
- import { mkdirSync as mkdirSync2, unlinkSync as unlinkSync2, rmdirSync as rmdirSync2 } from "fs";
4329
+ import { mkdirSync as mkdirSync3, unlinkSync as unlinkSync2, rmdirSync as rmdirSync2 } from "fs";
4147
4330
  import { dirname as dirname3 } from "path";
4148
4331
  var lastResponseStore = { content: "" };
4149
4332
  function cleanupRejectedTeeFile(filePath) {
@@ -4193,7 +4376,7 @@ Any of these triggers means use save_last_response, NOT write_file:
4193
4376
  throw new ToolError("save_last_response", "No content to save: AI has not produced any response yet, or the last response was empty.");
4194
4377
  }
4195
4378
  undoStack.push(filePath, `save_last_response: ${filePath}`);
4196
- mkdirSync2(dirname3(filePath), { recursive: true });
4379
+ mkdirSync3(dirname3(filePath), { recursive: true });
4197
4380
  atomicWriteFileSync(filePath, content);
4198
4381
  const lines = content.split("\n").length;
4199
4382
  return `File saved: ${filePath} (${lines} lines, ${content.length} bytes)`;
@@ -4201,11 +4384,11 @@ Any of these triggers means use save_last_response, NOT write_file:
4201
4384
  };
4202
4385
 
4203
4386
  // 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";
4387
+ import { existsSync as existsSync10, readFileSync as readFileSync7, statSync as statSync6, mkdirSync as mkdirSync4 } from "fs";
4388
+ import { join as join5 } from "path";
4206
4389
  import { homedir as homedir3 } from "os";
4207
4390
  function getMemoryFilePath() {
4208
- return join4(homedir3(), CONFIG_DIR_NAME, MEMORY_FILE_NAME);
4391
+ return join5(homedir3(), CONFIG_DIR_NAME, MEMORY_FILE_NAME);
4209
4392
  }
4210
4393
  function formatTimestamp() {
4211
4394
  const now = /* @__PURE__ */ new Date();
@@ -4229,16 +4412,16 @@ var saveMemoryTool = {
4229
4412
  const content = String(args["content"] ?? "").trim();
4230
4413
  if (!content) throw new ToolError("save_memory", "content is required");
4231
4414
  const memoryPath = getMemoryFilePath();
4232
- const configDir = join4(homedir3(), CONFIG_DIR_NAME);
4233
- if (!existsSync9(configDir)) {
4234
- mkdirSync3(configDir, { recursive: true });
4415
+ const configDir = join5(homedir3(), CONFIG_DIR_NAME);
4416
+ if (!existsSync10(configDir)) {
4417
+ mkdirSync4(configDir, { recursive: true });
4235
4418
  }
4236
4419
  const timestamp = formatTimestamp();
4237
4420
  const entry = `
4238
4421
  ## ${timestamp}
4239
4422
  ${content}
4240
4423
  `;
4241
- const previous = existsSync9(memoryPath) ? readFileSync6(memoryPath, "utf-8") : "";
4424
+ const previous = existsSync10(memoryPath) ? readFileSync7(memoryPath, "utf-8") : "";
4242
4425
  atomicWriteFileSync(memoryPath, previous + entry);
4243
4426
  const byteSize = statSync6(memoryPath).size;
4244
4427
  return `Memory saved successfully. File size: ${byteSize} bytes in ${MEMORY_FILE_NAME}`;
@@ -4775,11 +4958,27 @@ var spawnAgentTool = {
4775
4958
  if (!ctx.provider) {
4776
4959
  throw new ToolError("spawn_agent", "provider not initialized (context not injected)");
4777
4960
  }
4961
+ const hookConfig = ctx.configManager?.get("hooks") ?? void 0;
4962
+ const hookConfigDir = ctx.configManager?.getConfigDir() ?? process.cwd();
4963
+ const taskCount = singleTask ? 1 : tasksArr.length;
4964
+ const hookPayload = {
4965
+ task: singleTask || void 0,
4966
+ tasks: tasksArr.length > 0 ? tasksArr : void 0,
4967
+ taskCount,
4968
+ maxRounds
4969
+ };
4970
+ const startDecisions = runLifecycleHooks(hookConfig, "SubagentStart", hookPayload, hookConfigDir);
4971
+ const startDeny = startDecisions.find((d) => d.action === "deny");
4972
+ if (startDeny) {
4973
+ throw new ToolError("spawn_agent", "SubagentStart hook denied: " + (startDeny.reason ?? "denied"));
4974
+ }
4975
+ let subagentStatus = "error";
4778
4976
  const guardWasActive = subAgentGuard.active;
4779
4977
  subAgentGuard.active = true;
4780
4978
  try {
4781
4979
  if (singleTask) {
4782
4980
  const { content, usage } = await runSubAgent(singleTask, maxRounds, null, ctx);
4981
+ subagentStatus = "ok";
4783
4982
  return [
4784
4983
  "## Sub-Agent Result",
4785
4984
  "",
@@ -4808,8 +5007,10 @@ var spawnAgentTool = {
4808
5007
  });
4809
5008
  lines.push("---");
4810
5009
  lines.push(`Total token usage: ${totalIn} input, ${totalOut} output`);
5010
+ subagentStatus = "ok";
4811
5011
  return lines.join("\n");
4812
5012
  } finally {
5013
+ runLifecycleHooks(hookConfig, "SubagentStop", { ...hookPayload, status: subagentStatus }, hookConfigDir);
4813
5014
  subAgentGuard.active = guardWasActive;
4814
5015
  }
4815
5016
  }
@@ -5032,14 +5233,14 @@ var taskStopTool = {
5032
5233
 
5033
5234
  // src/tools/builtin/git-tools.ts
5034
5235
  import { execFileSync as execFileSync2 } from "child_process";
5035
- import { existsSync as existsSync10 } from "fs";
5036
- import { join as join5 } from "path";
5236
+ import { existsSync as existsSync11 } from "fs";
5237
+ import { join as join6 } from "path";
5037
5238
  function assertGitRepo(cwd) {
5038
5239
  let dir = cwd;
5039
5240
  const root = dir.split(/[\\/]/)[0] + (dir.includes("\\") ? "\\" : "/");
5040
5241
  while (dir && dir !== root) {
5041
- if (existsSync10(join5(dir, ".git"))) return;
5042
- const parent = join5(dir, "..");
5242
+ if (existsSync11(join6(dir, ".git"))) return;
5243
+ const parent = join6(dir, "..");
5043
5244
  if (parent === dir) break;
5044
5245
  dir = parent;
5045
5246
  }
@@ -5304,7 +5505,7 @@ ${commitOutput.trim()}`;
5304
5505
  };
5305
5506
 
5306
5507
  // src/tools/builtin/notebook-edit.ts
5307
- import { readFileSync as readFileSync7, existsSync as existsSync11 } from "fs";
5508
+ import { readFileSync as readFileSync8, existsSync as existsSync12 } from "fs";
5308
5509
  import { writeFile } from "fs/promises";
5309
5510
  import { resolve as resolve5, extname as extname2 } from "path";
5310
5511
  var notebookEditTool = {
@@ -5359,10 +5560,10 @@ var notebookEditTool = {
5359
5560
  if (extname2(absPath).toLowerCase() !== ".ipynb") {
5360
5561
  throw new ToolError("notebook_edit", "path must point to a .ipynb file");
5361
5562
  }
5362
- if (!existsSync11(absPath)) {
5563
+ if (!existsSync12(absPath)) {
5363
5564
  throw new ToolError("notebook_edit", `Notebook not found: ${filePath}`);
5364
5565
  }
5365
- const raw = readFileSync7(absPath, "utf-8");
5566
+ const raw = readFileSync8(absPath, "utf-8");
5366
5567
  const nb = parseNotebook(raw);
5367
5568
  const cellIdx0 = cellIndexRaw - 1;
5368
5569
  undoStack.push(absPath, `notebook_edit (${action}): ${filePath}`);
@@ -5779,8 +5980,8 @@ function estimateToolDefinitionTokens(def) {
5779
5980
 
5780
5981
  // src/tools/registry.ts
5781
5982
  import { pathToFileURL } from "url";
5782
- import { existsSync as existsSync12, mkdirSync as mkdirSync4, readdirSync as readdirSync6 } from "fs";
5783
- import { join as join6 } from "path";
5983
+ import { existsSync as existsSync13, mkdirSync as mkdirSync5, readdirSync as readdirSync6 } from "fs";
5984
+ import { join as join7 } from "path";
5784
5985
  var ToolRegistry = class {
5785
5986
  tools = /* @__PURE__ */ new Map();
5786
5987
  pluginToolNames = /* @__PURE__ */ new Set();
@@ -5941,9 +6142,9 @@ var ToolRegistry = class {
5941
6142
  * Returns the number of successfully loaded plugins.
5942
6143
  */
5943
6144
  async loadPlugins(pluginsDir, allowPlugins = false) {
5944
- if (!existsSync12(pluginsDir)) {
6145
+ if (!existsSync13(pluginsDir)) {
5945
6146
  try {
5946
- mkdirSync4(pluginsDir, { recursive: true });
6147
+ mkdirSync5(pluginsDir, { recursive: true });
5947
6148
  } catch {
5948
6149
  }
5949
6150
  return 0;
@@ -5967,12 +6168,12 @@ var ToolRegistry = class {
5967
6168
  process.stderr.write(
5968
6169
  `
5969
6170
  [plugins] \u26A0 Loading ${files.length} plugin(s) with FULL system privileges:
5970
- ` + files.map((f) => ` + ${join6(pluginsDir, f)}`).join("\n") + "\n\n"
6171
+ ` + files.map((f) => ` + ${join7(pluginsDir, f)}`).join("\n") + "\n\n"
5971
6172
  );
5972
6173
  let loaded = 0;
5973
6174
  for (const file of files) {
5974
6175
  try {
5975
- const fileUrl = pathToFileURL(join6(pluginsDir, file)).href;
6176
+ const fileUrl = pathToFileURL(join7(pluginsDir, file)).href;
5976
6177
  const mod = await import(fileUrl);
5977
6178
  const tool = mod.tool ?? mod.default?.tool ?? mod.default;
5978
6179
  if (!tool || typeof tool.execute !== "function" || !tool.definition?.name) {
@@ -6004,6 +6205,12 @@ export {
6004
6205
  theme,
6005
6206
  undoStack,
6006
6207
  renderDiff,
6208
+ listHooks,
6209
+ trustHook,
6210
+ untrustHook,
6211
+ getPendingHookTrust,
6212
+ runLifecycleHooks,
6213
+ runHook,
6007
6214
  isInterrupted,
6008
6215
  requestInterrupt,
6009
6216
  resetInterrupt,
@@ -6011,7 +6218,6 @@ export {
6011
6218
  rlInternal,
6012
6219
  groupCallsByPhase,
6013
6220
  runSafePhases,
6014
- runHook,
6015
6221
  checkPermissionWithProfile,
6016
6222
  formatPermissionProfileWarning,
6017
6223
  checkNetworkPolicy,