jinzd-ai-cli 0.4.210 → 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 +122 -1
  2. package/README.zh-CN.md +63 -0
  3. package/dist/{batch-MFPYHOIL.js → batch-WIKFEOAZ.js} +2 -2
  4. package/dist/{chat-index-UBCWHBLR.js → chat-index-R2E27VXN.js} +1 -1
  5. package/dist/{chunk-GRJNQJA5.js → chunk-GBBVCZ4W.js} +1 -1
  6. package/dist/{chunk-7JWT3KXO.js → chunk-HW2ALWQJ.js} +2 -2
  7. package/dist/{chunk-E2ZWWE6Q.js → chunk-HX6N6QEY.js} +1 -1
  8. package/dist/{chunk-DJ35G5VO.js → chunk-IFZX26K7.js} +1 -1
  9. package/dist/{chunk-HZQVX7VF.js → chunk-KE4B3NOQ.js} +1 -1
  10. package/dist/{chunk-NWP7C6CC.js → chunk-LU6FBJQ5.js} +1 -1
  11. package/dist/{chunk-NEPFADHX.js → chunk-N5LB3PPL.js} +567 -87
  12. package/dist/{chunk-P6MTFCXB.js → chunk-NYCBOVNF.js} +62 -5
  13. package/dist/{chunk-W7UKO3PS.js → chunk-OQGVGPEK.js} +5 -0
  14. package/dist/{ci-QLJUDLMY.js → ci-X3LNUFZV.js} +2 -2
  15. package/dist/{constants-47OR72MV.js → constants-KDWG4KR4.js} +1 -1
  16. package/dist/{doctor-cli-32COMA5K.js → doctor-cli-TJTWIW7U.js} +4 -4
  17. package/dist/electron-server.js +947 -280
  18. package/dist/{hub-3NWJUCLK.js → hub-JWSV4MKC.js} +1 -1
  19. package/dist/index.js +160 -18
  20. package/dist/{run-tests-NT2UIUVB.js → run-tests-D7YBY4XB.js} +1 -1
  21. package/dist/{run-tests-UT7RZ7Y3.js → run-tests-H4Q2PRX6.js} +2 -2
  22. package/dist/{server-AIHVMLNU.js → server-OFKGDRYI.js} +4 -4
  23. package/dist/{server-E5D54DIZ.js → server-VNCN7AY5.js} +187 -29
  24. package/dist/{task-orchestrator-BFDSTSOH.js → task-orchestrator-AL2E642M.js} +4 -4
  25. package/dist/{usage-P4B2RZKL.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-E2ZWWE6Q.js";
8
+ } from "./chunk-HX6N6QEY.js";
9
9
  import {
10
10
  runTool
11
- } from "./chunk-DJ35G5VO.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-GRJNQJA5.js";
29
+ } from "./chunk-GBBVCZ4W.js";
30
30
  import {
31
31
  fileCheckpoints
32
32
  } from "./chunk-4BKXL7SM.js";
@@ -1050,12 +1050,13 @@ ${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
+ import { tmpdir } from "os";
1059
1060
 
1060
1061
  // src/core/readline-internal.ts
1061
1062
  function rlInternal(rl) {
@@ -1262,6 +1263,155 @@ function simpleDiff(oldLines, newLines) {
1262
1263
 
1263
1264
  // src/tools/hooks.ts
1264
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
+ }
1265
1415
  function rewriteTemplate(template, isWindows) {
1266
1416
  const ref = (name) => isWindows ? `%${name}%` : `$${name}`;
1267
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"));
@@ -1272,7 +1422,7 @@ function runHook(template, vars) {
1272
1422
  const cmd = rewriteTemplate(template, isWindows);
1273
1423
  try {
1274
1424
  execSync(cmd, {
1275
- timeout: 5e3,
1425
+ timeout: DEFAULT_TIMEOUT_MS,
1276
1426
  stdio: ["pipe", "pipe", "pipe"],
1277
1427
  encoding: "utf-8",
1278
1428
  env: {
@@ -1290,6 +1440,7 @@ function runHook(template, vars) {
1290
1440
  }
1291
1441
 
1292
1442
  // src/tools/permissions.ts
1443
+ import { isAbsolute, resolve as resolve3 } from "path";
1293
1444
  function checkPermission(toolName, args, dangerLevel, rules, defaultAction = "confirm") {
1294
1445
  for (const rule of rules) {
1295
1446
  if (rule.tool !== "*" && rule.tool !== toolName) continue;
@@ -1310,6 +1461,225 @@ function checkPermission(toolName, args, dangerLevel, rules, defaultAction = "co
1310
1461
  }
1311
1462
  return defaultAction;
1312
1463
  }
1464
+ var BUILTIN_PROFILE_NAMES = /* @__PURE__ */ new Set(["legacy", "read-only", "workspace-write", "danger-full-access"]);
1465
+ var READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
1466
+ "read_file",
1467
+ "list_dir",
1468
+ "grep_files",
1469
+ "glob_files",
1470
+ "find_symbol",
1471
+ "get_outline",
1472
+ "find_references",
1473
+ "search_code",
1474
+ "git_status",
1475
+ "git_diff",
1476
+ "git_log",
1477
+ "task_list",
1478
+ "web_fetch",
1479
+ "web_search",
1480
+ "google_search",
1481
+ "ask_user",
1482
+ "write_todos",
1483
+ "recall_memory"
1484
+ ]);
1485
+ function normalizePathForPermission(value) {
1486
+ return value.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
1487
+ }
1488
+ function isInsidePath(root, target) {
1489
+ const absoluteTarget = isAbsolute(target) ? target : resolve3(root, target);
1490
+ const r = normalizePathForPermission(resolve3(root));
1491
+ const t = normalizePathForPermission(absoluteTarget);
1492
+ return t === r || t.startsWith(`${r}/`);
1493
+ }
1494
+ function extractPathArg(toolName, args) {
1495
+ const raw = args["path"] ?? args["filePath"] ?? args["notebook_path"];
1496
+ if (typeof raw === "string" && raw.trim()) return raw.trim();
1497
+ if (toolName === "save_last_response") {
1498
+ const output = args["outputPath"] ?? args["file"] ?? args["path"];
1499
+ if (typeof output === "string" && output.trim()) return output.trim();
1500
+ }
1501
+ return null;
1502
+ }
1503
+ function isExplicitFileWriteTool(toolName) {
1504
+ return toolName === "write_file" || toolName === "edit_file" || toolName === "notebook_edit" || toolName === "save_last_response";
1505
+ }
1506
+ function resolveProfileName(ctx) {
1507
+ const requested = ctx?.profileName ?? "legacy";
1508
+ if (ctx?.allowedProfiles && ctx.allowedProfiles.length > 0 && !ctx.allowedProfiles.includes(requested)) {
1509
+ return "legacy";
1510
+ }
1511
+ if (BUILTIN_PROFILE_NAMES.has(requested)) return requested;
1512
+ const ext = ctx?.profile?.extends;
1513
+ return ext && BUILTIN_PROFILE_NAMES.has(ext) ? ext : "legacy";
1514
+ }
1515
+ function getProfileRules(ctx) {
1516
+ return ctx?.profile?.rules ?? [];
1517
+ }
1518
+ function getProfileDefaultAction(name, ctx) {
1519
+ if (ctx?.profile?.defaultAction) return ctx.profile.defaultAction;
1520
+ if (name === "danger-full-access") return "auto-approve";
1521
+ return "confirm";
1522
+ }
1523
+ function getWorkspaceRoots(ctx) {
1524
+ const roots = [ctx?.workspaceRoot, ...ctx?.profile?.workspaceRoots ?? []].filter((v) => typeof v === "string" && v.trim().length > 0);
1525
+ return roots;
1526
+ }
1527
+ function getAllowedWriteRoots(ctx) {
1528
+ const roots = [...getWorkspaceRoots(ctx)];
1529
+ const allowTemp = ctx?.profile?.allowTemp !== false;
1530
+ if (allowTemp) roots.push(...ctx?.tempDirs ?? []);
1531
+ return roots.filter((v) => v.trim().length > 0);
1532
+ }
1533
+ function profileHardDecision(profileName, toolName, args, dangerLevel, ctx) {
1534
+ if (profileName === "legacy") return null;
1535
+ if (profileName === "read-only") {
1536
+ const mcpReadOnly = toolName.startsWith("mcp__") && dangerLevel === "safe";
1537
+ if (dangerLevel !== "safe" || !READ_ONLY_TOOLS.has(toolName) && !mcpReadOnly) {
1538
+ return { action: "deny", reason: "blocked by read-only permission profile", profileName };
1539
+ }
1540
+ return null;
1541
+ }
1542
+ if (profileName === "workspace-write") {
1543
+ if (dangerLevel === "destructive") return { action: "confirm", reason: "destructive tools always require confirmation", profileName };
1544
+ if (dangerLevel === "write" && isExplicitFileWriteTool(toolName)) {
1545
+ const target = extractPathArg(toolName, args);
1546
+ if (!target) return { action: "confirm", reason: "file write target is not explicit", profileName };
1547
+ const allowedRoots = getAllowedWriteRoots(ctx);
1548
+ if (allowedRoots.length === 0 || !allowedRoots.some((root) => isInsidePath(root, target))) {
1549
+ return { action: "deny", reason: "file write target is outside workspace/temp roots", profileName };
1550
+ }
1551
+ }
1552
+ return null;
1553
+ }
1554
+ if (profileName === "danger-full-access") {
1555
+ if (dangerLevel === "destructive") return { action: "confirm", reason: "destructive tools still require confirmation", profileName };
1556
+ return null;
1557
+ }
1558
+ return null;
1559
+ }
1560
+ function checkPermissionWithProfile(toolName, args, dangerLevel, rules, defaultAction = "confirm", profileContext) {
1561
+ const profileName = resolveProfileName(profileContext);
1562
+ const hard = profileHardDecision(profileName, toolName, args, dangerLevel, profileContext);
1563
+ if (hard?.action === "deny") return hard;
1564
+ const combinedRules = [...getProfileRules(profileContext), ...rules];
1565
+ const profileDefault = profileName === "legacy" ? defaultAction : getProfileDefaultAction(profileName, profileContext);
1566
+ const action = checkPermission(toolName, args, dangerLevel, combinedRules, profileDefault);
1567
+ if (hard?.action === "confirm" && action === "auto-approve") return hard;
1568
+ return { action, profileName };
1569
+ }
1570
+ function formatPermissionProfileWarning(profileName) {
1571
+ if (profileName === "danger-full-access") {
1572
+ return "\u26A0 danger-full-access permission profile is active. Write tools may auto-run; destructive tools still require confirmation.";
1573
+ }
1574
+ if (profileName === "read-only") return "Read-only permission profile is active. Write and destructive tools are blocked.";
1575
+ if (profileName === "workspace-write") return "Workspace-write permission profile is active. Writes outside workspace/temp roots are blocked.";
1576
+ return null;
1577
+ }
1578
+ var NETWORK_TOOL_HOSTS = {
1579
+ web_search: ["cn.bing.com", "www.google.com"],
1580
+ google_search: ["www.googleapis.com"]
1581
+ };
1582
+ var NETWORK_TOOL_PORTS = {
1583
+ web_search: [443],
1584
+ google_search: [443]
1585
+ };
1586
+ function networkActionToPermission(action) {
1587
+ if (action === "allow") return null;
1588
+ return action;
1589
+ }
1590
+ function domainMatches(pattern, host) {
1591
+ const p = pattern.trim().toLowerCase().replace(/^\*\./, "");
1592
+ const h = host.trim().toLowerCase().replace(/:\d+$/, "");
1593
+ return h === p || h.endsWith(`.${p}`);
1594
+ }
1595
+ function isPrivateNetworkHost(host) {
1596
+ const h = host.toLowerCase().replace(/^\[|\]$/g, "");
1597
+ if (h === "" || h === "localhost") return true;
1598
+ if (/^(127|10)\./.test(h)) return true;
1599
+ if (/^192\.168\./.test(h)) return true;
1600
+ if (/^172\.(1[6-9]|2\d|3[0-1])\./.test(h)) return true;
1601
+ if (/^169\.254\./.test(h)) return true;
1602
+ if (h === "::1" || h === "::") return true;
1603
+ if (/^f[cd][0-9a-f]*:/i.test(h) || /^fe[89ab][0-9a-f]*:/i.test(h)) return true;
1604
+ return false;
1605
+ }
1606
+ function extractNetworkHosts(toolName, args) {
1607
+ if (toolName === "web_fetch") {
1608
+ const raw = String(args["url"] ?? "").trim();
1609
+ try {
1610
+ return raw ? [new URL(raw).hostname] : [];
1611
+ } catch {
1612
+ return [];
1613
+ }
1614
+ }
1615
+ return NETWORK_TOOL_HOSTS[toolName] ?? [];
1616
+ }
1617
+ function extractNetworkPorts(toolName, args) {
1618
+ if (toolName === "web_fetch") {
1619
+ const raw = String(args["url"] ?? "").trim();
1620
+ try {
1621
+ if (!raw) return [];
1622
+ const url = new URL(raw);
1623
+ if (url.port) return [Number(url.port)];
1624
+ return [url.protocol === "http:" ? 80 : 443];
1625
+ } catch {
1626
+ return [];
1627
+ }
1628
+ }
1629
+ return NETWORK_TOOL_PORTS[toolName] ?? [];
1630
+ }
1631
+ function isShellNetworkCommand(command) {
1632
+ return /\b(curl|wget|aria2c|Invoke-WebRequest|Invoke-RestMethod|iwr|irm|ssh|scp|sftp|ftp|telnet|nc|ncat|netcat)\b/i.test(command) || /\b(git\s+(clone|fetch|pull|push)|npm\s+(install|publish|view)|pnpm\s+(add|install)|yarn\s+(add|install)|pip\s+install|python\s+-m\s+pip\s+install)\b/i.test(command);
1633
+ }
1634
+ function networkToolAction(toolName, policy) {
1635
+ if (toolName === "web_fetch") return policy.tools?.web_fetch ?? policy.defaultAction ?? "confirm";
1636
+ if (toolName === "web_search") return policy.tools?.web_search ?? policy.defaultAction ?? "confirm";
1637
+ if (toolName === "google_search") return policy.tools?.google_search ?? policy.defaultAction ?? "confirm";
1638
+ if (toolName.startsWith("mcp__")) return policy.tools?.mcp ?? policy.defaultAction ?? "confirm";
1639
+ return null;
1640
+ }
1641
+ function checkNetworkPolicy(toolName, args, policy) {
1642
+ if (!policy?.enabled) return null;
1643
+ let action = networkToolAction(toolName, policy);
1644
+ let reason = "network access requires confirmation";
1645
+ if (toolName === "bash") {
1646
+ const command = String(args["command"] ?? "");
1647
+ if (!isShellNetworkCommand(command)) return null;
1648
+ action = policy.tools?.shell ?? policy.defaultAction ?? "confirm";
1649
+ reason = "shell command may access the network";
1650
+ }
1651
+ if (!action) return null;
1652
+ const hosts = extractNetworkHosts(toolName, args);
1653
+ for (const host of hosts) {
1654
+ if (policy.denyDomains?.some((pattern) => domainMatches(pattern, host))) {
1655
+ return { action: "deny", reason: `network host "${host}" is denied by networkPolicy`, profileName: "networkPolicy" };
1656
+ }
1657
+ if (!policy.allowPrivateNetwork && isPrivateNetworkHost(host)) {
1658
+ return { action: "deny", reason: `network host "${host}" is private/internal`, profileName: "networkPolicy" };
1659
+ }
1660
+ }
1661
+ if (hosts.length > 0 && policy.allowDomains && policy.allowDomains.length > 0) {
1662
+ const allAllowed = hosts.every((host) => policy.allowDomains.some((pattern) => domainMatches(pattern, host)));
1663
+ if (!allAllowed) {
1664
+ return { action: "deny", reason: `network host is not in networkPolicy.allowDomains`, profileName: "networkPolicy" };
1665
+ }
1666
+ }
1667
+ const ports = extractNetworkPorts(toolName, args);
1668
+ for (const port of ports) {
1669
+ if (policy.denyPorts?.includes(port)) {
1670
+ return { action: "deny", reason: `network port ${port} is denied by networkPolicy`, profileName: "networkPolicy" };
1671
+ }
1672
+ }
1673
+ if (ports.length > 0 && policy.allowPorts && policy.allowPorts.length > 0) {
1674
+ const allPortsAllowed = ports.every((port) => policy.allowPorts.includes(port));
1675
+ if (!allPortsAllowed) {
1676
+ return { action: "deny", reason: `network port is not in networkPolicy.allowPorts`, profileName: "networkPolicy" };
1677
+ }
1678
+ }
1679
+ const permissionAction = networkActionToPermission(action);
1680
+ if (!permissionAction) return null;
1681
+ return { action: permissionAction, reason, profileName: "networkPolicy" };
1682
+ }
1313
1683
 
1314
1684
  // src/tools/truncate.ts
1315
1685
  var DEFAULT_MAX_TOOL_OUTPUT_CHARS = 12e3;
@@ -1564,11 +1934,23 @@ var ToolExecutor = class {
1564
1934
  /** 权限规则(可选) */
1565
1935
  permissionRules = [];
1566
1936
  defaultPermission = "confirm";
1937
+ permissionProfileName = "legacy";
1938
+ permissionProfile;
1939
+ allowedPermissionProfiles = [];
1940
+ workspaceRoot = process.cwd();
1941
+ networkPolicy;
1942
+ hookConfigDir = process.cwd();
1567
1943
  /** 注入 hooks 和 permission rules 配置 */
1568
1944
  setConfig(opts) {
1569
1945
  this.hookConfig = opts.hookConfig;
1570
1946
  if (opts.permissionRules) this.permissionRules = opts.permissionRules;
1571
1947
  if (opts.defaultPermission) this.defaultPermission = opts.defaultPermission;
1948
+ if (opts.permissionProfileName) this.permissionProfileName = opts.permissionProfileName;
1949
+ if (opts.permissionProfile) this.permissionProfile = opts.permissionProfile;
1950
+ if (opts.allowedPermissionProfiles) this.allowedPermissionProfiles = opts.allowedPermissionProfiles;
1951
+ if (opts.workspaceRoot) this.workspaceRoot = opts.workspaceRoot;
1952
+ if (opts.networkPolicy) this.networkPolicy = opts.networkPolicy;
1953
+ if (opts.hookConfigDir) this.hookConfigDir = opts.hookConfigDir;
1572
1954
  }
1573
1955
  async execute(call) {
1574
1956
  return runWithSessionKey(this.sessionKey, () => this.executeInner(call));
@@ -1583,31 +1965,97 @@ var ToolExecutor = class {
1583
1965
  };
1584
1966
  }
1585
1967
  const dangerLevel = getDangerLevel(call.name, call.arguments);
1968
+ let toolCallAlreadyPrinted = false;
1586
1969
  runHook(this.hookConfig?.preToolExecution, {
1587
1970
  tool: call.name,
1588
1971
  dangerLevel,
1589
1972
  args: JSON.stringify(call.arguments).slice(0, 200)
1590
1973
  });
1591
- if (this.permissionRules.length > 0) {
1592
- const action = checkPermission(call.name, call.arguments, dangerLevel, this.permissionRules, this.defaultPermission);
1593
- if (action === "deny") {
1594
- return { callId: call.id, content: `[Permission denied] Tool ${call.name} is blocked by permission rules. Do not retry.`, isError: true };
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
+ }
1992
+ const permission = checkPermissionWithProfile(
1993
+ call.name,
1994
+ call.arguments,
1995
+ dangerLevel,
1996
+ this.permissionRules,
1997
+ this.defaultPermission,
1998
+ {
1999
+ profileName: this.permissionProfileName,
2000
+ profile: this.permissionProfile,
2001
+ allowedProfiles: this.allowedPermissionProfiles,
2002
+ workspaceRoot: this.workspaceRoot,
2003
+ tempDirs: [tmpdir()]
1595
2004
  }
1596
- if (action === "auto-approve") {
1597
- this.printToolCall(call);
1598
- try {
1599
- const rawContent = await runTool(tool, call.arguments, call.name);
1600
- const content = truncateOutput(rawContent, call.name);
1601
- const wasTruncated = content !== rawContent;
1602
- this.printToolResult(call.name, rawContent, false, wasTruncated);
1603
- runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
1604
- return { callId: call.id, content, isError: false };
1605
- } catch (err) {
1606
- const message = err instanceof Error ? err.message : String(err);
1607
- this.printToolResult(call.name, message, true, false);
1608
- runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
1609
- return { callId: call.id, content: message, isError: true };
1610
- }
2005
+ );
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
+ }
2015
+ if (networkPermission?.action === "deny") {
2016
+ const reason = networkPermission.reason ? ` (${networkPermission.reason})` : "";
2017
+ return {
2018
+ callId: call.id,
2019
+ content: `[Permission denied] Tool ${call.name} is blocked by networkPolicy${reason}. Do not retry.`,
2020
+ isError: true
2021
+ };
2022
+ }
2023
+ if (permission.action === "deny") {
2024
+ const reason = permission.reason ? ` (${permission.reason})` : "";
2025
+ return {
2026
+ callId: call.id,
2027
+ content: `[Permission denied] Tool ${call.name} is blocked by permission profile "${permission.profileName}"${reason}. Do not retry.`,
2028
+ isError: true
2029
+ };
2030
+ }
2031
+ if (permission.action === "auto-approve" && networkPermission?.action !== "confirm") {
2032
+ this.printToolCall(call);
2033
+ try {
2034
+ const rawContent = await runTool(tool, call.arguments, call.name);
2035
+ const content = truncateOutput(rawContent, call.name);
2036
+ const wasTruncated = content !== rawContent;
2037
+ this.printToolResult(call.name, rawContent, false, wasTruncated);
2038
+ runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
2039
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
2040
+ return { callId: call.id, content, isError: false };
2041
+ } catch (err) {
2042
+ const message = err instanceof Error ? err.message : String(err);
2043
+ this.printToolResult(call.name, message, true, false);
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 });
2046
+ return { callId: call.id, content: message, isError: true };
2047
+ }
2048
+ }
2049
+ if (networkPermission?.action === "confirm" && dangerLevel === "safe") {
2050
+ this.printToolCall(call);
2051
+ toolCallAlreadyPrinted = true;
2052
+ const confirmed = await this.confirm(call, "write");
2053
+ if (!confirmed) {
2054
+ return {
2055
+ callId: call.id,
2056
+ content: `[User cancelled] The user declined network access for ${call.name}. Do not retry without asking.`,
2057
+ isError: true
2058
+ };
1611
2059
  }
1612
2060
  }
1613
2061
  if (this.sessionAutoApprove && dangerLevel === "write") {
@@ -1620,11 +2068,13 @@ var ToolExecutor = class {
1620
2068
  const wasTruncated = content !== rawContent;
1621
2069
  this.printToolResult(call.name, rawContent, false, wasTruncated);
1622
2070
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
2071
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
1623
2072
  return { callId: call.id, content, isError: false };
1624
2073
  } catch (err) {
1625
2074
  const message = err instanceof Error ? err.message : String(err);
1626
2075
  this.printToolResult(call.name, message, true, false);
1627
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 });
1628
2078
  return { callId: call.id, content: message, isError: true };
1629
2079
  }
1630
2080
  }
@@ -1658,11 +2108,13 @@ var ToolExecutor = class {
1658
2108
  const wasTruncated = content !== rawContent;
1659
2109
  this.printToolResult(call.name, rawContent, false, wasTruncated);
1660
2110
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
2111
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
1661
2112
  return { callId: call.id, content, isError: false };
1662
2113
  } catch (err) {
1663
2114
  const message = err instanceof Error ? err.message : String(err);
1664
2115
  this.printToolResult(call.name, message, true, false);
1665
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 });
1666
2118
  return { callId: call.id, content: message, isError: true };
1667
2119
  }
1668
2120
  }
@@ -1697,6 +2149,9 @@ var ToolExecutor = class {
1697
2149
  * 然后让用户 approve all / reject all / 选择性 approve。
1698
2150
  */
1699
2151
  async executeBatchFileWrites(calls) {
2152
+ if (this.permissionProfileName !== "legacy" || this.permissionRules.length > 0) {
2153
+ return Promise.all(calls.map((call) => this.execute(call)));
2154
+ }
1700
2155
  console.log();
1701
2156
  console.log(theme.heading(`\u270E Batch file writes (${calls.length} files):`));
1702
2157
  console.log(theme.dim("\u2500".repeat(50)));
@@ -1770,7 +2225,7 @@ var ToolExecutor = class {
1770
2225
  rl.resume();
1771
2226
  process.stdout.write(prompt);
1772
2227
  this.confirming = true;
1773
- return new Promise((resolve5) => {
2228
+ return new Promise((resolve6) => {
1774
2229
  let completed = false;
1775
2230
  const cleanup = (result) => {
1776
2231
  if (completed) return;
@@ -1780,7 +2235,7 @@ var ToolExecutor = class {
1780
2235
  rl.pause();
1781
2236
  rlAny.output = savedOutput;
1782
2237
  this.confirming = false;
1783
- resolve5(result);
2238
+ resolve6(result);
1784
2239
  };
1785
2240
  const onLine = (line) => {
1786
2241
  const trimmed = line.trim();
@@ -1847,10 +2302,10 @@ var ToolExecutor = class {
1847
2302
  const filePath = String(call.arguments["path"] ?? "");
1848
2303
  const newContent = String(call.arguments["content"] ?? "");
1849
2304
  if (!filePath) return;
1850
- if (existsSync4(filePath)) {
2305
+ if (existsSync5(filePath)) {
1851
2306
  let oldContent;
1852
2307
  try {
1853
- oldContent = readFileSync3(filePath, "utf-8");
2308
+ oldContent = readFileSync4(filePath, "utf-8");
1854
2309
  } catch {
1855
2310
  return;
1856
2311
  }
@@ -1873,7 +2328,7 @@ var ToolExecutor = class {
1873
2328
  }
1874
2329
  } else if (call.name === "edit_file") {
1875
2330
  const filePath = String(call.arguments["path"] ?? "");
1876
- if (!filePath || !existsSync4(filePath)) return;
2331
+ if (!filePath || !existsSync5(filePath)) return;
1877
2332
  const oldStr = call.arguments["old_str"];
1878
2333
  const newStr = call.arguments["new_str"];
1879
2334
  if (oldStr !== void 0) {
@@ -1900,7 +2355,7 @@ var ToolExecutor = class {
1900
2355
  const to = Number(call.arguments["delete_to_line"] ?? from);
1901
2356
  let fileContent;
1902
2357
  try {
1903
- fileContent = readFileSync3(filePath, "utf-8");
2358
+ fileContent = readFileSync4(filePath, "utf-8");
1904
2359
  } catch {
1905
2360
  return;
1906
2361
  }
@@ -1954,7 +2409,7 @@ var ToolExecutor = class {
1954
2409
  rl.resume();
1955
2410
  process.stdout.write(color("Proceed? [y/N] (type y + Enter to confirm) "));
1956
2411
  this.confirming = true;
1957
- return new Promise((resolve5) => {
2412
+ return new Promise((resolve6) => {
1958
2413
  let completed = false;
1959
2414
  const cleanup = (answer) => {
1960
2415
  if (completed) return;
@@ -1964,7 +2419,7 @@ var ToolExecutor = class {
1964
2419
  rl.pause();
1965
2420
  rlAny.output = savedOutput;
1966
2421
  this.confirming = false;
1967
- resolve5(answer === "y");
2422
+ resolve6(answer === "y");
1968
2423
  };
1969
2424
  const onLine = (line) => {
1970
2425
  const trimmed = line.trim();
@@ -1992,11 +2447,11 @@ var ToolExecutor = class {
1992
2447
  };
1993
2448
 
1994
2449
  // src/tools/sensitive-paths.ts
1995
- import { resolve as resolve3, sep as sep2, basename as basename2 } from "path";
2450
+ import { resolve as resolve4, sep as sep2, basename as basename2 } from "path";
1996
2451
  import { homedir as homedir2 } from "os";
1997
2452
  var home = homedir2();
1998
2453
  function norm(p) {
1999
- const abs = resolve3(p);
2454
+ const abs = resolve4(p);
2000
2455
  return process.platform === "win32" ? abs.toLowerCase() : abs;
2001
2456
  }
2002
2457
  function homeRel(p) {
@@ -2135,7 +2590,7 @@ Do NOT split a long document into many write_file(append=true) calls. That patte
2135
2590
  }
2136
2591
  undoStack.push(filePath, `write_file${appendMode ? " (append)" : ""}: ${filePath}`);
2137
2592
  fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
2138
- mkdirSync(dirname2(filePath), { recursive: true });
2593
+ mkdirSync2(dirname2(filePath), { recursive: true });
2139
2594
  if (appendMode) {
2140
2595
  appendFileSync(filePath, content, encoding);
2141
2596
  } else {
@@ -2155,7 +2610,7 @@ Do NOT split a long document into many write_file(append=true) calls. That patte
2155
2610
  };
2156
2611
 
2157
2612
  // src/tools/builtin/edit-file.ts
2158
- import { readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
2613
+ import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
2159
2614
 
2160
2615
  // src/tools/builtin/patch-apply.ts
2161
2616
  function parseUnifiedDiff(patch) {
@@ -2516,7 +2971,7 @@ Note: Path can be absolute or relative to cwd.`,
2516
2971
  const filePath = String(args["path"] ?? "");
2517
2972
  const encoding = args["encoding"] ?? "utf-8";
2518
2973
  if (!filePath) throw new ToolError("edit_file", "path is required");
2519
- 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}`);
2520
2975
  const verdict = classifyWritePath(filePath);
2521
2976
  if (verdict.sensitive && subAgentGuard.active) {
2522
2977
  throw new ToolError(
@@ -2524,7 +2979,7 @@ Note: Path can be absolute or relative to cwd.`,
2524
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.`
2525
2980
  );
2526
2981
  }
2527
- const original = readFileSync4(filePath, encoding);
2982
+ const original = readFileSync5(filePath, encoding);
2528
2983
  if (args["patch"] !== void 0) {
2529
2984
  const patchText = String(args["patch"] ?? "");
2530
2985
  const stopOnError = args["stop_on_error"] !== false;
@@ -2682,8 +3137,8 @@ function truncatePreview(str, maxLen = 80) {
2682
3137
  }
2683
3138
 
2684
3139
  // src/tools/builtin/list-dir.ts
2685
- import { readdirSync as readdirSync3, statSync as statSync3, existsSync as existsSync6 } from "fs";
2686
- 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";
2687
3142
  var listDirTool = {
2688
3143
  definition: {
2689
3144
  name: "list_dir",
@@ -2705,7 +3160,7 @@ var listDirTool = {
2705
3160
  async execute(args) {
2706
3161
  const dirPath = String(args["path"] ?? process.cwd());
2707
3162
  const recursive = Boolean(args["recursive"] ?? false);
2708
- if (!existsSync6(dirPath)) {
3163
+ if (!existsSync7(dirPath)) {
2709
3164
  const targetName = basename3(dirPath).toLowerCase();
2710
3165
  const cwd = process.cwd();
2711
3166
  const suggestions = [];
@@ -2763,11 +3218,11 @@ function listRecursive(basePath, indent, recursive, lines) {
2763
3218
  if (entry.isDirectory()) {
2764
3219
  lines.push(`${indent}\u{1F4C1} ${entry.name}/`);
2765
3220
  if (recursive) {
2766
- listRecursive(join(basePath, entry.name), indent + " ", true, lines);
3221
+ listRecursive(join2(basePath, entry.name), indent + " ", true, lines);
2767
3222
  }
2768
3223
  } else {
2769
3224
  try {
2770
- const stat = statSync3(join(basePath, entry.name));
3225
+ const stat = statSync3(join2(basePath, entry.name));
2771
3226
  const size = formatSize(stat.size);
2772
3227
  lines.push(`${indent}\u{1F4C4} ${entry.name} (${size})`);
2773
3228
  } catch {
@@ -2783,9 +3238,9 @@ function formatSize(bytes) {
2783
3238
  }
2784
3239
 
2785
3240
  // src/tools/builtin/grep-files.ts
2786
- 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";
2787
3242
  import { readFile } from "fs/promises";
2788
- import { join as join2, relative } from "path";
3243
+ import { join as join3, relative } from "path";
2789
3244
  var grepFilesTool = {
2790
3245
  definition: {
2791
3246
  name: "grep_files",
@@ -2836,7 +3291,7 @@ Supports regex. Automatically skips node_modules, dist, .git directories.`,
2836
3291
  const contextLines = Math.max(0, Number(args["context_lines"] ?? 0));
2837
3292
  const maxResults = Math.max(1, Number(args["max_results"] ?? 50));
2838
3293
  if (!pattern) throw new ToolError("grep_files", "pattern is required");
2839
- 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}`);
2840
3295
  const MAX_PATTERN_LENGTH = 1e3;
2841
3296
  if (pattern.length > MAX_PATTERN_LENGTH) {
2842
3297
  throw new ToolError("grep_files", `Pattern too long (${pattern.length} chars, max ${MAX_PATTERN_LENGTH}). Use a shorter pattern.`);
@@ -2930,11 +3385,11 @@ function collectFilePaths(rootPath, filePattern, maxFiles) {
2930
3385
  if (paths.length >= maxFiles) return;
2931
3386
  if (entry.isDirectory()) {
2932
3387
  if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
2933
- walk(join2(dirPath, entry.name));
3388
+ walk(join3(dirPath, entry.name));
2934
3389
  } else if (entry.isFile()) {
2935
3390
  if (isBinary(entry.name)) continue;
2936
3391
  if (filePattern && !matchesFilePattern(entry.name, filePattern)) continue;
2937
- const fullPath = join2(dirPath, entry.name);
3392
+ const fullPath = join3(dirPath, entry.name);
2938
3393
  try {
2939
3394
  if (statSync4(fullPath).size > 1e6) continue;
2940
3395
  } catch {
@@ -2989,7 +3444,7 @@ function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, re
2989
3444
  }
2990
3445
  let content;
2991
3446
  try {
2992
- content = readFileSync5(fullPath, "utf-8");
3447
+ content = readFileSync6(fullPath, "utf-8");
2993
3448
  } catch {
2994
3449
  return;
2995
3450
  }
@@ -3020,8 +3475,8 @@ function searchInFile(fullPath, displayPath, regex, contextLines, maxResults, re
3020
3475
  }
3021
3476
 
3022
3477
  // src/tools/builtin/glob-files.ts
3023
- import { readdirSync as readdirSync5, statSync as statSync5, existsSync as existsSync8 } from "fs";
3024
- 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";
3025
3480
  var globFilesTool = {
3026
3481
  definition: {
3027
3482
  name: "glob_files",
@@ -3054,7 +3509,7 @@ Results sorted by most recent modification time. Automatically skips node_module
3054
3509
  const rootPath = String(args["path"] ?? process.cwd());
3055
3510
  const maxResults = Math.max(1, Number(args["max_results"] ?? 100));
3056
3511
  if (!pattern) throw new ToolError("glob_files", "pattern is required");
3057
- 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}`);
3058
3513
  const regex = globToRegex(pattern);
3059
3514
  const matches = [];
3060
3515
  collectMatchingFiles(rootPath, rootPath, regex, matches, maxResults);
@@ -3131,7 +3586,7 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
3131
3586
  }
3132
3587
  for (const entry of entries) {
3133
3588
  if (results.length >= maxResults) break;
3134
- const fullPath = join3(dirPath, entry.name);
3589
+ const fullPath = join4(dirPath, entry.name);
3135
3590
  if (entry.isDirectory()) {
3136
3591
  if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".")) continue;
3137
3592
  collectMatchingFiles(fullPath, rootPath, regex, results, maxResults);
@@ -3213,7 +3668,7 @@ var runInteractiveTool = {
3213
3668
  PYTHONDONTWRITEBYTECODE: "1"
3214
3669
  };
3215
3670
  const prefixWarnings = [argsTypeWarning, stdinTypeWarning].filter(Boolean).join("");
3216
- return new Promise((resolve5) => {
3671
+ return new Promise((resolve6) => {
3217
3672
  const child = spawn2(executable, cmdArgs.map(String), {
3218
3673
  cwd: process.cwd(),
3219
3674
  env,
@@ -3246,22 +3701,22 @@ var runInteractiveTool = {
3246
3701
  setTimeout(writeNextLine, 400);
3247
3702
  const timer = setTimeout(() => {
3248
3703
  child.kill();
3249
- resolve5(`${prefixWarnings}[Timeout after ${timeout}ms]
3704
+ resolve6(`${prefixWarnings}[Timeout after ${timeout}ms]
3250
3705
  ${buildOutput(stdout, stderr)}`);
3251
3706
  }, timeout);
3252
3707
  child.on("close", (code) => {
3253
3708
  clearTimeout(timer);
3254
3709
  const output = buildOutput(stdout, stderr);
3255
3710
  if (code !== 0 && code !== null) {
3256
- resolve5(`${prefixWarnings}Exit code ${code}:
3711
+ resolve6(`${prefixWarnings}Exit code ${code}:
3257
3712
  ${output}`);
3258
3713
  } else {
3259
- resolve5(`${prefixWarnings}${output || "(no output)"}`);
3714
+ resolve6(`${prefixWarnings}${output || "(no output)"}`);
3260
3715
  }
3261
3716
  });
3262
3717
  child.on("error", (err) => {
3263
3718
  clearTimeout(timer);
3264
- resolve5(
3719
+ resolve6(
3265
3720
  `${prefixWarnings}Failed to start process "${executable}": ${err.message}
3266
3721
  Hint: On Windows, use the full path to the executable, e.g.:
3267
3722
  C:\\Users\\Jinzd\\anaconda3\\envs\\python312\\python.exe`
@@ -3871,7 +4326,7 @@ ${preamble}`;
3871
4326
  }
3872
4327
 
3873
4328
  // src/tools/builtin/save-last-response.ts
3874
- 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";
3875
4330
  import { dirname as dirname3 } from "path";
3876
4331
  var lastResponseStore = { content: "" };
3877
4332
  function cleanupRejectedTeeFile(filePath) {
@@ -3921,7 +4376,7 @@ Any of these triggers means use save_last_response, NOT write_file:
3921
4376
  throw new ToolError("save_last_response", "No content to save: AI has not produced any response yet, or the last response was empty.");
3922
4377
  }
3923
4378
  undoStack.push(filePath, `save_last_response: ${filePath}`);
3924
- mkdirSync2(dirname3(filePath), { recursive: true });
4379
+ mkdirSync3(dirname3(filePath), { recursive: true });
3925
4380
  atomicWriteFileSync(filePath, content);
3926
4381
  const lines = content.split("\n").length;
3927
4382
  return `File saved: ${filePath} (${lines} lines, ${content.length} bytes)`;
@@ -3929,11 +4384,11 @@ Any of these triggers means use save_last_response, NOT write_file:
3929
4384
  };
3930
4385
 
3931
4386
  // src/tools/builtin/save-memory.ts
3932
- import { existsSync as existsSync9, readFileSync as readFileSync6, statSync as statSync6, mkdirSync as mkdirSync3 } from "fs";
3933
- 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";
3934
4389
  import { homedir as homedir3 } from "os";
3935
4390
  function getMemoryFilePath() {
3936
- return join4(homedir3(), CONFIG_DIR_NAME, MEMORY_FILE_NAME);
4391
+ return join5(homedir3(), CONFIG_DIR_NAME, MEMORY_FILE_NAME);
3937
4392
  }
3938
4393
  function formatTimestamp() {
3939
4394
  const now = /* @__PURE__ */ new Date();
@@ -3957,16 +4412,16 @@ var saveMemoryTool = {
3957
4412
  const content = String(args["content"] ?? "").trim();
3958
4413
  if (!content) throw new ToolError("save_memory", "content is required");
3959
4414
  const memoryPath = getMemoryFilePath();
3960
- const configDir = join4(homedir3(), CONFIG_DIR_NAME);
3961
- if (!existsSync9(configDir)) {
3962
- mkdirSync3(configDir, { recursive: true });
4415
+ const configDir = join5(homedir3(), CONFIG_DIR_NAME);
4416
+ if (!existsSync10(configDir)) {
4417
+ mkdirSync4(configDir, { recursive: true });
3963
4418
  }
3964
4419
  const timestamp = formatTimestamp();
3965
4420
  const entry = `
3966
4421
  ## ${timestamp}
3967
4422
  ${content}
3968
4423
  `;
3969
- const previous = existsSync9(memoryPath) ? readFileSync6(memoryPath, "utf-8") : "";
4424
+ const previous = existsSync10(memoryPath) ? readFileSync7(memoryPath, "utf-8") : "";
3970
4425
  atomicWriteFileSync(memoryPath, previous + entry);
3971
4426
  const byteSize = statSync6(memoryPath).size;
3972
4427
  return `Memory saved successfully. File size: ${byteSize} bytes in ${MEMORY_FILE_NAME}`;
@@ -4013,7 +4468,7 @@ function promptUser(rl, question) {
4013
4468
  console.log();
4014
4469
  console.log(chalk4.cyan("\u2753 ") + chalk4.bold(question));
4015
4470
  process.stdout.write(chalk4.cyan("> "));
4016
- return new Promise((resolve5) => {
4471
+ return new Promise((resolve6) => {
4017
4472
  let completed = false;
4018
4473
  const cleanup = (answer) => {
4019
4474
  if (completed) return;
@@ -4023,7 +4478,7 @@ function promptUser(rl, question) {
4023
4478
  rl.pause();
4024
4479
  rlAny.output = savedOutput;
4025
4480
  askUserContext.prompting = false;
4026
- resolve5(answer);
4481
+ resolve6(answer);
4027
4482
  };
4028
4483
  const onLine = (line) => {
4029
4484
  cleanup(line);
@@ -4503,11 +4958,27 @@ var spawnAgentTool = {
4503
4958
  if (!ctx.provider) {
4504
4959
  throw new ToolError("spawn_agent", "provider not initialized (context not injected)");
4505
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";
4506
4976
  const guardWasActive = subAgentGuard.active;
4507
4977
  subAgentGuard.active = true;
4508
4978
  try {
4509
4979
  if (singleTask) {
4510
4980
  const { content, usage } = await runSubAgent(singleTask, maxRounds, null, ctx);
4981
+ subagentStatus = "ok";
4511
4982
  return [
4512
4983
  "## Sub-Agent Result",
4513
4984
  "",
@@ -4536,8 +5007,10 @@ var spawnAgentTool = {
4536
5007
  });
4537
5008
  lines.push("---");
4538
5009
  lines.push(`Total token usage: ${totalIn} input, ${totalOut} output`);
5010
+ subagentStatus = "ok";
4539
5011
  return lines.join("\n");
4540
5012
  } finally {
5013
+ runLifecycleHooks(hookConfig, "SubagentStop", { ...hookPayload, status: subagentStatus }, hookConfigDir);
4541
5014
  subAgentGuard.active = guardWasActive;
4542
5015
  }
4543
5016
  }
@@ -4760,14 +5233,14 @@ var taskStopTool = {
4760
5233
 
4761
5234
  // src/tools/builtin/git-tools.ts
4762
5235
  import { execFileSync as execFileSync2 } from "child_process";
4763
- import { existsSync as existsSync10 } from "fs";
4764
- import { join as join5 } from "path";
5236
+ import { existsSync as existsSync11 } from "fs";
5237
+ import { join as join6 } from "path";
4765
5238
  function assertGitRepo(cwd) {
4766
5239
  let dir = cwd;
4767
5240
  const root = dir.split(/[\\/]/)[0] + (dir.includes("\\") ? "\\" : "/");
4768
5241
  while (dir && dir !== root) {
4769
- if (existsSync10(join5(dir, ".git"))) return;
4770
- const parent = join5(dir, "..");
5242
+ if (existsSync11(join6(dir, ".git"))) return;
5243
+ const parent = join6(dir, "..");
4771
5244
  if (parent === dir) break;
4772
5245
  dir = parent;
4773
5246
  }
@@ -5032,9 +5505,9 @@ ${commitOutput.trim()}`;
5032
5505
  };
5033
5506
 
5034
5507
  // src/tools/builtin/notebook-edit.ts
5035
- import { readFileSync as readFileSync7, existsSync as existsSync11 } from "fs";
5508
+ import { readFileSync as readFileSync8, existsSync as existsSync12 } from "fs";
5036
5509
  import { writeFile } from "fs/promises";
5037
- import { resolve as resolve4, extname as extname2 } from "path";
5510
+ import { resolve as resolve5, extname as extname2 } from "path";
5038
5511
  var notebookEditTool = {
5039
5512
  definition: {
5040
5513
  name: "notebook_edit",
@@ -5083,14 +5556,14 @@ var notebookEditTool = {
5083
5556
  if (!Number.isInteger(cellIndexRaw) || cellIndexRaw < 1) {
5084
5557
  throw new ToolError("notebook_edit", "cell_index must be a positive integer (1-based)");
5085
5558
  }
5086
- const absPath = resolve4(filePath);
5559
+ const absPath = resolve5(filePath);
5087
5560
  if (extname2(absPath).toLowerCase() !== ".ipynb") {
5088
5561
  throw new ToolError("notebook_edit", "path must point to a .ipynb file");
5089
5562
  }
5090
- if (!existsSync11(absPath)) {
5563
+ if (!existsSync12(absPath)) {
5091
5564
  throw new ToolError("notebook_edit", `Notebook not found: ${filePath}`);
5092
5565
  }
5093
- const raw = readFileSync7(absPath, "utf-8");
5566
+ const raw = readFileSync8(absPath, "utf-8");
5094
5567
  const nb = parseNotebook(raw);
5095
5568
  const cellIdx0 = cellIndexRaw - 1;
5096
5569
  undoStack.push(absPath, `notebook_edit (${action}): ${filePath}`);
@@ -5507,8 +5980,8 @@ function estimateToolDefinitionTokens(def) {
5507
5980
 
5508
5981
  // src/tools/registry.ts
5509
5982
  import { pathToFileURL } from "url";
5510
- import { existsSync as existsSync12, mkdirSync as mkdirSync4, readdirSync as readdirSync6 } from "fs";
5511
- 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";
5512
5985
  var ToolRegistry = class {
5513
5986
  tools = /* @__PURE__ */ new Map();
5514
5987
  pluginToolNames = /* @__PURE__ */ new Set();
@@ -5669,9 +6142,9 @@ var ToolRegistry = class {
5669
6142
  * Returns the number of successfully loaded plugins.
5670
6143
  */
5671
6144
  async loadPlugins(pluginsDir, allowPlugins = false) {
5672
- if (!existsSync12(pluginsDir)) {
6145
+ if (!existsSync13(pluginsDir)) {
5673
6146
  try {
5674
- mkdirSync4(pluginsDir, { recursive: true });
6147
+ mkdirSync5(pluginsDir, { recursive: true });
5675
6148
  } catch {
5676
6149
  }
5677
6150
  return 0;
@@ -5695,12 +6168,12 @@ var ToolRegistry = class {
5695
6168
  process.stderr.write(
5696
6169
  `
5697
6170
  [plugins] \u26A0 Loading ${files.length} plugin(s) with FULL system privileges:
5698
- ` + files.map((f) => ` + ${join6(pluginsDir, f)}`).join("\n") + "\n\n"
6171
+ ` + files.map((f) => ` + ${join7(pluginsDir, f)}`).join("\n") + "\n\n"
5699
6172
  );
5700
6173
  let loaded = 0;
5701
6174
  for (const file of files) {
5702
6175
  try {
5703
- const fileUrl = pathToFileURL(join6(pluginsDir, file)).href;
6176
+ const fileUrl = pathToFileURL(join7(pluginsDir, file)).href;
5704
6177
  const mod = await import(fileUrl);
5705
6178
  const tool = mod.tool ?? mod.default?.tool ?? mod.default;
5706
6179
  if (!tool || typeof tool.execute !== "function" || !tool.definition?.name) {
@@ -5732,6 +6205,12 @@ export {
5732
6205
  theme,
5733
6206
  undoStack,
5734
6207
  renderDiff,
6208
+ listHooks,
6209
+ trustHook,
6210
+ untrustHook,
6211
+ getPendingHookTrust,
6212
+ runLifecycleHooks,
6213
+ runHook,
5735
6214
  isInterrupted,
5736
6215
  requestInterrupt,
5737
6216
  resetInterrupt,
@@ -5739,8 +6218,9 @@ export {
5739
6218
  rlInternal,
5740
6219
  groupCallsByPhase,
5741
6220
  runSafePhases,
5742
- runHook,
5743
- checkPermission,
6221
+ checkPermissionWithProfile,
6222
+ formatPermissionProfileWarning,
6223
+ checkNetworkPolicy,
5744
6224
  setMaxOutputCap,
5745
6225
  setContextWindow,
5746
6226
  truncateForPersist,