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
@@ -37,7 +37,7 @@ import {
37
37
  VERSION,
38
38
  buildUserIdentityPrompt,
39
39
  runTestsTool
40
- } from "./chunk-ZY2N2N6T.js";
40
+ } from "./chunk-KE4B3NOQ.js";
41
41
  import {
42
42
  hasSemanticIndex,
43
43
  semanticSearch
@@ -49,8 +49,9 @@ import "./chunk-IEQAE3QG.js";
49
49
  import {
50
50
  loadChatIndex,
51
51
  redactJson,
52
+ scanString,
52
53
  searchChatMemory
53
- } from "./chunk-W7UKO3PS.js";
54
+ } from "./chunk-OQGVGPEK.js";
54
55
  import "./chunk-JV5N65KN.js";
55
56
  import {
56
57
  atomicWriteFileSync
@@ -60,8 +61,8 @@ import {
60
61
  import express from "express";
61
62
  import { createServer } from "http";
62
63
  import { WebSocketServer } from "ws";
63
- import { join as join19, dirname as dirname6, resolve as resolve8, relative as relative4, sep as sep3 } from "path";
64
- import { existsSync as existsSync25, readFileSync as readFileSync19, readdirSync as readdirSync12, statSync as statSync10, realpathSync } from "fs";
64
+ import { join as join20, dirname as dirname6, resolve as resolve8, relative as relative4, sep as sep3 } from "path";
65
+ import { existsSync as existsSync26, readFileSync as readFileSync20, readdirSync as readdirSync12, statSync as statSync10, realpathSync } from "fs";
65
66
  import { networkInterfaces } from "os";
66
67
 
67
68
  // src/config/config-manager.ts
@@ -208,10 +209,33 @@ var ConfigSchema = z.object({
208
209
  env: z.record(z.string()).optional(),
209
210
  timeout: z.number().default(3e4)
210
211
  })).default({}),
211
- // 工具执行钩子(shell 命令,模板变量:{tool} {dangerLevel} {args} {status})
212
+ // Hooks 生命周期(v0.4.212+):兼容旧 pre/post 字符串,同时支持 events.<EventName> 结构化 JSON hook。
212
213
  hooks: z.object({
214
+ enabled: z.boolean().default(true),
213
215
  preToolExecution: z.string().optional(),
214
- postToolExecution: z.string().optional()
216
+ postToolExecution: z.string().optional(),
217
+ events: z.record(z.union([
218
+ z.string(),
219
+ z.object({
220
+ command: z.string(),
221
+ source: z.enum(["user", "project", "managed"]).default("user"),
222
+ description: z.string().optional(),
223
+ required: z.boolean().default(false),
224
+ timeoutMs: z.number().int().min(100).max(3e4).default(5e3),
225
+ disabled: z.boolean().default(false)
226
+ }),
227
+ z.array(z.union([
228
+ z.string(),
229
+ z.object({
230
+ command: z.string(),
231
+ source: z.enum(["user", "project", "managed"]).default("user"),
232
+ description: z.string().optional(),
233
+ required: z.boolean().default(false),
234
+ timeoutMs: z.number().int().min(100).max(3e4).default(5e3),
235
+ disabled: z.boolean().default(false)
236
+ })
237
+ ]))
238
+ ])).default({})
215
239
  }).optional(),
216
240
  // 网络访问治理(v0.4.211+):默认关闭以保持兼容;开启后统一治理 web/search/MCP/shell 网络出口。
217
241
  networkPolicy: z.object({
@@ -5761,12 +5785,12 @@ ${content}`;
5761
5785
  };
5762
5786
 
5763
5787
  // src/tools/builtin/write-file.ts
5764
- import { appendFileSync, mkdirSync as mkdirSync4 } from "fs";
5788
+ import { appendFileSync, mkdirSync as mkdirSync5 } from "fs";
5765
5789
  import { dirname as dirname3 } from "path";
5766
5790
 
5767
5791
  // src/tools/executor.ts
5768
5792
  import chalk3 from "chalk";
5769
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
5793
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
5770
5794
  import { tmpdir } from "os";
5771
5795
 
5772
5796
  // src/core/readline-internal.ts
@@ -5974,6 +5998,139 @@ function simpleDiff(oldLines, newLines) {
5974
5998
 
5975
5999
  // src/tools/hooks.ts
5976
6000
  import { execSync } from "child_process";
6001
+ import { createHash } from "crypto";
6002
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5 } from "fs";
6003
+ import { join as join3 } from "path";
6004
+ var TRUST_FILE = "hooks-trust.json";
6005
+ var DEFAULT_TIMEOUT_MS = 5e3;
6006
+ function asArray(entry) {
6007
+ if (!entry) return [];
6008
+ const raw = Array.isArray(entry) ? entry : [entry];
6009
+ return raw.map((item) => typeof item === "string" ? { command: item } : item);
6010
+ }
6011
+ function hookTrustPath(configDir) {
6012
+ return join3(configDir, TRUST_FILE);
6013
+ }
6014
+ function loadHookTrustStore(configDir) {
6015
+ const file = hookTrustPath(configDir);
6016
+ if (!existsSync6(file)) return { records: [] };
6017
+ try {
6018
+ const parsed = JSON.parse(readFileSync5(file, "utf-8"));
6019
+ return { records: Array.isArray(parsed.records) ? parsed.records : [] };
6020
+ } catch {
6021
+ return { records: [] };
6022
+ }
6023
+ }
6024
+ function hashHook(event, command) {
6025
+ return createHash("sha256").update(`${event}
6026
+ ${command}`, "utf-8").digest("hex");
6027
+ }
6028
+ function isTrusted(source, event, hash, store) {
6029
+ if (source === "user" || source === "managed") return true;
6030
+ return store.records.some((r) => r.source === source && r.event === event && r.hash === hash);
6031
+ }
6032
+ function listHooks(config, configDir) {
6033
+ if (!config || config.enabled === false) return [];
6034
+ const store = loadHookTrustStore(configDir);
6035
+ const out = [];
6036
+ const push = (event, hook, index) => {
6037
+ if (!hook.command) return;
6038
+ const source = hook.source ?? "user";
6039
+ const hash = hashHook(event, hook.command);
6040
+ out.push({
6041
+ id: `${event}:${index}`,
6042
+ event,
6043
+ command: hook.command,
6044
+ source,
6045
+ hash,
6046
+ trusted: isTrusted(source, event, hash, store),
6047
+ disabled: hook.disabled === true,
6048
+ description: hook.description
6049
+ });
6050
+ };
6051
+ if (config.preToolExecution) push("PreToolUse", { command: config.preToolExecution }, 0);
6052
+ if (config.postToolExecution) push("PostToolUse", { command: config.postToolExecution }, 0);
6053
+ for (const [event, entry] of Object.entries(config.events ?? {})) {
6054
+ asArray(entry).forEach((hook, i) => push(event, hook, i));
6055
+ }
6056
+ return out;
6057
+ }
6058
+ function getPendingHookTrust(config, configDir) {
6059
+ return listHooks(config, configDir).filter((h) => h.source === "project" && !h.disabled && !h.trusted);
6060
+ }
6061
+ function normalizeDecision(value, hook) {
6062
+ if (!value || typeof value !== "object") return null;
6063
+ const obj = value;
6064
+ const action = obj.action;
6065
+ if (action !== "allow" && action !== "deny" && action !== "ask") return null;
6066
+ return {
6067
+ action,
6068
+ reason: typeof obj.reason === "string" ? obj.reason : void 0,
6069
+ prompt: typeof obj.prompt === "string" ? obj.prompt : void 0,
6070
+ contextAppend: typeof obj.contextAppend === "string" ? obj.contextAppend : void 0,
6071
+ warning: typeof obj.warning === "string" ? obj.warning : void 0,
6072
+ warnings: Array.isArray(obj.warnings) ? obj.warnings.filter((x) => typeof x === "string") : void 0,
6073
+ hook
6074
+ };
6075
+ }
6076
+ function runStructuredHook(hook, descriptor, payload) {
6077
+ if (hook.disabled === true) return null;
6078
+ const timeout = Math.max(100, Math.min(hook.timeoutMs ?? DEFAULT_TIMEOUT_MS, 3e4));
6079
+ try {
6080
+ const stdout = execSync(hook.command, {
6081
+ timeout,
6082
+ stdio: ["pipe", "pipe", "pipe"],
6083
+ encoding: "utf-8",
6084
+ env: {
6085
+ ...process.env,
6086
+ AICLI_HOOK_EVENT: payload.event,
6087
+ AICLI_HOOK_EVENT_JSON: JSON.stringify(payload)
6088
+ }
6089
+ }).trim();
6090
+ if (!stdout) return null;
6091
+ return normalizeDecision(JSON.parse(stdout), descriptor);
6092
+ } catch (err) {
6093
+ process.stderr.write(`\u26A0 Hook failed: ${hook.command.slice(0, 100)}
6094
+ `);
6095
+ if (hook.required) {
6096
+ return { action: "deny", reason: err instanceof Error ? err.message : String(err), hook: descriptor };
6097
+ }
6098
+ return null;
6099
+ }
6100
+ }
6101
+ function runLifecycleHooks(config, event, payload, opts) {
6102
+ if (!config || config.enabled === false) return [];
6103
+ const options = typeof opts === "string" ? { configDir: opts } : opts;
6104
+ const store = loadHookTrustStore(options.configDir);
6105
+ const hooks = asArray(config.events?.[event]);
6106
+ const decisions = [];
6107
+ for (let i = 0; i < hooks.length; i++) {
6108
+ const hook = hooks[i];
6109
+ if (!hook.command || hook.disabled) continue;
6110
+ const source = hook.source ?? "user";
6111
+ const hash = hashHook(event, hook.command);
6112
+ const descriptor = {
6113
+ id: `${event}:${i}`,
6114
+ event,
6115
+ command: hook.command,
6116
+ source,
6117
+ hash,
6118
+ trusted: isTrusted(source, event, hash, store),
6119
+ disabled: false,
6120
+ description: hook.description
6121
+ };
6122
+ if (!descriptor.trusted) {
6123
+ options.onSummary?.(`hook skipped: ${event} project hook requires trust (${hash.slice(0, 12)})`);
6124
+ continue;
6125
+ }
6126
+ const decision = runStructuredHook(hook, descriptor, { event, ...payload });
6127
+ if (decision) {
6128
+ decisions.push(decision);
6129
+ options.onSummary?.(`hook ${event}: ${decision.action}${decision.reason ? ` (${decision.reason})` : ""}`);
6130
+ }
6131
+ }
6132
+ return decisions;
6133
+ }
5977
6134
  function rewriteTemplate(template, isWindows) {
5978
6135
  const ref = (name) => isWindows ? `%${name}%` : `$${name}`;
5979
6136
  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"));
@@ -5984,7 +6141,7 @@ function runHook(template, vars) {
5984
6141
  const cmd = rewriteTemplate(template, isWindows);
5985
6142
  try {
5986
6143
  execSync(cmd, {
5987
- timeout: 5e3,
6144
+ timeout: DEFAULT_TIMEOUT_MS,
5988
6145
  stdio: ["pipe", "pipe", "pipe"],
5989
6146
  encoding: "utf-8",
5990
6147
  env: {
@@ -6384,8 +6541,8 @@ var theme = new Proxy(DARK_THEME, {
6384
6541
  });
6385
6542
 
6386
6543
  // src/diagnostics/tool-stats.ts
6387
- import { existsSync as existsSync6, readFileSync as readFileSync5, mkdirSync as mkdirSync3 } from "fs";
6388
- import { join as join3, dirname as dirname2 } from "path";
6544
+ import { existsSync as existsSync7, readFileSync as readFileSync6, mkdirSync as mkdirSync4 } from "fs";
6545
+ import { join as join4, dirname as dirname2 } from "path";
6389
6546
  import { homedir as homedir3 } from "os";
6390
6547
  var STATS_FILE_NAME = "tool-stats.json";
6391
6548
  var STATS_VERSION = 1;
@@ -6395,16 +6552,16 @@ var dirty = false;
6395
6552
  var pendingWrites = 0;
6396
6553
  var configDirOverride;
6397
6554
  function statsFilePath() {
6398
- const base = configDirOverride ?? join3(homedir3(), CONFIG_DIR_NAME);
6399
- return join3(base, STATS_FILE_NAME);
6555
+ const base = configDirOverride ?? join4(homedir3(), CONFIG_DIR_NAME);
6556
+ return join4(base, STATS_FILE_NAME);
6400
6557
  }
6401
6558
  function load() {
6402
6559
  const path3 = statsFilePath();
6403
- if (!existsSync6(path3)) {
6560
+ if (!existsSync7(path3)) {
6404
6561
  return { version: STATS_VERSION, startedAt: (/* @__PURE__ */ new Date()).toISOString(), entries: {} };
6405
6562
  }
6406
6563
  try {
6407
- const raw = JSON.parse(readFileSync5(path3, "utf-8"));
6564
+ const raw = JSON.parse(readFileSync6(path3, "utf-8"));
6408
6565
  if (raw.version !== STATS_VERSION || !raw.entries) {
6409
6566
  return { version: STATS_VERSION, startedAt: (/* @__PURE__ */ new Date()).toISOString(), entries: {} };
6410
6567
  }
@@ -6462,7 +6619,7 @@ function flush() {
6462
6619
  if (!dirty || state === null) return;
6463
6620
  const path3 = statsFilePath();
6464
6621
  try {
6465
- mkdirSync3(dirname2(path3), { recursive: true });
6622
+ mkdirSync4(dirname2(path3), { recursive: true });
6466
6623
  atomicWriteFileSync(path3, JSON.stringify(state, null, 2));
6467
6624
  dirty = false;
6468
6625
  pendingWrites = 0;
@@ -6549,6 +6706,7 @@ var ToolExecutor = class {
6549
6706
  allowedPermissionProfiles = [];
6550
6707
  workspaceRoot = process.cwd();
6551
6708
  networkPolicy;
6709
+ hookConfigDir = process.cwd();
6552
6710
  /** 注入 hooks 和 permission rules 配置 */
6553
6711
  setConfig(opts) {
6554
6712
  this.hookConfig = opts.hookConfig;
@@ -6559,6 +6717,7 @@ var ToolExecutor = class {
6559
6717
  if (opts.allowedPermissionProfiles) this.allowedPermissionProfiles = opts.allowedPermissionProfiles;
6560
6718
  if (opts.workspaceRoot) this.workspaceRoot = opts.workspaceRoot;
6561
6719
  if (opts.networkPolicy) this.networkPolicy = opts.networkPolicy;
6720
+ if (opts.hookConfigDir) this.hookConfigDir = opts.hookConfigDir;
6562
6721
  }
6563
6722
  async execute(call) {
6564
6723
  return runWithSessionKey(this.sessionKey, () => this.executeInner(call));
@@ -6579,6 +6738,24 @@ var ToolExecutor = class {
6579
6738
  dangerLevel,
6580
6739
  args: JSON.stringify(call.arguments).slice(0, 200)
6581
6740
  });
6741
+ const preToolDecisions = runLifecycleHooks(
6742
+ this.hookConfig,
6743
+ "PreToolUse",
6744
+ { tool: call.name, dangerLevel, args: call.arguments },
6745
+ { configDir: this.hookConfigDir, onSummary: (msg) => console.log(theme.dim(` [hook] ${msg}`)) }
6746
+ );
6747
+ const preDeny = preToolDecisions.find((d) => d.action === "deny");
6748
+ if (preDeny) {
6749
+ return {
6750
+ callId: call.id,
6751
+ content: `[Hook denied] ${preDeny.reason ?? `PreToolUse blocked ${call.name}`}. Do not retry without asking.`,
6752
+ isError: true
6753
+ };
6754
+ }
6755
+ for (const d of preToolDecisions) {
6756
+ const warnings = [d.warning, ...d.warnings ?? []].filter(Boolean);
6757
+ for (const w of warnings) console.log(theme.warning(` \u26A0 Hook warning: ${w}`));
6758
+ }
6582
6759
  const permission = checkPermissionWithProfile(
6583
6760
  call.name,
6584
6761
  call.arguments,
@@ -6594,6 +6771,14 @@ var ToolExecutor = class {
6594
6771
  }
6595
6772
  );
6596
6773
  const networkPermission = checkNetworkPolicy(call.name, call.arguments, this.networkPolicy);
6774
+ if (networkPermission?.action === "confirm" || permission.action === "confirm") {
6775
+ runLifecycleHooks(
6776
+ this.hookConfig,
6777
+ "PermissionRequest",
6778
+ { tool: call.name, dangerLevel, args: call.arguments, reason: networkPermission?.reason ?? permission.reason },
6779
+ { configDir: this.hookConfigDir, onSummary: (msg) => console.log(theme.dim(" [hook] " + msg)) }
6780
+ );
6781
+ }
6597
6782
  if (networkPermission?.action === "deny") {
6598
6783
  const reason = networkPermission.reason ? ` (${networkPermission.reason})` : "";
6599
6784
  return {
@@ -6618,11 +6803,13 @@ var ToolExecutor = class {
6618
6803
  const wasTruncated = content !== rawContent;
6619
6804
  this.printToolResult(call.name, rawContent, false, wasTruncated);
6620
6805
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
6806
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
6621
6807
  return { callId: call.id, content, isError: false };
6622
6808
  } catch (err) {
6623
6809
  const message = err instanceof Error ? err.message : String(err);
6624
6810
  this.printToolResult(call.name, message, true, false);
6625
6811
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
6812
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
6626
6813
  return { callId: call.id, content: message, isError: true };
6627
6814
  }
6628
6815
  }
@@ -6648,11 +6835,13 @@ var ToolExecutor = class {
6648
6835
  const wasTruncated = content !== rawContent;
6649
6836
  this.printToolResult(call.name, rawContent, false, wasTruncated);
6650
6837
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
6838
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
6651
6839
  return { callId: call.id, content, isError: false };
6652
6840
  } catch (err) {
6653
6841
  const message = err instanceof Error ? err.message : String(err);
6654
6842
  this.printToolResult(call.name, message, true, false);
6655
6843
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
6844
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
6656
6845
  return { callId: call.id, content: message, isError: true };
6657
6846
  }
6658
6847
  }
@@ -6686,11 +6875,13 @@ var ToolExecutor = class {
6686
6875
  const wasTruncated = content !== rawContent;
6687
6876
  this.printToolResult(call.name, rawContent, false, wasTruncated);
6688
6877
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
6878
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
6689
6879
  return { callId: call.id, content, isError: false };
6690
6880
  } catch (err) {
6691
6881
  const message = err instanceof Error ? err.message : String(err);
6692
6882
  this.printToolResult(call.name, message, true, false);
6693
6883
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
6884
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
6694
6885
  return { callId: call.id, content: message, isError: true };
6695
6886
  }
6696
6887
  }
@@ -6878,10 +7069,10 @@ var ToolExecutor = class {
6878
7069
  const filePath = String(call.arguments["path"] ?? "");
6879
7070
  const newContent = String(call.arguments["content"] ?? "");
6880
7071
  if (!filePath) return;
6881
- if (existsSync7(filePath)) {
7072
+ if (existsSync8(filePath)) {
6882
7073
  let oldContent;
6883
7074
  try {
6884
- oldContent = readFileSync6(filePath, "utf-8");
7075
+ oldContent = readFileSync7(filePath, "utf-8");
6885
7076
  } catch {
6886
7077
  return;
6887
7078
  }
@@ -6904,7 +7095,7 @@ var ToolExecutor = class {
6904
7095
  }
6905
7096
  } else if (call.name === "edit_file") {
6906
7097
  const filePath = String(call.arguments["path"] ?? "");
6907
- if (!filePath || !existsSync7(filePath)) return;
7098
+ if (!filePath || !existsSync8(filePath)) return;
6908
7099
  const oldStr = call.arguments["old_str"];
6909
7100
  const newStr = call.arguments["new_str"];
6910
7101
  if (oldStr !== void 0) {
@@ -6931,7 +7122,7 @@ var ToolExecutor = class {
6931
7122
  const to = Number(call.arguments["delete_to_line"] ?? from);
6932
7123
  let fileContent;
6933
7124
  try {
6934
- fileContent = readFileSync6(filePath, "utf-8");
7125
+ fileContent = readFileSync7(filePath, "utf-8");
6935
7126
  } catch {
6936
7127
  return;
6937
7128
  }
@@ -7166,7 +7357,7 @@ Do NOT split a long document into many write_file(append=true) calls. That patte
7166
7357
  }
7167
7358
  undoStack.push(filePath, `write_file${appendMode ? " (append)" : ""}: ${filePath}`);
7168
7359
  fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
7169
- mkdirSync4(dirname3(filePath), { recursive: true });
7360
+ mkdirSync5(dirname3(filePath), { recursive: true });
7170
7361
  if (appendMode) {
7171
7362
  appendFileSync(filePath, content, encoding);
7172
7363
  } else {
@@ -7186,7 +7377,7 @@ Do NOT split a long document into many write_file(append=true) calls. That patte
7186
7377
  };
7187
7378
 
7188
7379
  // src/tools/builtin/edit-file.ts
7189
- import { readFileSync as readFileSync7, existsSync as existsSync8 } from "fs";
7380
+ import { readFileSync as readFileSync8, existsSync as existsSync9 } from "fs";
7190
7381
 
7191
7382
  // src/tools/builtin/patch-apply.ts
7192
7383
  function parseUnifiedDiff(patch) {
@@ -7547,7 +7738,7 @@ Note: Path can be absolute or relative to cwd.`,
7547
7738
  const filePath = String(args["path"] ?? "");
7548
7739
  const encoding = args["encoding"] ?? "utf-8";
7549
7740
  if (!filePath) throw new ToolError("edit_file", "path is required");
7550
- if (!existsSync8(filePath)) throw new ToolError("edit_file", `File not found: ${filePath}`);
7741
+ if (!existsSync9(filePath)) throw new ToolError("edit_file", `File not found: ${filePath}`);
7551
7742
  const verdict = classifyWritePath(filePath);
7552
7743
  if (verdict.sensitive && subAgentGuard.active) {
7553
7744
  throw new ToolError(
@@ -7555,7 +7746,7 @@ Note: Path can be absolute or relative to cwd.`,
7555
7746
  `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.`
7556
7747
  );
7557
7748
  }
7558
- const original = readFileSync7(filePath, encoding);
7749
+ const original = readFileSync8(filePath, encoding);
7559
7750
  if (args["patch"] !== void 0) {
7560
7751
  const patchText = String(args["patch"] ?? "");
7561
7752
  const stopOnError = args["stop_on_error"] !== false;
@@ -7713,8 +7904,8 @@ function truncatePreview(str, maxLen = 80) {
7713
7904
  }
7714
7905
 
7715
7906
  // src/tools/builtin/list-dir.ts
7716
- import { readdirSync as readdirSync4, statSync as statSync3, existsSync as existsSync9 } from "fs";
7717
- import { join as join4, basename as basename3 } from "path";
7907
+ import { readdirSync as readdirSync4, statSync as statSync3, existsSync as existsSync10 } from "fs";
7908
+ import { join as join5, basename as basename3 } from "path";
7718
7909
  var listDirTool = {
7719
7910
  definition: {
7720
7911
  name: "list_dir",
@@ -7736,7 +7927,7 @@ var listDirTool = {
7736
7927
  async execute(args) {
7737
7928
  const dirPath = String(args["path"] ?? process.cwd());
7738
7929
  const recursive = Boolean(args["recursive"] ?? false);
7739
- if (!existsSync9(dirPath)) {
7930
+ if (!existsSync10(dirPath)) {
7740
7931
  const targetName = basename3(dirPath).toLowerCase();
7741
7932
  const cwd = process.cwd();
7742
7933
  const suggestions = [];
@@ -7794,11 +7985,11 @@ function listRecursive(basePath, indent, recursive, lines) {
7794
7985
  if (entry.isDirectory()) {
7795
7986
  lines.push(`${indent}\u{1F4C1} ${entry.name}/`);
7796
7987
  if (recursive) {
7797
- listRecursive(join4(basePath, entry.name), indent + " ", true, lines);
7988
+ listRecursive(join5(basePath, entry.name), indent + " ", true, lines);
7798
7989
  }
7799
7990
  } else {
7800
7991
  try {
7801
- const stat = statSync3(join4(basePath, entry.name));
7992
+ const stat = statSync3(join5(basePath, entry.name));
7802
7993
  const size = formatSize(stat.size);
7803
7994
  lines.push(`${indent}\u{1F4C4} ${entry.name} (${size})`);
7804
7995
  } catch {
@@ -7814,9 +8005,9 @@ function formatSize(bytes) {
7814
8005
  }
7815
8006
 
7816
8007
  // src/tools/builtin/grep-files.ts
7817
- import { readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync4, existsSync as existsSync10 } from "fs";
8008
+ import { readdirSync as readdirSync5, readFileSync as readFileSync9, statSync as statSync4, existsSync as existsSync11 } from "fs";
7818
8009
  import { readFile } from "fs/promises";
7819
- import { join as join5, relative } from "path";
8010
+ import { join as join6, relative } from "path";
7820
8011
  var grepFilesTool = {
7821
8012
  definition: {
7822
8013
  name: "grep_files",
@@ -7867,7 +8058,7 @@ Supports regex. Automatically skips node_modules, dist, .git directories.`,
7867
8058
  const contextLines = Math.max(0, Number(args["context_lines"] ?? 0));
7868
8059
  const maxResults = Math.max(1, Number(args["max_results"] ?? 50));
7869
8060
  if (!pattern) throw new ToolError("grep_files", "pattern is required");
7870
- if (!existsSync10(rootPath)) throw new ToolError("grep_files", `Path not found: ${rootPath}`);
8061
+ if (!existsSync11(rootPath)) throw new ToolError("grep_files", `Path not found: ${rootPath}`);
7871
8062
  const MAX_PATTERN_LENGTH = 1e3;
7872
8063
  if (pattern.length > MAX_PATTERN_LENGTH) {
7873
8064
  throw new ToolError("grep_files", `Pattern too long (${pattern.length} chars, max ${MAX_PATTERN_LENGTH}). Use a shorter pattern.`);
@@ -7961,11 +8152,11 @@ function collectFilePaths(rootPath, filePattern, maxFiles) {
7961
8152
  if (paths.length >= maxFiles) return;
7962
8153
  if (entry.isDirectory()) {
7963
8154
  if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
7964
- walk(join5(dirPath, entry.name));
8155
+ walk(join6(dirPath, entry.name));
7965
8156
  } else if (entry.isFile()) {
7966
8157
  if (isBinary(entry.name)) continue;
7967
8158
  if (filePattern && !matchesFilePattern(entry.name, filePattern)) continue;
7968
- const fullPath = join5(dirPath, entry.name);
8159
+ const fullPath = join6(dirPath, entry.name);
7969
8160
  try {
7970
8161
  if (statSync4(fullPath).size > 1e6) continue;
7971
8162
  } catch {
@@ -8020,7 +8211,7 @@ function searchInFile(fullPath, displayPath2, regex, contextLines, maxResults, r
8020
8211
  }
8021
8212
  let content;
8022
8213
  try {
8023
- content = readFileSync8(fullPath, "utf-8");
8214
+ content = readFileSync9(fullPath, "utf-8");
8024
8215
  } catch {
8025
8216
  return;
8026
8217
  }
@@ -8051,8 +8242,8 @@ function searchInFile(fullPath, displayPath2, regex, contextLines, maxResults, r
8051
8242
  }
8052
8243
 
8053
8244
  // src/tools/builtin/glob-files.ts
8054
- import { readdirSync as readdirSync6, statSync as statSync5, existsSync as existsSync11 } from "fs";
8055
- import { join as join6, relative as relative2, basename as basename4 } from "path";
8245
+ import { readdirSync as readdirSync6, statSync as statSync5, existsSync as existsSync12 } from "fs";
8246
+ import { join as join7, relative as relative2, basename as basename4 } from "path";
8056
8247
  var globFilesTool = {
8057
8248
  definition: {
8058
8249
  name: "glob_files",
@@ -8085,7 +8276,7 @@ Results sorted by most recent modification time. Automatically skips node_module
8085
8276
  const rootPath = String(args["path"] ?? process.cwd());
8086
8277
  const maxResults = Math.max(1, Number(args["max_results"] ?? 100));
8087
8278
  if (!pattern) throw new ToolError("glob_files", "pattern is required");
8088
- if (!existsSync11(rootPath)) throw new ToolError("glob_files", `Path not found: ${rootPath}`);
8279
+ if (!existsSync12(rootPath)) throw new ToolError("glob_files", `Path not found: ${rootPath}`);
8089
8280
  const regex = globToRegex(pattern);
8090
8281
  const matches = [];
8091
8282
  collectMatchingFiles(rootPath, rootPath, regex, matches, maxResults);
@@ -8162,7 +8353,7 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
8162
8353
  }
8163
8354
  for (const entry of entries) {
8164
8355
  if (results.length >= maxResults) break;
8165
- const fullPath = join6(dirPath, entry.name);
8356
+ const fullPath = join7(dirPath, entry.name);
8166
8357
  if (entry.isDirectory()) {
8167
8358
  if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".")) continue;
8168
8359
  collectMatchingFiles(fullPath, rootPath, regex, results, maxResults);
@@ -8902,7 +9093,7 @@ ${preamble}`;
8902
9093
  }
8903
9094
 
8904
9095
  // src/tools/builtin/save-last-response.ts
8905
- import { mkdirSync as mkdirSync5, unlinkSync as unlinkSync3, rmdirSync as rmdirSync2 } from "fs";
9096
+ import { mkdirSync as mkdirSync6, unlinkSync as unlinkSync3, rmdirSync as rmdirSync2 } from "fs";
8906
9097
  import { dirname as dirname4 } from "path";
8907
9098
  var lastResponseStore = { content: "" };
8908
9099
  function cleanupRejectedTeeFile(filePath) {
@@ -8952,7 +9143,7 @@ Any of these triggers means use save_last_response, NOT write_file:
8952
9143
  throw new ToolError("save_last_response", "No content to save: AI has not produced any response yet, or the last response was empty.");
8953
9144
  }
8954
9145
  undoStack.push(filePath, `save_last_response: ${filePath}`);
8955
- mkdirSync5(dirname4(filePath), { recursive: true });
9146
+ mkdirSync6(dirname4(filePath), { recursive: true });
8956
9147
  atomicWriteFileSync(filePath, content);
8957
9148
  const lines = content.split("\n").length;
8958
9149
  return `File saved: ${filePath} (${lines} lines, ${content.length} bytes)`;
@@ -8960,11 +9151,11 @@ Any of these triggers means use save_last_response, NOT write_file:
8960
9151
  };
8961
9152
 
8962
9153
  // src/tools/builtin/save-memory.ts
8963
- import { existsSync as existsSync12, readFileSync as readFileSync9, statSync as statSync6, mkdirSync as mkdirSync6 } from "fs";
8964
- import { join as join7 } from "path";
9154
+ import { existsSync as existsSync13, readFileSync as readFileSync10, statSync as statSync6, mkdirSync as mkdirSync7 } from "fs";
9155
+ import { join as join8 } from "path";
8965
9156
  import { homedir as homedir5 } from "os";
8966
9157
  function getMemoryFilePath() {
8967
- return join7(homedir5(), CONFIG_DIR_NAME, MEMORY_FILE_NAME);
9158
+ return join8(homedir5(), CONFIG_DIR_NAME, MEMORY_FILE_NAME);
8968
9159
  }
8969
9160
  function formatTimestamp() {
8970
9161
  const now = /* @__PURE__ */ new Date();
@@ -8988,16 +9179,16 @@ var saveMemoryTool = {
8988
9179
  const content = String(args["content"] ?? "").trim();
8989
9180
  if (!content) throw new ToolError("save_memory", "content is required");
8990
9181
  const memoryPath = getMemoryFilePath();
8991
- const configDir = join7(homedir5(), CONFIG_DIR_NAME);
8992
- if (!existsSync12(configDir)) {
8993
- mkdirSync6(configDir, { recursive: true });
9182
+ const configDir = join8(homedir5(), CONFIG_DIR_NAME);
9183
+ if (!existsSync13(configDir)) {
9184
+ mkdirSync7(configDir, { recursive: true });
8994
9185
  }
8995
9186
  const timestamp = formatTimestamp();
8996
9187
  const entry = `
8997
9188
  ## ${timestamp}
8998
9189
  ${content}
8999
9190
  `;
9000
- const previous = existsSync12(memoryPath) ? readFileSync9(memoryPath, "utf-8") : "";
9191
+ const previous = existsSync13(memoryPath) ? readFileSync10(memoryPath, "utf-8") : "";
9001
9192
  atomicWriteFileSync(memoryPath, previous + entry);
9002
9193
  const byteSize = statSync6(memoryPath).size;
9003
9194
  return `Memory saved successfully. File size: ${byteSize} bytes in ${MEMORY_FILE_NAME}`;
@@ -9534,11 +9725,27 @@ var spawnAgentTool = {
9534
9725
  if (!ctx.provider) {
9535
9726
  throw new ToolError("spawn_agent", "provider not initialized (context not injected)");
9536
9727
  }
9728
+ const hookConfig = ctx.configManager?.get("hooks") ?? void 0;
9729
+ const hookConfigDir = ctx.configManager?.getConfigDir() ?? process.cwd();
9730
+ const taskCount = singleTask ? 1 : tasksArr.length;
9731
+ const hookPayload = {
9732
+ task: singleTask || void 0,
9733
+ tasks: tasksArr.length > 0 ? tasksArr : void 0,
9734
+ taskCount,
9735
+ maxRounds
9736
+ };
9737
+ const startDecisions = runLifecycleHooks(hookConfig, "SubagentStart", hookPayload, hookConfigDir);
9738
+ const startDeny = startDecisions.find((d) => d.action === "deny");
9739
+ if (startDeny) {
9740
+ throw new ToolError("spawn_agent", "SubagentStart hook denied: " + (startDeny.reason ?? "denied"));
9741
+ }
9742
+ let subagentStatus = "error";
9537
9743
  const guardWasActive = subAgentGuard.active;
9538
9744
  subAgentGuard.active = true;
9539
9745
  try {
9540
9746
  if (singleTask) {
9541
9747
  const { content, usage } = await runSubAgent(singleTask, maxRounds, null, ctx);
9748
+ subagentStatus = "ok";
9542
9749
  return [
9543
9750
  "## Sub-Agent Result",
9544
9751
  "",
@@ -9567,8 +9774,10 @@ var spawnAgentTool = {
9567
9774
  });
9568
9775
  lines.push("---");
9569
9776
  lines.push(`Total token usage: ${totalIn} input, ${totalOut} output`);
9777
+ subagentStatus = "ok";
9570
9778
  return lines.join("\n");
9571
9779
  } finally {
9780
+ runLifecycleHooks(hookConfig, "SubagentStop", { ...hookPayload, status: subagentStatus }, hookConfigDir);
9572
9781
  subAgentGuard.active = guardWasActive;
9573
9782
  }
9574
9783
  }
@@ -9791,14 +10000,14 @@ var taskStopTool = {
9791
10000
 
9792
10001
  // src/tools/builtin/git-tools.ts
9793
10002
  import { execFileSync as execFileSync2 } from "child_process";
9794
- import { existsSync as existsSync13 } from "fs";
9795
- import { join as join8 } from "path";
10003
+ import { existsSync as existsSync14 } from "fs";
10004
+ import { join as join9 } from "path";
9796
10005
  function assertGitRepo(cwd) {
9797
10006
  let dir = cwd;
9798
10007
  const root = dir.split(/[\\/]/)[0] + (dir.includes("\\") ? "\\" : "/");
9799
10008
  while (dir && dir !== root) {
9800
- if (existsSync13(join8(dir, ".git"))) return;
9801
- const parent = join8(dir, "..");
10009
+ if (existsSync14(join9(dir, ".git"))) return;
10010
+ const parent = join9(dir, "..");
9802
10011
  if (parent === dir) break;
9803
10012
  dir = parent;
9804
10013
  }
@@ -10063,7 +10272,7 @@ ${commitOutput.trim()}`;
10063
10272
  };
10064
10273
 
10065
10274
  // src/tools/builtin/notebook-edit.ts
10066
- import { readFileSync as readFileSync10, existsSync as existsSync14 } from "fs";
10275
+ import { readFileSync as readFileSync11, existsSync as existsSync15 } from "fs";
10067
10276
  import { writeFile } from "fs/promises";
10068
10277
  import { resolve as resolve5, extname as extname2 } from "path";
10069
10278
  var notebookEditTool = {
@@ -10118,10 +10327,10 @@ var notebookEditTool = {
10118
10327
  if (extname2(absPath).toLowerCase() !== ".ipynb") {
10119
10328
  throw new ToolError("notebook_edit", "path must point to a .ipynb file");
10120
10329
  }
10121
- if (!existsSync14(absPath)) {
10330
+ if (!existsSync15(absPath)) {
10122
10331
  throw new ToolError("notebook_edit", `Notebook not found: ${filePath}`);
10123
10332
  }
10124
- const raw = readFileSync10(absPath, "utf-8");
10333
+ const raw = readFileSync11(absPath, "utf-8");
10125
10334
  const nb = parseNotebook(raw);
10126
10335
  const cellIdx0 = cellIndexRaw - 1;
10127
10336
  undoStack.push(absPath, `notebook_edit (${action}): ${filePath}`);
@@ -10538,8 +10747,8 @@ function estimateToolDefinitionTokens(def) {
10538
10747
 
10539
10748
  // src/tools/registry.ts
10540
10749
  import { pathToFileURL } from "url";
10541
- import { existsSync as existsSync15, mkdirSync as mkdirSync7, readdirSync as readdirSync7 } from "fs";
10542
- import { join as join9 } from "path";
10750
+ import { existsSync as existsSync16, mkdirSync as mkdirSync8, readdirSync as readdirSync7 } from "fs";
10751
+ import { join as join10 } from "path";
10543
10752
  var ToolRegistry = class {
10544
10753
  tools = /* @__PURE__ */ new Map();
10545
10754
  pluginToolNames = /* @__PURE__ */ new Set();
@@ -10700,9 +10909,9 @@ var ToolRegistry = class {
10700
10909
  * Returns the number of successfully loaded plugins.
10701
10910
  */
10702
10911
  async loadPlugins(pluginsDir, allowPlugins = false) {
10703
- if (!existsSync15(pluginsDir)) {
10912
+ if (!existsSync16(pluginsDir)) {
10704
10913
  try {
10705
- mkdirSync7(pluginsDir, { recursive: true });
10914
+ mkdirSync8(pluginsDir, { recursive: true });
10706
10915
  } catch {
10707
10916
  }
10708
10917
  return 0;
@@ -10726,12 +10935,12 @@ var ToolRegistry = class {
10726
10935
  process.stderr.write(
10727
10936
  `
10728
10937
  [plugins] \u26A0 Loading ${files.length} plugin(s) with FULL system privileges:
10729
- ` + files.map((f) => ` + ${join9(pluginsDir, f)}`).join("\n") + "\n\n"
10938
+ ` + files.map((f) => ` + ${join10(pluginsDir, f)}`).join("\n") + "\n\n"
10730
10939
  );
10731
10940
  let loaded = 0;
10732
10941
  for (const file of files) {
10733
10942
  try {
10734
- const fileUrl = pathToFileURL(join9(pluginsDir, file)).href;
10943
+ const fileUrl = pathToFileURL(join10(pluginsDir, file)).href;
10735
10944
  const mod = await import(fileUrl);
10736
10945
  const tool = mod.tool ?? mod.default?.tool ?? mod.default;
10737
10946
  if (!tool || typeof tool.execute !== "function" || !tool.definition?.name) {
@@ -11277,11 +11486,11 @@ var McpManager = class {
11277
11486
  };
11278
11487
 
11279
11488
  // src/skills/manager.ts
11280
- import { existsSync as existsSync16, readdirSync as readdirSync8, mkdirSync as mkdirSync8, statSync as statSync7 } from "fs";
11281
- import { join as join10 } from "path";
11489
+ import { existsSync as existsSync17, readdirSync as readdirSync8, mkdirSync as mkdirSync9, statSync as statSync7 } from "fs";
11490
+ import { join as join11 } from "path";
11282
11491
 
11283
11492
  // src/skills/types.ts
11284
- import { readFileSync as readFileSync11 } from "fs";
11493
+ import { readFileSync as readFileSync12 } from "fs";
11285
11494
  import { basename as basename5 } from "path";
11286
11495
  function parseSimpleYaml(yaml) {
11287
11496
  const result = {};
@@ -11303,7 +11512,7 @@ function parseYamlArray(value) {
11303
11512
  function parseSkillFile(filePath) {
11304
11513
  let raw;
11305
11514
  try {
11306
- raw = readFileSync11(filePath, "utf-8");
11515
+ raw = readFileSync12(filePath, "utf-8");
11307
11516
  } catch {
11308
11517
  return null;
11309
11518
  }
@@ -11346,9 +11555,9 @@ var SkillManager = class {
11346
11555
  /** 发现并加载 skillsDir 下所有 .md 文件,返回加载数量 */
11347
11556
  loadSkills() {
11348
11557
  this.skills.clear();
11349
- if (!existsSync16(this.skillsDir)) {
11558
+ if (!existsSync17(this.skillsDir)) {
11350
11559
  try {
11351
- mkdirSync8(this.skillsDir, { recursive: true });
11560
+ mkdirSync9(this.skillsDir, { recursive: true });
11352
11561
  } catch {
11353
11562
  }
11354
11563
  return 0;
@@ -11361,14 +11570,14 @@ var SkillManager = class {
11361
11570
  }
11362
11571
  for (const entry of entries) {
11363
11572
  let filePath;
11364
- const fullPath = join10(this.skillsDir, entry);
11573
+ const fullPath = join11(this.skillsDir, entry);
11365
11574
  if (entry.endsWith(".md")) {
11366
11575
  filePath = fullPath;
11367
11576
  } else {
11368
11577
  try {
11369
11578
  if (statSync7(fullPath).isDirectory()) {
11370
- const skillMd = join10(fullPath, "SKILL.md");
11371
- if (existsSync16(skillMd)) {
11579
+ const skillMd = join11(fullPath, "SKILL.md");
11580
+ if (existsSync17(skillMd)) {
11372
11581
  filePath = skillMd;
11373
11582
  } else {
11374
11583
  continue;
@@ -11433,7 +11642,7 @@ var SkillManager = class {
11433
11642
  // src/web/tool-executor-web.ts
11434
11643
  import { randomUUID as randomUUID2 } from "crypto";
11435
11644
  import { tmpdir as tmpdir2 } from "os";
11436
- import { existsSync as existsSync17, readFileSync as readFileSync12 } from "fs";
11645
+ import { existsSync as existsSync18, readFileSync as readFileSync13 } from "fs";
11437
11646
  var ToolExecutorWeb = class _ToolExecutorWeb {
11438
11647
  constructor(registry, ws) {
11439
11648
  this.registry = registry;
@@ -11449,6 +11658,7 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11449
11658
  allowedPermissionProfiles = [];
11450
11659
  workspaceRoot = process.cwd();
11451
11660
  networkPolicy;
11661
+ hookConfigDir = process.cwd();
11452
11662
  /** Pending confirm promises keyed by requestId */
11453
11663
  pendingConfirms = /* @__PURE__ */ new Map();
11454
11664
  /** Pending batch confirm promises */
@@ -11488,6 +11698,7 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11488
11698
  if (opts.allowedPermissionProfiles) this.allowedPermissionProfiles = opts.allowedPermissionProfiles;
11489
11699
  if (opts.workspaceRoot) this.workspaceRoot = opts.workspaceRoot;
11490
11700
  if (opts.networkPolicy) this.networkPolicy = opts.networkPolicy;
11701
+ if (opts.hookConfigDir) this.hookConfigDir = opts.hookConfigDir;
11491
11702
  }
11492
11703
  /** Clear M7 timeout timer for a requestId */
11493
11704
  clearPendingTimer(requestId) {
@@ -11536,7 +11747,6 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11536
11747
  }
11537
11748
  sendToolCallStart(call) {
11538
11749
  const dangerLevel = getDangerLevel(call.name, call.arguments);
11539
- let toolCallStarted = false;
11540
11750
  const startTime = Date.now();
11541
11751
  this.toolStartTimes.set(call.id, startTime);
11542
11752
  const msg = {
@@ -11569,9 +11779,9 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11569
11779
  if (call.name === "write_file") {
11570
11780
  const filePath = String(call.arguments["path"] ?? "");
11571
11781
  const newContent = String(call.arguments["content"] ?? "");
11572
- if (filePath && existsSync17(filePath)) {
11782
+ if (filePath && existsSync18(filePath)) {
11573
11783
  try {
11574
- const old = readFileSync12(filePath, "utf-8");
11784
+ const old = readFileSync13(filePath, "utf-8");
11575
11785
  return renderDiff(old, newContent, { filePath });
11576
11786
  } catch {
11577
11787
  }
@@ -11651,6 +11861,26 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11651
11861
  dangerLevel,
11652
11862
  args: JSON.stringify(call.arguments).slice(0, 200)
11653
11863
  });
11864
+ const preToolDecisions = runLifecycleHooks(
11865
+ this.hookConfig,
11866
+ "PreToolUse",
11867
+ { tool: call.name, dangerLevel, args: call.arguments },
11868
+ { configDir: this.hookConfigDir, onSummary: (message) => this.send({ type: "info", message: `[hook] ${message}` }) }
11869
+ );
11870
+ const preDeny = preToolDecisions.find((d) => d.action === "deny");
11871
+ if (preDeny) {
11872
+ const result = {
11873
+ callId: call.id,
11874
+ content: `[Hook denied] ${preDeny.reason ?? `PreToolUse blocked ${call.name}`}. Do not retry without asking.`,
11875
+ isError: true
11876
+ };
11877
+ this.sendToolCallResult(call, result.content, true);
11878
+ return result;
11879
+ }
11880
+ for (const d of preToolDecisions) {
11881
+ const warnings = [d.warning, ...d.warnings ?? []].filter(Boolean);
11882
+ for (const warning of warnings) this.send({ type: "info", message: `[hook] ${warning}` });
11883
+ }
11654
11884
  const permission = checkPermissionWithProfile(
11655
11885
  call.name,
11656
11886
  call.arguments,
@@ -11666,6 +11896,14 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11666
11896
  }
11667
11897
  );
11668
11898
  const networkPermission = checkNetworkPolicy(call.name, call.arguments, this.networkPolicy);
11899
+ if (networkPermission?.action === "confirm" || permission.action === "confirm") {
11900
+ runLifecycleHooks(
11901
+ this.hookConfig,
11902
+ "PermissionRequest",
11903
+ { tool: call.name, dangerLevel, args: call.arguments, reason: networkPermission?.reason ?? permission.reason },
11904
+ { configDir: this.hookConfigDir, onSummary: (message) => this.send({ type: "info", message: "[hook] " + message }) }
11905
+ );
11906
+ }
11669
11907
  if (networkPermission?.action === "deny") {
11670
11908
  const reason = networkPermission.reason ? ` (${networkPermission.reason})` : "";
11671
11909
  return {
@@ -11689,11 +11927,13 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11689
11927
  const content = truncateOutput(rawContent, call.name);
11690
11928
  this.sendToolCallResult(call, rawContent, false);
11691
11929
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
11930
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
11692
11931
  return { callId: call.id, content, isError: false };
11693
11932
  } catch (err) {
11694
11933
  const message = err instanceof Error ? err.message : String(err);
11695
11934
  this.sendToolCallResult(call, message, true);
11696
11935
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
11936
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
11697
11937
  return { callId: call.id, content: message, isError: true };
11698
11938
  }
11699
11939
  }
@@ -11714,11 +11954,13 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11714
11954
  const content = truncateOutput(rawContent, call.name);
11715
11955
  this.sendToolCallResult(call, rawContent, false);
11716
11956
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
11957
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
11717
11958
  return { callId: call.id, content, isError: false };
11718
11959
  } catch (err) {
11719
11960
  const message = err instanceof Error ? err.message : String(err);
11720
11961
  this.sendToolCallResult(call, message, true);
11721
11962
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
11963
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
11722
11964
  return { callId: call.id, content: message, isError: true };
11723
11965
  }
11724
11966
  }
@@ -11736,11 +11978,13 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11736
11978
  const content = truncateOutput(rawContent, call.name);
11737
11979
  this.sendToolCallResult(call, rawContent, false);
11738
11980
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
11981
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
11739
11982
  return { callId: call.id, content, isError: false };
11740
11983
  } catch (err) {
11741
11984
  const message = err instanceof Error ? err.message : String(err);
11742
11985
  this.sendToolCallResult(call, message, true);
11743
11986
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
11987
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
11744
11988
  return { callId: call.id, content: message, isError: true };
11745
11989
  }
11746
11990
  }
@@ -11811,20 +12055,20 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11811
12055
  };
11812
12056
 
11813
12057
  // src/core/system-prompt-builder.ts
11814
- import { existsSync as existsSync19, readFileSync as readFileSync14 } from "fs";
11815
- import { join as join12 } from "path";
12058
+ import { existsSync as existsSync20, readFileSync as readFileSync15 } from "fs";
12059
+ import { join as join13 } from "path";
11816
12060
 
11817
12061
  // src/repl/dev-state.ts
11818
- import { existsSync as existsSync18, readFileSync as readFileSync13, unlinkSync as unlinkSync4, mkdirSync as mkdirSync9 } from "fs";
11819
- import { join as join11 } from "path";
12062
+ import { existsSync as existsSync19, readFileSync as readFileSync14, unlinkSync as unlinkSync4, mkdirSync as mkdirSync10 } from "fs";
12063
+ import { join as join12 } from "path";
11820
12064
  import { homedir as homedir6 } from "os";
11821
12065
  function getDevStatePath() {
11822
- return join11(homedir6(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
12066
+ return join12(homedir6(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
11823
12067
  }
11824
12068
  function loadDevState() {
11825
12069
  const path3 = getDevStatePath();
11826
- if (!existsSync18(path3)) return null;
11827
- const content = readFileSync13(path3, "utf-8").trim();
12070
+ if (!existsSync19(path3)) return null;
12071
+ const content = readFileSync14(path3, "utf-8").trim();
11828
12072
  return content || null;
11829
12073
  }
11830
12074
 
@@ -11882,9 +12126,9 @@ ${ctx.activeSkill.content}`);
11882
12126
  return { stable: stableParts.join("\n\n---\n\n"), volatile };
11883
12127
  }
11884
12128
  function loadMemoryContent(configDir) {
11885
- const memoryPath = join12(configDir, MEMORY_FILE_NAME);
11886
- if (!existsSync19(memoryPath)) return null;
11887
- let content = readFileSync14(memoryPath, "utf-8").trim();
12129
+ const memoryPath = join13(configDir, MEMORY_FILE_NAME);
12130
+ if (!existsSync20(memoryPath)) return null;
12131
+ let content = readFileSync15(memoryPath, "utf-8").trim();
11888
12132
  if (!content) return null;
11889
12133
  if (content.length > MEMORY_MAX_CHARS) {
11890
12134
  content = content.slice(-MEMORY_MAX_CHARS);
@@ -12093,8 +12337,8 @@ function autoTrimSessionIfNeeded(session, sizeLimit = SESSION_SIZE_LIMIT) {
12093
12337
  }
12094
12338
 
12095
12339
  // src/core/context-files.ts
12096
- import { existsSync as existsSync20, readFileSync as readFileSync15 } from "fs";
12097
- import { join as join13, relative as relative3, resolve as resolve6 } from "path";
12340
+ import { existsSync as existsSync21, readFileSync as readFileSync16 } from "fs";
12341
+ import { join as join14, relative as relative3, resolve as resolve6 } from "path";
12098
12342
  function uniqueNonEmpty(values) {
12099
12343
  const seen = /* @__PURE__ */ new Set();
12100
12344
  const out = [];
@@ -12124,7 +12368,7 @@ function readContextFile(level, filePath, cwd, maxBytes) {
12124
12368
  const name = filePath.split(/[\\/]/).pop() ?? filePath;
12125
12369
  const shown = displayPath(filePath, cwd);
12126
12370
  try {
12127
- const raw = readFileSync15(filePath);
12371
+ const raw = readFileSync16(filePath);
12128
12372
  const byteCount = raw.byteLength;
12129
12373
  if (byteCount === 0) {
12130
12374
  return {
@@ -12171,8 +12415,8 @@ function readContextFile(level, filePath, cwd, maxBytes) {
12171
12415
  function findFirstContextFile(level, dir, cwd, candidates, maxBytes) {
12172
12416
  const skipped = [];
12173
12417
  for (const candidate of candidates) {
12174
- const filePath = join13(dir, candidate);
12175
- if (!existsSync20(filePath)) continue;
12418
+ const filePath = join14(dir, candidate);
12419
+ if (!existsSync21(filePath)) continue;
12176
12420
  const result = readContextFile(level, filePath, cwd, maxBytes);
12177
12421
  if (result.skipped) skipped.push(result.skipped);
12178
12422
  if (result.layer) return { layer: result.layer, skipped };
@@ -12199,7 +12443,7 @@ function loadContextFiles(options) {
12199
12443
  });
12200
12444
  return { layers: [], mergedContent: "", skipped, totalChars: 0, totalBytes: 0, maxBytes, candidates };
12201
12445
  }
12202
- if (!existsSync20(filePath)) {
12446
+ if (!existsSync21(filePath)) {
12203
12447
  skipped.push({
12204
12448
  level: "single",
12205
12449
  filePath,
@@ -12250,13 +12494,13 @@ function loadContextFiles(options) {
12250
12494
  }
12251
12495
 
12252
12496
  // src/web/session-handler.ts
12253
- import { existsSync as existsSync23, readFileSync as readFileSync17, writeFileSync as writeFileSync2, mkdirSync as mkdirSync10, readdirSync as readdirSync10, statSync as statSync9, createWriteStream } from "fs";
12254
- import { join as join17, resolve as resolve7, dirname as dirname5 } from "path";
12497
+ import { existsSync as existsSync24, readFileSync as readFileSync18, writeFileSync as writeFileSync2, mkdirSync as mkdirSync11, readdirSync as readdirSync10, statSync as statSync9, createWriteStream } from "fs";
12498
+ import { join as join18, resolve as resolve7, dirname as dirname5 } from "path";
12255
12499
 
12256
12500
  // src/tools/git-context.ts
12257
12501
  import { execSync as execSync2 } from "child_process";
12258
- import { existsSync as existsSync21 } from "fs";
12259
- import { join as join14 } from "path";
12502
+ import { existsSync as existsSync22 } from "fs";
12503
+ import { join as join15 } from "path";
12260
12504
  function runGit2(cmd, cwd) {
12261
12505
  try {
12262
12506
  return execSync2(`git ${cmd}`, {
@@ -12273,7 +12517,7 @@ function getGitRoot(cwd = process.cwd()) {
12273
12517
  return runGit2("rev-parse --show-toplevel", cwd);
12274
12518
  }
12275
12519
  function getGitContext(cwd = process.cwd()) {
12276
- if (!existsSync21(join14(cwd, ".git"))) {
12520
+ if (!existsSync22(join15(cwd, ".git"))) {
12277
12521
  const result = runGit2("rev-parse --git-dir", cwd);
12278
12522
  if (!result) return null;
12279
12523
  }
@@ -12402,8 +12646,8 @@ If no security issues found, state "\u2705 No security vulnerabilities detected"
12402
12646
  }
12403
12647
 
12404
12648
  // src/repl/commands/project-init.ts
12405
- import { existsSync as existsSync22, readFileSync as readFileSync16, readdirSync as readdirSync9, statSync as statSync8 } from "fs";
12406
- import { join as join15 } from "path";
12649
+ import { existsSync as existsSync23, readFileSync as readFileSync17, readdirSync as readdirSync9, statSync as statSync8 } from "fs";
12650
+ import { join as join16 } from "path";
12407
12651
  var SCAN_SKIP_DIRS = /* @__PURE__ */ new Set([
12408
12652
  "node_modules",
12409
12653
  ".git",
@@ -12447,11 +12691,11 @@ function scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
12447
12691
  const sorted = filtered.sort((a, b) => {
12448
12692
  let aIsDir = false, bIsDir = false;
12449
12693
  try {
12450
- aIsDir = statSync8(join15(d, a)).isDirectory();
12694
+ aIsDir = statSync8(join16(d, a)).isDirectory();
12451
12695
  } catch {
12452
12696
  }
12453
12697
  try {
12454
- bIsDir = statSync8(join15(d, b)).isDirectory();
12698
+ bIsDir = statSync8(join16(d, b)).isDirectory();
12455
12699
  } catch {
12456
12700
  }
12457
12701
  if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
@@ -12459,7 +12703,7 @@ function scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
12459
12703
  });
12460
12704
  for (let i = 0; i < sorted.length && count < maxEntries; i++) {
12461
12705
  const name = sorted[i];
12462
- const fullPath = join15(d, name);
12706
+ const fullPath = join16(d, name);
12463
12707
  const isLast = i === sorted.length - 1;
12464
12708
  const connector = isLast ? "+-- " : "|-- ";
12465
12709
  let isDir;
@@ -12486,7 +12730,7 @@ function scanProject(cwd) {
12486
12730
  configFiles: [],
12487
12731
  directoryStructure: ""
12488
12732
  };
12489
- const check = (file) => existsSync22(join15(cwd, file));
12733
+ const check = (file) => existsSync23(join16(cwd, file));
12490
12734
  const configCandidates = [
12491
12735
  "package.json",
12492
12736
  "tsconfig.json",
@@ -12513,7 +12757,7 @@ function scanProject(cwd) {
12513
12757
  info.type = "node";
12514
12758
  info.language = check("tsconfig.json") ? "TypeScript" : "JavaScript";
12515
12759
  try {
12516
- const pkg = JSON.parse(readFileSync16(join15(cwd, "package.json"), "utf-8"));
12760
+ const pkg = JSON.parse(readFileSync17(join16(cwd, "package.json"), "utf-8"));
12517
12761
  const scripts = pkg.scripts ?? {};
12518
12762
  info.buildCommand = scripts.build ? "npm run build" : void 0;
12519
12763
  info.testCommand = scripts.test ? "npm test" : void 0;
@@ -13173,7 +13417,7 @@ function resolveRoleProviders(roles, lookup, defaultProvider, defaultModel, avai
13173
13417
  }
13174
13418
 
13175
13419
  // src/hub/persist.ts
13176
- import { join as join16 } from "path";
13420
+ import { join as join17 } from "path";
13177
13421
  function discussionToMessages(state2) {
13178
13422
  const out = [];
13179
13423
  const t0 = state2.messages[0]?.timestamp ?? /* @__PURE__ */ new Date();
@@ -13211,7 +13455,7 @@ async function persistDiscussion(state2, config, defaultProvider, defaultModel)
13211
13455
  session.title = `[Hub] ${state2.topic.slice(0, 48)}`.replace(/\n/g, " ");
13212
13456
  session.titleAiGenerated = true;
13213
13457
  await sm.save();
13214
- return { id: session.id, path: join16(config.getHistoryDir(), `${session.id}.json`) };
13458
+ return { id: session.id, path: join17(config.getHistoryDir(), `${session.id}.json`) };
13215
13459
  }
13216
13460
 
13217
13461
  // src/web/session-handler.ts
@@ -13314,6 +13558,7 @@ var SessionHandler = class {
13314
13558
  const permissionProfiles = this.config.get("permissionProfiles") ?? {};
13315
13559
  this.toolExecutor.setConfig({
13316
13560
  hookConfig: hooks,
13561
+ hookConfigDir: this.config.getConfigDir(),
13317
13562
  permissionRules,
13318
13563
  defaultPermission,
13319
13564
  permissionProfileName,
@@ -13326,6 +13571,11 @@ var SessionHandler = class {
13326
13571
  if (permissionProfileWarning) {
13327
13572
  this.send({ type: "info", message: permissionProfileWarning });
13328
13573
  }
13574
+ const pendingHooks = getPendingHookTrust(hooks, this.config.getConfigDir());
13575
+ if (pendingHooks.length > 0) {
13576
+ this.send({ type: "info", message: `${pendingHooks.length} project hook(s) require trust. Use /hooks list and /hooks trust <id>.` });
13577
+ }
13578
+ runLifecycleHooks(hooks, "SessionStart", { source: "web" }, { configDir: this.config.getConfigDir() });
13329
13579
  this.sendStatus();
13330
13580
  askUserContext.rl = null;
13331
13581
  askUserContext.prompting = false;
@@ -13434,6 +13684,7 @@ var SessionHandler = class {
13434
13684
  }
13435
13685
  }
13436
13686
  onDisconnect() {
13687
+ runLifecycleHooks(this.config.get("hooks") ?? void 0, "Stop", { sessionId: this.sessions.current?.id }, { configDir: this.config.getConfigDir() });
13437
13688
  this.toolExecutor.cancelAll();
13438
13689
  if (this.abortController) this.abortController.abort();
13439
13690
  for (const resolve9 of this.pendingAskUser.values()) resolve9(null);
@@ -13611,6 +13862,26 @@ var SessionHandler = class {
13611
13862
  try {
13612
13863
  this.ensureSession();
13613
13864
  const session = this.sessions.current;
13865
+ const promptDecisions = runLifecycleHooks(
13866
+ this.config.get("hooks") ?? void 0,
13867
+ "UserPromptSubmit",
13868
+ { sessionId: session.id, prompt: content },
13869
+ { configDir: this.config.getConfigDir(), onSummary: (message) => this.send({ type: "info", message: "[hook] " + message }) }
13870
+ );
13871
+ const promptDeny = promptDecisions.find((d) => d.action === "deny");
13872
+ if (promptDeny) {
13873
+ this.send({ type: "error", message: "Prompt blocked by hook: " + (promptDeny.reason ?? "UserPromptSubmit denied") });
13874
+ return;
13875
+ }
13876
+ for (const d of promptDecisions) {
13877
+ const warnings = [d.warning, ...d.warnings ?? []].filter(Boolean);
13878
+ for (const warning of warnings) this.send({ type: "info", message: "[hook] " + warning });
13879
+ }
13880
+ const secretHits = scanString(content, { customRegexes: [] });
13881
+ if (secretHits.length > 0) {
13882
+ const kinds = [...new Set(secretHits.map((h) => h.kind))].join(", ");
13883
+ this.send({ type: "info", message: "Possible secret in prompt (" + kinds + ")." });
13884
+ }
13614
13885
  const ALLOWED_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"]);
13615
13886
  const MAX_IMAGE_SIZE = 10 * 1024 * 1024;
13616
13887
  let msgContent;
@@ -13886,7 +14157,7 @@ Try: /compact to reduce context, /clear to reset, or switch to a larger-context
13886
14157
  this.send({ type: "response_done", content, usage });
13887
14158
  if (pendingTeeSave && isCleanDocumentBody(content)) {
13888
14159
  try {
13889
- mkdirSync10(dirname5(pendingTeeSave), { recursive: true });
14160
+ mkdirSync11(dirname5(pendingTeeSave), { recursive: true });
13890
14161
  const bodyToSave = stripOuterCodeFence(content);
13891
14162
  atomicWriteFileSync(pendingTeeSave, bodyToSave);
13892
14163
  undoStack.push(pendingTeeSave, `save_last_response (deferred): ${pendingTeeSave}`);
@@ -14062,7 +14333,7 @@ ${summaryContent}`,
14062
14333
  let isError = false;
14063
14334
  let summary;
14064
14335
  try {
14065
- mkdirSync10(dirname5(saveToFile), { recursive: true });
14336
+ mkdirSync11(dirname5(saveToFile), { recursive: true });
14066
14337
  fileStream = createWriteStream(saveToFile);
14067
14338
  const teeSystemPrompt = stripToolCallReminder(systemPrompt ?? "") + CONTENT_ONLY_STREAM_REMINDER;
14068
14339
  const teeExtraMessages = extraMessages.length > 0 ? [...extraMessages, { role: "user", content: TEE_FINAL_USER_NUDGE }] : [{ role: "user", content: TEE_FINAL_USER_NUDGE }];
@@ -14648,9 +14919,9 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
14648
14919
  let modifiedFiles = 0;
14649
14920
  const diffLines2 = [];
14650
14921
  for (const [filePath, { earliest }] of fileMap) {
14651
- const currentContent = existsSync23(filePath) ? (() => {
14922
+ const currentContent = existsSync24(filePath) ? (() => {
14652
14923
  try {
14653
- return readFileSync17(filePath, "utf-8");
14924
+ return readFileSync18(filePath, "utf-8");
14654
14925
  } catch {
14655
14926
  return null;
14656
14927
  }
@@ -15166,7 +15437,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
15166
15437
  case "test": {
15167
15438
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
15168
15439
  try {
15169
- const { executeTests } = await import("./run-tests-PACN4UYX.js");
15440
+ const { executeTests } = await import("./run-tests-D7YBY4XB.js");
15170
15441
  const argStr = args.join(" ").trim();
15171
15442
  let testArgs = {};
15172
15443
  if (argStr) {
@@ -15183,9 +15454,9 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
15183
15454
  // ── /init ───────────────────────────────────────────────────────
15184
15455
  case "init": {
15185
15456
  const cwd = process.cwd();
15186
- const targetPath = join17(cwd, "AICLI.md");
15457
+ const targetPath = join18(cwd, "AICLI.md");
15187
15458
  const force = args.includes("--force");
15188
- if (existsSync23(targetPath) && !force) {
15459
+ if (existsSync24(targetPath) && !force) {
15189
15460
  this.send({ type: "info", message: `AICLI.md already exists at ${targetPath}
15190
15461
  Use /init --force to overwrite.` });
15191
15462
  break;
@@ -15218,7 +15489,7 @@ Use /context reload to load it.` });
15218
15489
  lines.push("**Config Files:**");
15219
15490
  lines.push(` Dir: ${configDir}`);
15220
15491
  const checkFile = (label, filePath) => {
15221
- const exists = existsSync23(filePath);
15492
+ const exists = existsSync24(filePath);
15222
15493
  let extra = "";
15223
15494
  if (exists) {
15224
15495
  try {
@@ -15228,9 +15499,9 @@ Use /context reload to load it.` });
15228
15499
  }
15229
15500
  lines.push(` ${exists ? "\u2713" : "\u2013"} ${label.padEnd(14)} ${exists ? filePath + extra : "(not found)"}`);
15230
15501
  };
15231
- checkFile("config.json", join17(configDir, "config.json"));
15232
- checkFile("memory.md", join17(configDir, MEMORY_FILE_NAME));
15233
- checkFile("dev-state.md", join17(configDir, "dev-state.md"));
15502
+ checkFile("config.json", join18(configDir, "config.json"));
15503
+ checkFile("memory.md", join18(configDir, MEMORY_FILE_NAME));
15504
+ checkFile("dev-state.md", join18(configDir, "dev-state.md"));
15234
15505
  lines.push("");
15235
15506
  if (this.mcpManager) {
15236
15507
  lines.push("**MCP Servers:**");
@@ -15418,7 +15689,7 @@ Use /add-dir remove to clear.` : "No directories added.\nUsage: /add-dir <path>
15418
15689
  break;
15419
15690
  }
15420
15691
  const dirPath = resolve7(sub);
15421
- if (!existsSync23(dirPath)) {
15692
+ if (!existsSync24(dirPath)) {
15422
15693
  this.send({ type: "error", message: `Directory not found: ${dirPath}` });
15423
15694
  break;
15424
15695
  }
@@ -15434,8 +15705,8 @@ It will be included in AI context for subsequent messages.` });
15434
15705
  // ── /commands ───────────────────────────────────────────────────
15435
15706
  case "commands": {
15436
15707
  const configDir = this.config.getConfigDir();
15437
- const commandsDir = join17(configDir, CUSTOM_COMMANDS_DIR_NAME);
15438
- if (!existsSync23(commandsDir)) {
15708
+ const commandsDir = join18(configDir, CUSTOM_COMMANDS_DIR_NAME);
15709
+ if (!existsSync24(commandsDir)) {
15439
15710
  this.send({ type: "info", message: `No custom commands directory.
15440
15711
  Create: ${commandsDir}/ with .md files.` });
15441
15712
  break;
@@ -15461,7 +15732,7 @@ Add .md files to create commands.` });
15461
15732
  // ── /plugins ────────────────────────────────────────────────────
15462
15733
  case "plugins": {
15463
15734
  const configDir = this.config.getConfigDir();
15464
- const pluginsDir = join17(configDir, PLUGINS_DIR_NAME);
15735
+ const pluginsDir = join18(configDir, PLUGINS_DIR_NAME);
15465
15736
  const pluginTools = this.toolRegistry.listPluginTools();
15466
15737
  const lines = [`\u{1F50C} **Plugins:**`, `Dir: ${pluginsDir}`, ""];
15467
15738
  if (pluginTools.length === 0) {
@@ -15516,6 +15787,21 @@ Add .md files to create commands.` });
15516
15787
  this.send({ type: "info", message: "Not enough messages to compact." });
15517
15788
  return;
15518
15789
  }
15790
+ const preDecisions = runLifecycleHooks(
15791
+ this.config.get("hooks") ?? void 0,
15792
+ "PreCompact",
15793
+ { sessionId: session.id, messageCount: session.messages.length, instruction },
15794
+ { configDir: this.config.getConfigDir(), onSummary: (message) => this.send({ type: "info", message: "[hook] " + message }) }
15795
+ );
15796
+ const preDeny = preDecisions.find((d) => d.action === "deny");
15797
+ if (preDeny) {
15798
+ this.send({ type: "error", message: "Compact blocked by hook: " + (preDeny.reason ?? "PreCompact denied") });
15799
+ return;
15800
+ }
15801
+ for (const d of preDecisions) {
15802
+ const warnings = [d.warning, ...d.warnings ?? []].filter(Boolean);
15803
+ for (const warning of warnings) this.send({ type: "info", message: "[hook] " + warning });
15804
+ }
15519
15805
  const provider = this.providers.get(this.currentProvider);
15520
15806
  if (!provider) return;
15521
15807
  this.send({ type: "info", message: "Compacting conversation..." });
@@ -15535,7 +15821,15 @@ Add .md files to create commands.` });
15535
15821
  });
15536
15822
  const summaryMsg = { role: "user", content: "[Conversation summary]", timestamp: /* @__PURE__ */ new Date() };
15537
15823
  const ackMsg = { role: "assistant", content: response.content, timestamp: /* @__PURE__ */ new Date() };
15538
- session.compact(summaryMsg, ackMsg, keepLast);
15824
+ const beforeCount = session.messages.length;
15825
+ const removedCount = session.compact(summaryMsg, ackMsg, keepLast);
15826
+ const afterCount = session.messages.length;
15827
+ runLifecycleHooks(
15828
+ this.config.get("hooks") ?? void 0,
15829
+ "PostCompact",
15830
+ { sessionId: session.id, beforeCount, afterCount, removedCount, keepLast, instruction },
15831
+ { configDir: this.config.getConfigDir(), onSummary: (message) => this.send({ type: "info", message: "[hook] " + message }) }
15832
+ );
15539
15833
  this.send({ type: "info", message: `Compacted: removed ${toSummarize.length} messages, kept last ${keepLast}.` });
15540
15834
  this.sendStatus();
15541
15835
  } catch (err) {
@@ -15635,11 +15929,11 @@ Add .md files to create commands.` });
15635
15929
  }
15636
15930
  memoryShow() {
15637
15931
  const configDir = this.config.getConfigDir();
15638
- const memPath = join17(configDir, MEMORY_FILE_NAME);
15932
+ const memPath = join18(configDir, MEMORY_FILE_NAME);
15639
15933
  let content = "";
15640
15934
  try {
15641
- if (existsSync23(memPath)) {
15642
- content = readFileSync17(memPath, "utf-8");
15935
+ if (existsSync24(memPath)) {
15936
+ content = readFileSync18(memPath, "utf-8");
15643
15937
  }
15644
15938
  } catch (err) {
15645
15939
  process.stderr.write(`[web] Failed to read memory file: ${err instanceof Error ? err.message : err}
@@ -15653,11 +15947,11 @@ Add .md files to create commands.` });
15653
15947
  }
15654
15948
  memoryAdd(text) {
15655
15949
  const configDir = this.config.getConfigDir();
15656
- const memPath = join17(configDir, MEMORY_FILE_NAME);
15950
+ const memPath = join18(configDir, MEMORY_FILE_NAME);
15657
15951
  try {
15658
- mkdirSync10(configDir, { recursive: true });
15952
+ mkdirSync11(configDir, { recursive: true });
15659
15953
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ");
15660
- const previous = existsSync23(memPath) ? readFileSync17(memPath, "utf-8") : "";
15954
+ const previous = existsSync24(memPath) ? readFileSync18(memPath, "utf-8") : "";
15661
15955
  const entry = `
15662
15956
  - [${timestamp}] ${text}
15663
15957
  `;
@@ -15669,7 +15963,7 @@ Add .md files to create commands.` });
15669
15963
  }
15670
15964
  memoryClear() {
15671
15965
  const configDir = this.config.getConfigDir();
15672
- const memPath = join17(configDir, MEMORY_FILE_NAME);
15966
+ const memPath = join18(configDir, MEMORY_FILE_NAME);
15673
15967
  try {
15674
15968
  atomicWriteFileSync(memPath, "");
15675
15969
  this.send({ type: "info", message: "\u{1F5D1}\uFE0F Persistent memory cleared." });
@@ -15687,7 +15981,7 @@ Add .md files to create commands.` });
15687
15981
  return;
15688
15982
  }
15689
15983
  try {
15690
- const { searchChatMemory: searchChatMemory2, loadChatIndex: loadChatIndex2 } = await import("./chat-index-UBCWHBLR.js");
15984
+ const { searchChatMemory: searchChatMemory2, loadChatIndex: loadChatIndex2 } = await import("./chat-index-R2E27VXN.js");
15691
15985
  const loaded = loadChatIndex2();
15692
15986
  if (!loaded || loaded.idx.chunks.length === 0) {
15693
15987
  this.send({ type: "memory_hits", query: q, hits: [], indexMissing: true });
@@ -15723,7 +16017,7 @@ Add .md files to create commands.` });
15723
16017
  }
15724
16018
  async handleMemoryStatus() {
15725
16019
  try {
15726
- const { getChatIndexStatus } = await import("./chat-index-UBCWHBLR.js");
16020
+ const { getChatIndexStatus } = await import("./chat-index-R2E27VXN.js");
15727
16021
  const s = getChatIndexStatus();
15728
16022
  this.send({
15729
16023
  type: "memory_status",
@@ -15748,7 +16042,7 @@ Add .md files to create commands.` });
15748
16042
  type: "info",
15749
16043
  message: full ? "\u{1F9E0} Rebuilding chat memory index (this may take a while on first run \u2014 ~117 MB embedder)." : "\u{1F9E0} Refreshing chat memory index (incremental)\u2026"
15750
16044
  });
15751
- const { buildChatIndex } = await import("./chat-index-UBCWHBLR.js");
16045
+ const { buildChatIndex } = await import("./chat-index-R2E27VXN.js");
15752
16046
  const stats = await buildChatIndex({
15753
16047
  full,
15754
16048
  onProgress: (p) => {
@@ -15903,8 +16197,8 @@ async function setupProxy(configProxy) {
15903
16197
  }
15904
16198
 
15905
16199
  // src/web/auth.ts
15906
- import { existsSync as existsSync24, readFileSync as readFileSync18, writeFileSync as writeFileSync3, mkdirSync as mkdirSync11, readdirSync as readdirSync11, copyFileSync } from "fs";
15907
- import { join as join18 } from "path";
16200
+ import { existsSync as existsSync25, readFileSync as readFileSync19, writeFileSync as writeFileSync3, mkdirSync as mkdirSync12, readdirSync as readdirSync11, copyFileSync } from "fs";
16201
+ import { join as join19 } from "path";
15908
16202
  import { createHmac, randomBytes, timingSafeEqual, pbkdf2Sync } from "crypto";
15909
16203
  var USERS_FILE = "users.json";
15910
16204
  var TOKEN_EXPIRY_HOURS = 24;
@@ -15919,7 +16213,7 @@ var AuthManager = class {
15919
16213
  db;
15920
16214
  constructor(baseDir) {
15921
16215
  this.baseDir = baseDir;
15922
- this.usersFile = join18(baseDir, USERS_FILE);
16216
+ this.usersFile = join19(baseDir, USERS_FILE);
15923
16217
  this.db = this.loadOrCreate();
15924
16218
  }
15925
16219
  // ── Public API ─────────────────────────────────────────────────
@@ -15948,9 +16242,9 @@ var AuthManager = class {
15948
16242
  }
15949
16243
  const salt = randomBytes(16).toString("hex");
15950
16244
  const passwordHash = this.hashPassword(password, salt);
15951
- const dataDir = join18(USERS_DIR, username);
15952
- const fullDataDir = join18(this.baseDir, dataDir);
15953
- mkdirSync11(join18(fullDataDir, "history"), { recursive: true });
16245
+ const dataDir = join19(USERS_DIR, username);
16246
+ const fullDataDir = join19(this.baseDir, dataDir);
16247
+ mkdirSync12(join19(fullDataDir, "history"), { recursive: true });
15954
16248
  const user = {
15955
16249
  username,
15956
16250
  passwordHash,
@@ -16065,7 +16359,7 @@ var AuthManager = class {
16065
16359
  getUserDataDir(username) {
16066
16360
  const user = this.db.users.find((u) => u.username === username);
16067
16361
  if (!user) throw new Error(`User not found: ${username}`);
16068
- return join18(this.baseDir, user.dataDir);
16362
+ return join19(this.baseDir, user.dataDir);
16069
16363
  }
16070
16364
  /** List all usernames */
16071
16365
  listUsers() {
@@ -16101,30 +16395,30 @@ var AuthManager = class {
16101
16395
  const err = this.register(username, password);
16102
16396
  if (err) return err;
16103
16397
  const userDir = this.getUserDataDir(username);
16104
- const globalConfig = join18(this.baseDir, "config.json");
16105
- if (existsSync24(globalConfig)) {
16398
+ const globalConfig = join19(this.baseDir, "config.json");
16399
+ if (existsSync25(globalConfig)) {
16106
16400
  try {
16107
- const content = readFileSync18(globalConfig, "utf-8");
16108
- writeFileSync3(join18(userDir, "config.json"), content, "utf-8");
16401
+ const content = readFileSync19(globalConfig, "utf-8");
16402
+ writeFileSync3(join19(userDir, "config.json"), content, "utf-8");
16109
16403
  } catch {
16110
16404
  }
16111
16405
  }
16112
- const globalMemory = join18(this.baseDir, "memory.md");
16113
- if (existsSync24(globalMemory)) {
16406
+ const globalMemory = join19(this.baseDir, "memory.md");
16407
+ if (existsSync25(globalMemory)) {
16114
16408
  try {
16115
- const content = readFileSync18(globalMemory, "utf-8");
16116
- writeFileSync3(join18(userDir, "memory.md"), content, "utf-8");
16409
+ const content = readFileSync19(globalMemory, "utf-8");
16410
+ writeFileSync3(join19(userDir, "memory.md"), content, "utf-8");
16117
16411
  } catch {
16118
16412
  }
16119
16413
  }
16120
- const globalHistory = join18(this.baseDir, "history");
16121
- if (existsSync24(globalHistory)) {
16414
+ const globalHistory = join19(this.baseDir, "history");
16415
+ if (existsSync25(globalHistory)) {
16122
16416
  try {
16123
16417
  const files = readdirSync11(globalHistory).filter((f) => f.endsWith(".json"));
16124
- const userHistory = join18(userDir, "history");
16418
+ const userHistory = join19(userDir, "history");
16125
16419
  for (const f of files) {
16126
16420
  try {
16127
- copyFileSync(join18(globalHistory, f), join18(userHistory, f));
16421
+ copyFileSync(join19(globalHistory, f), join19(userHistory, f));
16128
16422
  } catch {
16129
16423
  }
16130
16424
  }
@@ -16135,9 +16429,9 @@ var AuthManager = class {
16135
16429
  }
16136
16430
  // ── Private methods ────────────────────────────────────────────
16137
16431
  loadOrCreate() {
16138
- if (existsSync24(this.usersFile)) {
16432
+ if (existsSync25(this.usersFile)) {
16139
16433
  try {
16140
- return JSON.parse(readFileSync18(this.usersFile, "utf-8"));
16434
+ return JSON.parse(readFileSync19(this.usersFile, "utf-8"));
16141
16435
  } catch {
16142
16436
  }
16143
16437
  }
@@ -16153,7 +16447,7 @@ var AuthManager = class {
16153
16447
  this.saveDB(this.db);
16154
16448
  }
16155
16449
  saveDB(db) {
16156
- mkdirSync11(this.baseDir, { recursive: true });
16450
+ mkdirSync12(this.baseDir, { recursive: true });
16157
16451
  atomicWriteFileSync(this.usersFile, JSON.stringify(db, null, 2));
16158
16452
  }
16159
16453
  /** Legacy hash — kept only for migrating old users (v0.2.x) */
@@ -16254,8 +16548,8 @@ async function startWebServer(options = {}) {
16254
16548
  }
16255
16549
  }
16256
16550
  let skillManager = null;
16257
- const skillsDir = join19(config.getConfigDir(), SKILLS_DIR_NAME);
16258
- if (existsSync25(skillsDir)) {
16551
+ const skillsDir = join20(config.getConfigDir(), SKILLS_DIR_NAME);
16552
+ if (existsSync26(skillsDir)) {
16259
16553
  skillManager = new SkillManager(skillsDir, config.get("ui").skillSizeWarn);
16260
16554
  skillManager.loadSkills();
16261
16555
  const count = skillManager.listSkills().length;
@@ -16326,18 +16620,18 @@ async function startWebServer(options = {}) {
16326
16620
  next();
16327
16621
  };
16328
16622
  const moduleDir = getModuleDir();
16329
- let clientDir = join19(moduleDir, "web", "client");
16330
- if (!existsSync25(clientDir)) {
16331
- clientDir = join19(moduleDir, "client");
16623
+ let clientDir = join20(moduleDir, "web", "client");
16624
+ if (!existsSync26(clientDir)) {
16625
+ clientDir = join20(moduleDir, "client");
16332
16626
  }
16333
- if (!existsSync25(clientDir)) {
16334
- clientDir = join19(moduleDir, "..", "..", "src", "web", "client");
16627
+ if (!existsSync26(clientDir)) {
16628
+ clientDir = join20(moduleDir, "..", "..", "src", "web", "client");
16335
16629
  }
16336
- if (!existsSync25(clientDir)) {
16337
- clientDir = join19(process.cwd(), "src", "web", "client");
16630
+ if (!existsSync26(clientDir)) {
16631
+ clientDir = join20(process.cwd(), "src", "web", "client");
16338
16632
  }
16339
16633
  console.log(` Static files: ${clientDir}`);
16340
- app.use("/vendor", express.static(join19(clientDir, "vendor"), { maxAge: "1y", immutable: true }));
16634
+ app.use("/vendor", express.static(join20(clientDir, "vendor"), { maxAge: "1y", immutable: true }));
16341
16635
  app.use(express.static(clientDir));
16342
16636
  app.get("/api/status", (_req, res) => {
16343
16637
  res.json({
@@ -16404,7 +16698,7 @@ async function startWebServer(options = {}) {
16404
16698
  app.get("/api/files", requireAuth, (req, res) => {
16405
16699
  const cwd = process.cwd();
16406
16700
  const prefix = req.query.prefix || "";
16407
- const targetDir = join19(cwd, prefix);
16701
+ const targetDir = join20(cwd, prefix);
16408
16702
  try {
16409
16703
  const canonicalTarget = realpathSync(resolve8(targetDir));
16410
16704
  const canonicalCwd = realpathSync(resolve8(cwd));
@@ -16421,7 +16715,7 @@ async function startWebServer(options = {}) {
16421
16715
  const entries = readdirSync12(targetDir, { withFileTypes: true });
16422
16716
  const files = entries.filter((e) => !SKIP.has(e.name) && !e.name.startsWith(".")).slice(0, 50).map((e) => ({
16423
16717
  name: e.name,
16424
- path: relative4(cwd, join19(targetDir, e.name)).replace(/\\/g, "/"),
16718
+ path: relative4(cwd, join20(targetDir, e.name)).replace(/\\/g, "/"),
16425
16719
  isDir: e.isDirectory()
16426
16720
  }));
16427
16721
  res.json({ files });
@@ -16457,8 +16751,8 @@ async function startWebServer(options = {}) {
16457
16751
  try {
16458
16752
  const authUser = req._authUser;
16459
16753
  const histDir = authUser ? getUserShared(authUser).config.getHistoryDir() : config.getHistoryDir();
16460
- const filePath = join19(histDir, `${id}.json`);
16461
- if (!existsSync25(filePath)) {
16754
+ const filePath = join20(histDir, `${id}.json`);
16755
+ if (!existsSync26(filePath)) {
16462
16756
  res.status(404).json({ error: "Session not found" });
16463
16757
  return;
16464
16758
  }
@@ -16473,7 +16767,7 @@ async function startWebServer(options = {}) {
16473
16767
  res.status(404).json({ error: "Session not found" });
16474
16768
  return;
16475
16769
  }
16476
- const data = JSON.parse(readFileSync19(filePath, "utf-8"));
16770
+ const data = JSON.parse(readFileSync20(filePath, "utf-8"));
16477
16771
  res.json({ session: data });
16478
16772
  } catch (err) {
16479
16773
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
@@ -16486,7 +16780,7 @@ async function startWebServer(options = {}) {
16486
16780
  return;
16487
16781
  }
16488
16782
  const cwd = process.cwd();
16489
- const fullPath = resolve8(join19(cwd, filePath));
16783
+ const fullPath = resolve8(join20(cwd, filePath));
16490
16784
  try {
16491
16785
  const canonicalFull = realpathSync(fullPath);
16492
16786
  const canonicalCwd = realpathSync(resolve8(cwd));
@@ -16504,7 +16798,7 @@ async function startWebServer(options = {}) {
16504
16798
  res.json({ error: `File too large (${(stat.size / 1024).toFixed(0)} KB, max 512 KB)` });
16505
16799
  return;
16506
16800
  }
16507
- const content = readFileSync19(fullPath, "utf-8");
16801
+ const content = readFileSync20(fullPath, "utf-8");
16508
16802
  res.json({ content, size: stat.size });
16509
16803
  } catch {
16510
16804
  res.json({ error: "Cannot read file" });
@@ -16759,17 +17053,17 @@ function resolveProjectMcpPath() {
16759
17053
  const cwd = process.cwd();
16760
17054
  const gitRoot = getGitRoot(cwd);
16761
17055
  const projectRoot = gitRoot ?? cwd;
16762
- const configPath = join19(projectRoot, MCP_PROJECT_CONFIG_NAME);
16763
- return existsSync25(configPath) ? configPath : null;
17056
+ const configPath = join20(projectRoot, MCP_PROJECT_CONFIG_NAME);
17057
+ return existsSync26(configPath) ? configPath : null;
16764
17058
  }
16765
17059
  function loadProjectMcpConfig() {
16766
17060
  const cwd = process.cwd();
16767
17061
  const gitRoot = getGitRoot(cwd);
16768
17062
  const projectRoot = gitRoot ?? cwd;
16769
- const configPath = join19(projectRoot, MCP_PROJECT_CONFIG_NAME);
16770
- if (!existsSync25(configPath)) return null;
17063
+ const configPath = join20(projectRoot, MCP_PROJECT_CONFIG_NAME);
17064
+ if (!existsSync26(configPath)) return null;
16771
17065
  try {
16772
- const raw = JSON.parse(readFileSync19(configPath, "utf-8"));
17066
+ const raw = JSON.parse(readFileSync20(configPath, "utf-8"));
16773
17067
  return raw.mcpServers ?? raw;
16774
17068
  } catch {
16775
17069
  return null;