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
@@ -37,7 +37,7 @@ import {
37
37
  VERSION,
38
38
  buildUserIdentityPrompt,
39
39
  runTestsTool
40
- } from "./chunk-HZQVX7VF.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 resolve7, 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,12 +209,69 @@ 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 网络出口。
241
+ networkPolicy: z.object({
242
+ enabled: z.boolean().default(false),
243
+ defaultAction: z.enum(["allow", "confirm", "deny"]).default("confirm"),
244
+ allowDomains: z.array(z.string()).default([]),
245
+ denyDomains: z.array(z.string()).default([]),
246
+ allowPorts: z.array(z.number().int().min(1).max(65535)).default([]),
247
+ denyPorts: z.array(z.number().int().min(1).max(65535)).default([]),
248
+ allowPrivateNetwork: z.boolean().default(false),
249
+ tools: z.object({
250
+ web_fetch: z.enum(["allow", "confirm", "deny"]).optional(),
251
+ web_search: z.enum(["allow", "confirm", "deny"]).optional(),
252
+ google_search: z.enum(["allow", "confirm", "deny"]).optional(),
253
+ shell: z.enum(["allow", "confirm", "deny"]).optional(),
254
+ mcp: z.enum(["allow", "confirm", "deny"]).optional()
255
+ }).default({})
256
+ }).default({ enabled: false, defaultAction: "confirm", allowDomains: [], denyDomains: [], allowPorts: [], denyPorts: [], allowPrivateNetwork: false, tools: {} }),
257
+ // 权限 Profile(v0.4.211+):legacy 保持旧行为;其余 profile 提供更清晰的安全边界。
258
+ defaultPermissionProfile: z.enum(["legacy", "read-only", "workspace-write", "danger-full-access"]).default("legacy"),
259
+ allowedPermissionProfiles: z.array(z.string()).default(["legacy", "read-only", "workspace-write", "danger-full-access"]),
260
+ permissionProfiles: z.record(z.object({
261
+ extends: z.enum(["legacy", "read-only", "workspace-write", "danger-full-access"]).optional(),
262
+ defaultAction: z.enum(["auto-approve", "deny", "confirm"]).optional(),
263
+ workspaceRoots: z.array(z.string()).optional(),
264
+ allowTemp: z.boolean().optional(),
265
+ rules: z.array(z.object({
266
+ tool: z.string(),
267
+ action: z.enum(["auto-approve", "deny", "confirm"]),
268
+ when: z.object({
269
+ dangerLevel: z.enum(["safe", "write", "destructive"]).optional(),
270
+ pathPattern: z.string().optional()
271
+ }).optional()
272
+ })).optional()
273
+ })).default({}),
274
+ // 工具权限规则(按顺序匹配第一个生效;作为当前权限 Profile 内的细粒度规则)
217
275
  permissionRules: z.array(z.object({
218
276
  tool: z.string(),
219
277
  action: z.enum(["auto-approve", "deny", "confirm"]),
@@ -222,7 +280,7 @@ var ConfigSchema = z.object({
222
280
  pathPattern: z.string().optional()
223
281
  }).optional()
224
282
  })).default([]),
225
- // 无规则匹配时的默认权限动作
283
+ // 无规则匹配时的默认权限动作(legacy profile 兼容旧行为)
226
284
  defaultPermission: z.enum(["auto-approve", "deny", "confirm"]).default("confirm"),
227
285
  // 自动上下文压缩开关
228
286
  // 当对话估算 token 数超过模型 contextWindow 的 80% 时,自动触发 compact 压缩旧消息
@@ -5727,12 +5785,13 @@ ${content}`;
5727
5785
  };
5728
5786
 
5729
5787
  // src/tools/builtin/write-file.ts
5730
- import { appendFileSync, mkdirSync as mkdirSync4 } from "fs";
5788
+ import { appendFileSync, mkdirSync as mkdirSync5 } from "fs";
5731
5789
  import { dirname as dirname3 } from "path";
5732
5790
 
5733
5791
  // src/tools/executor.ts
5734
5792
  import chalk3 from "chalk";
5735
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
5793
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
5794
+ import { tmpdir } from "os";
5736
5795
 
5737
5796
  // src/core/readline-internal.ts
5738
5797
  function rlInternal(rl) {
@@ -5939,6 +5998,139 @@ function simpleDiff(oldLines, newLines) {
5939
5998
 
5940
5999
  // src/tools/hooks.ts
5941
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
+ }
5942
6134
  function rewriteTemplate(template, isWindows) {
5943
6135
  const ref = (name) => isWindows ? `%${name}%` : `$${name}`;
5944
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"));
@@ -5949,7 +6141,7 @@ function runHook(template, vars) {
5949
6141
  const cmd = rewriteTemplate(template, isWindows);
5950
6142
  try {
5951
6143
  execSync(cmd, {
5952
- timeout: 5e3,
6144
+ timeout: DEFAULT_TIMEOUT_MS,
5953
6145
  stdio: ["pipe", "pipe", "pipe"],
5954
6146
  encoding: "utf-8",
5955
6147
  env: {
@@ -5967,6 +6159,7 @@ function runHook(template, vars) {
5967
6159
  }
5968
6160
 
5969
6161
  // src/tools/permissions.ts
6162
+ import { isAbsolute, resolve as resolve3 } from "path";
5970
6163
  function checkPermission(toolName, args, dangerLevel, rules, defaultAction = "confirm") {
5971
6164
  for (const rule of rules) {
5972
6165
  if (rule.tool !== "*" && rule.tool !== toolName) continue;
@@ -5987,6 +6180,225 @@ function checkPermission(toolName, args, dangerLevel, rules, defaultAction = "co
5987
6180
  }
5988
6181
  return defaultAction;
5989
6182
  }
6183
+ var BUILTIN_PROFILE_NAMES = /* @__PURE__ */ new Set(["legacy", "read-only", "workspace-write", "danger-full-access"]);
6184
+ var READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
6185
+ "read_file",
6186
+ "list_dir",
6187
+ "grep_files",
6188
+ "glob_files",
6189
+ "find_symbol",
6190
+ "get_outline",
6191
+ "find_references",
6192
+ "search_code",
6193
+ "git_status",
6194
+ "git_diff",
6195
+ "git_log",
6196
+ "task_list",
6197
+ "web_fetch",
6198
+ "web_search",
6199
+ "google_search",
6200
+ "ask_user",
6201
+ "write_todos",
6202
+ "recall_memory"
6203
+ ]);
6204
+ function normalizePathForPermission(value) {
6205
+ return value.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
6206
+ }
6207
+ function isInsidePath(root, target) {
6208
+ const absoluteTarget = isAbsolute(target) ? target : resolve3(root, target);
6209
+ const r = normalizePathForPermission(resolve3(root));
6210
+ const t = normalizePathForPermission(absoluteTarget);
6211
+ return t === r || t.startsWith(`${r}/`);
6212
+ }
6213
+ function extractPathArg(toolName, args) {
6214
+ const raw = args["path"] ?? args["filePath"] ?? args["notebook_path"];
6215
+ if (typeof raw === "string" && raw.trim()) return raw.trim();
6216
+ if (toolName === "save_last_response") {
6217
+ const output = args["outputPath"] ?? args["file"] ?? args["path"];
6218
+ if (typeof output === "string" && output.trim()) return output.trim();
6219
+ }
6220
+ return null;
6221
+ }
6222
+ function isExplicitFileWriteTool(toolName) {
6223
+ return toolName === "write_file" || toolName === "edit_file" || toolName === "notebook_edit" || toolName === "save_last_response";
6224
+ }
6225
+ function resolveProfileName(ctx) {
6226
+ const requested = ctx?.profileName ?? "legacy";
6227
+ if (ctx?.allowedProfiles && ctx.allowedProfiles.length > 0 && !ctx.allowedProfiles.includes(requested)) {
6228
+ return "legacy";
6229
+ }
6230
+ if (BUILTIN_PROFILE_NAMES.has(requested)) return requested;
6231
+ const ext = ctx?.profile?.extends;
6232
+ return ext && BUILTIN_PROFILE_NAMES.has(ext) ? ext : "legacy";
6233
+ }
6234
+ function getProfileRules(ctx) {
6235
+ return ctx?.profile?.rules ?? [];
6236
+ }
6237
+ function getProfileDefaultAction(name, ctx) {
6238
+ if (ctx?.profile?.defaultAction) return ctx.profile.defaultAction;
6239
+ if (name === "danger-full-access") return "auto-approve";
6240
+ return "confirm";
6241
+ }
6242
+ function getWorkspaceRoots(ctx) {
6243
+ const roots = [ctx?.workspaceRoot, ...ctx?.profile?.workspaceRoots ?? []].filter((v) => typeof v === "string" && v.trim().length > 0);
6244
+ return roots;
6245
+ }
6246
+ function getAllowedWriteRoots(ctx) {
6247
+ const roots = [...getWorkspaceRoots(ctx)];
6248
+ const allowTemp = ctx?.profile?.allowTemp !== false;
6249
+ if (allowTemp) roots.push(...ctx?.tempDirs ?? []);
6250
+ return roots.filter((v) => v.trim().length > 0);
6251
+ }
6252
+ function profileHardDecision(profileName, toolName, args, dangerLevel, ctx) {
6253
+ if (profileName === "legacy") return null;
6254
+ if (profileName === "read-only") {
6255
+ const mcpReadOnly = toolName.startsWith("mcp__") && dangerLevel === "safe";
6256
+ if (dangerLevel !== "safe" || !READ_ONLY_TOOLS.has(toolName) && !mcpReadOnly) {
6257
+ return { action: "deny", reason: "blocked by read-only permission profile", profileName };
6258
+ }
6259
+ return null;
6260
+ }
6261
+ if (profileName === "workspace-write") {
6262
+ if (dangerLevel === "destructive") return { action: "confirm", reason: "destructive tools always require confirmation", profileName };
6263
+ if (dangerLevel === "write" && isExplicitFileWriteTool(toolName)) {
6264
+ const target = extractPathArg(toolName, args);
6265
+ if (!target) return { action: "confirm", reason: "file write target is not explicit", profileName };
6266
+ const allowedRoots = getAllowedWriteRoots(ctx);
6267
+ if (allowedRoots.length === 0 || !allowedRoots.some((root) => isInsidePath(root, target))) {
6268
+ return { action: "deny", reason: "file write target is outside workspace/temp roots", profileName };
6269
+ }
6270
+ }
6271
+ return null;
6272
+ }
6273
+ if (profileName === "danger-full-access") {
6274
+ if (dangerLevel === "destructive") return { action: "confirm", reason: "destructive tools still require confirmation", profileName };
6275
+ return null;
6276
+ }
6277
+ return null;
6278
+ }
6279
+ function checkPermissionWithProfile(toolName, args, dangerLevel, rules, defaultAction = "confirm", profileContext) {
6280
+ const profileName = resolveProfileName(profileContext);
6281
+ const hard = profileHardDecision(profileName, toolName, args, dangerLevel, profileContext);
6282
+ if (hard?.action === "deny") return hard;
6283
+ const combinedRules = [...getProfileRules(profileContext), ...rules];
6284
+ const profileDefault = profileName === "legacy" ? defaultAction : getProfileDefaultAction(profileName, profileContext);
6285
+ const action = checkPermission(toolName, args, dangerLevel, combinedRules, profileDefault);
6286
+ if (hard?.action === "confirm" && action === "auto-approve") return hard;
6287
+ return { action, profileName };
6288
+ }
6289
+ function formatPermissionProfileWarning(profileName) {
6290
+ if (profileName === "danger-full-access") {
6291
+ return "\u26A0 danger-full-access permission profile is active. Write tools may auto-run; destructive tools still require confirmation.";
6292
+ }
6293
+ if (profileName === "read-only") return "Read-only permission profile is active. Write and destructive tools are blocked.";
6294
+ if (profileName === "workspace-write") return "Workspace-write permission profile is active. Writes outside workspace/temp roots are blocked.";
6295
+ return null;
6296
+ }
6297
+ var NETWORK_TOOL_HOSTS = {
6298
+ web_search: ["cn.bing.com", "www.google.com"],
6299
+ google_search: ["www.googleapis.com"]
6300
+ };
6301
+ var NETWORK_TOOL_PORTS = {
6302
+ web_search: [443],
6303
+ google_search: [443]
6304
+ };
6305
+ function networkActionToPermission(action) {
6306
+ if (action === "allow") return null;
6307
+ return action;
6308
+ }
6309
+ function domainMatches(pattern, host) {
6310
+ const p = pattern.trim().toLowerCase().replace(/^\*\./, "");
6311
+ const h = host.trim().toLowerCase().replace(/:\d+$/, "");
6312
+ return h === p || h.endsWith(`.${p}`);
6313
+ }
6314
+ function isPrivateNetworkHost(host) {
6315
+ const h = host.toLowerCase().replace(/^\[|\]$/g, "");
6316
+ if (h === "" || h === "localhost") return true;
6317
+ if (/^(127|10)\./.test(h)) return true;
6318
+ if (/^192\.168\./.test(h)) return true;
6319
+ if (/^172\.(1[6-9]|2\d|3[0-1])\./.test(h)) return true;
6320
+ if (/^169\.254\./.test(h)) return true;
6321
+ if (h === "::1" || h === "::") return true;
6322
+ if (/^f[cd][0-9a-f]*:/i.test(h) || /^fe[89ab][0-9a-f]*:/i.test(h)) return true;
6323
+ return false;
6324
+ }
6325
+ function extractNetworkHosts(toolName, args) {
6326
+ if (toolName === "web_fetch") {
6327
+ const raw = String(args["url"] ?? "").trim();
6328
+ try {
6329
+ return raw ? [new URL(raw).hostname] : [];
6330
+ } catch {
6331
+ return [];
6332
+ }
6333
+ }
6334
+ return NETWORK_TOOL_HOSTS[toolName] ?? [];
6335
+ }
6336
+ function extractNetworkPorts(toolName, args) {
6337
+ if (toolName === "web_fetch") {
6338
+ const raw = String(args["url"] ?? "").trim();
6339
+ try {
6340
+ if (!raw) return [];
6341
+ const url = new URL(raw);
6342
+ if (url.port) return [Number(url.port)];
6343
+ return [url.protocol === "http:" ? 80 : 443];
6344
+ } catch {
6345
+ return [];
6346
+ }
6347
+ }
6348
+ return NETWORK_TOOL_PORTS[toolName] ?? [];
6349
+ }
6350
+ function isShellNetworkCommand(command) {
6351
+ 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);
6352
+ }
6353
+ function networkToolAction(toolName, policy) {
6354
+ if (toolName === "web_fetch") return policy.tools?.web_fetch ?? policy.defaultAction ?? "confirm";
6355
+ if (toolName === "web_search") return policy.tools?.web_search ?? policy.defaultAction ?? "confirm";
6356
+ if (toolName === "google_search") return policy.tools?.google_search ?? policy.defaultAction ?? "confirm";
6357
+ if (toolName.startsWith("mcp__")) return policy.tools?.mcp ?? policy.defaultAction ?? "confirm";
6358
+ return null;
6359
+ }
6360
+ function checkNetworkPolicy(toolName, args, policy) {
6361
+ if (!policy?.enabled) return null;
6362
+ let action = networkToolAction(toolName, policy);
6363
+ let reason = "network access requires confirmation";
6364
+ if (toolName === "bash") {
6365
+ const command = String(args["command"] ?? "");
6366
+ if (!isShellNetworkCommand(command)) return null;
6367
+ action = policy.tools?.shell ?? policy.defaultAction ?? "confirm";
6368
+ reason = "shell command may access the network";
6369
+ }
6370
+ if (!action) return null;
6371
+ const hosts = extractNetworkHosts(toolName, args);
6372
+ for (const host of hosts) {
6373
+ if (policy.denyDomains?.some((pattern) => domainMatches(pattern, host))) {
6374
+ return { action: "deny", reason: `network host "${host}" is denied by networkPolicy`, profileName: "networkPolicy" };
6375
+ }
6376
+ if (!policy.allowPrivateNetwork && isPrivateNetworkHost(host)) {
6377
+ return { action: "deny", reason: `network host "${host}" is private/internal`, profileName: "networkPolicy" };
6378
+ }
6379
+ }
6380
+ if (hosts.length > 0 && policy.allowDomains && policy.allowDomains.length > 0) {
6381
+ const allAllowed = hosts.every((host) => policy.allowDomains.some((pattern) => domainMatches(pattern, host)));
6382
+ if (!allAllowed) {
6383
+ return { action: "deny", reason: `network host is not in networkPolicy.allowDomains`, profileName: "networkPolicy" };
6384
+ }
6385
+ }
6386
+ const ports = extractNetworkPorts(toolName, args);
6387
+ for (const port of ports) {
6388
+ if (policy.denyPorts?.includes(port)) {
6389
+ return { action: "deny", reason: `network port ${port} is denied by networkPolicy`, profileName: "networkPolicy" };
6390
+ }
6391
+ }
6392
+ if (ports.length > 0 && policy.allowPorts && policy.allowPorts.length > 0) {
6393
+ const allPortsAllowed = ports.every((port) => policy.allowPorts.includes(port));
6394
+ if (!allPortsAllowed) {
6395
+ return { action: "deny", reason: `network port is not in networkPolicy.allowPorts`, profileName: "networkPolicy" };
6396
+ }
6397
+ }
6398
+ const permissionAction = networkActionToPermission(action);
6399
+ if (!permissionAction) return null;
6400
+ return { action: permissionAction, reason, profileName: "networkPolicy" };
6401
+ }
5990
6402
 
5991
6403
  // src/tools/truncate.ts
5992
6404
  var DEFAULT_MAX_TOOL_OUTPUT_CHARS = 12e3;
@@ -6129,8 +6541,8 @@ var theme = new Proxy(DARK_THEME, {
6129
6541
  });
6130
6542
 
6131
6543
  // src/diagnostics/tool-stats.ts
6132
- import { existsSync as existsSync6, readFileSync as readFileSync5, mkdirSync as mkdirSync3 } from "fs";
6133
- 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";
6134
6546
  import { homedir as homedir3 } from "os";
6135
6547
  var STATS_FILE_NAME = "tool-stats.json";
6136
6548
  var STATS_VERSION = 1;
@@ -6140,16 +6552,16 @@ var dirty = false;
6140
6552
  var pendingWrites = 0;
6141
6553
  var configDirOverride;
6142
6554
  function statsFilePath() {
6143
- const base = configDirOverride ?? join3(homedir3(), CONFIG_DIR_NAME);
6144
- return join3(base, STATS_FILE_NAME);
6555
+ const base = configDirOverride ?? join4(homedir3(), CONFIG_DIR_NAME);
6556
+ return join4(base, STATS_FILE_NAME);
6145
6557
  }
6146
6558
  function load() {
6147
6559
  const path3 = statsFilePath();
6148
- if (!existsSync6(path3)) {
6560
+ if (!existsSync7(path3)) {
6149
6561
  return { version: STATS_VERSION, startedAt: (/* @__PURE__ */ new Date()).toISOString(), entries: {} };
6150
6562
  }
6151
6563
  try {
6152
- const raw = JSON.parse(readFileSync5(path3, "utf-8"));
6564
+ const raw = JSON.parse(readFileSync6(path3, "utf-8"));
6153
6565
  if (raw.version !== STATS_VERSION || !raw.entries) {
6154
6566
  return { version: STATS_VERSION, startedAt: (/* @__PURE__ */ new Date()).toISOString(), entries: {} };
6155
6567
  }
@@ -6207,7 +6619,7 @@ function flush() {
6207
6619
  if (!dirty || state === null) return;
6208
6620
  const path3 = statsFilePath();
6209
6621
  try {
6210
- mkdirSync3(dirname2(path3), { recursive: true });
6622
+ mkdirSync4(dirname2(path3), { recursive: true });
6211
6623
  atomicWriteFileSync(path3, JSON.stringify(state, null, 2));
6212
6624
  dirty = false;
6213
6625
  pendingWrites = 0;
@@ -6289,11 +6701,23 @@ var ToolExecutor = class {
6289
6701
  /** 权限规则(可选) */
6290
6702
  permissionRules = [];
6291
6703
  defaultPermission = "confirm";
6704
+ permissionProfileName = "legacy";
6705
+ permissionProfile;
6706
+ allowedPermissionProfiles = [];
6707
+ workspaceRoot = process.cwd();
6708
+ networkPolicy;
6709
+ hookConfigDir = process.cwd();
6292
6710
  /** 注入 hooks 和 permission rules 配置 */
6293
6711
  setConfig(opts) {
6294
6712
  this.hookConfig = opts.hookConfig;
6295
6713
  if (opts.permissionRules) this.permissionRules = opts.permissionRules;
6296
6714
  if (opts.defaultPermission) this.defaultPermission = opts.defaultPermission;
6715
+ if (opts.permissionProfileName) this.permissionProfileName = opts.permissionProfileName;
6716
+ if (opts.permissionProfile) this.permissionProfile = opts.permissionProfile;
6717
+ if (opts.allowedPermissionProfiles) this.allowedPermissionProfiles = opts.allowedPermissionProfiles;
6718
+ if (opts.workspaceRoot) this.workspaceRoot = opts.workspaceRoot;
6719
+ if (opts.networkPolicy) this.networkPolicy = opts.networkPolicy;
6720
+ if (opts.hookConfigDir) this.hookConfigDir = opts.hookConfigDir;
6297
6721
  }
6298
6722
  async execute(call) {
6299
6723
  return runWithSessionKey(this.sessionKey, () => this.executeInner(call));
@@ -6308,31 +6732,97 @@ var ToolExecutor = class {
6308
6732
  };
6309
6733
  }
6310
6734
  const dangerLevel = getDangerLevel(call.name, call.arguments);
6735
+ let toolCallAlreadyPrinted = false;
6311
6736
  runHook(this.hookConfig?.preToolExecution, {
6312
6737
  tool: call.name,
6313
6738
  dangerLevel,
6314
6739
  args: JSON.stringify(call.arguments).slice(0, 200)
6315
6740
  });
6316
- if (this.permissionRules.length > 0) {
6317
- const action = checkPermission(call.name, call.arguments, dangerLevel, this.permissionRules, this.defaultPermission);
6318
- if (action === "deny") {
6319
- return { callId: call.id, content: `[Permission denied] Tool ${call.name} is blocked by permission rules. Do not retry.`, isError: true };
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
+ }
6759
+ const permission = checkPermissionWithProfile(
6760
+ call.name,
6761
+ call.arguments,
6762
+ dangerLevel,
6763
+ this.permissionRules,
6764
+ this.defaultPermission,
6765
+ {
6766
+ profileName: this.permissionProfileName,
6767
+ profile: this.permissionProfile,
6768
+ allowedProfiles: this.allowedPermissionProfiles,
6769
+ workspaceRoot: this.workspaceRoot,
6770
+ tempDirs: [tmpdir()]
6320
6771
  }
6321
- if (action === "auto-approve") {
6322
- this.printToolCall(call);
6323
- try {
6324
- const rawContent = await runTool(tool, call.arguments, call.name);
6325
- const content = truncateOutput(rawContent, call.name);
6326
- const wasTruncated = content !== rawContent;
6327
- this.printToolResult(call.name, rawContent, false, wasTruncated);
6328
- runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
6329
- return { callId: call.id, content, isError: false };
6330
- } catch (err) {
6331
- const message = err instanceof Error ? err.message : String(err);
6332
- this.printToolResult(call.name, message, true, false);
6333
- runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
6334
- return { callId: call.id, content: message, isError: true };
6335
- }
6772
+ );
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
+ }
6782
+ if (networkPermission?.action === "deny") {
6783
+ const reason = networkPermission.reason ? ` (${networkPermission.reason})` : "";
6784
+ return {
6785
+ callId: call.id,
6786
+ content: `[Permission denied] Tool ${call.name} is blocked by networkPolicy${reason}. Do not retry.`,
6787
+ isError: true
6788
+ };
6789
+ }
6790
+ if (permission.action === "deny") {
6791
+ const reason = permission.reason ? ` (${permission.reason})` : "";
6792
+ return {
6793
+ callId: call.id,
6794
+ content: `[Permission denied] Tool ${call.name} is blocked by permission profile "${permission.profileName}"${reason}. Do not retry.`,
6795
+ isError: true
6796
+ };
6797
+ }
6798
+ if (permission.action === "auto-approve" && networkPermission?.action !== "confirm") {
6799
+ this.printToolCall(call);
6800
+ try {
6801
+ const rawContent = await runTool(tool, call.arguments, call.name);
6802
+ const content = truncateOutput(rawContent, call.name);
6803
+ const wasTruncated = content !== rawContent;
6804
+ this.printToolResult(call.name, rawContent, false, wasTruncated);
6805
+ runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
6806
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
6807
+ return { callId: call.id, content, isError: false };
6808
+ } catch (err) {
6809
+ const message = err instanceof Error ? err.message : String(err);
6810
+ this.printToolResult(call.name, message, true, false);
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 });
6813
+ return { callId: call.id, content: message, isError: true };
6814
+ }
6815
+ }
6816
+ if (networkPermission?.action === "confirm" && dangerLevel === "safe") {
6817
+ this.printToolCall(call);
6818
+ toolCallAlreadyPrinted = true;
6819
+ const confirmed = await this.confirm(call, "write");
6820
+ if (!confirmed) {
6821
+ return {
6822
+ callId: call.id,
6823
+ content: `[User cancelled] The user declined network access for ${call.name}. Do not retry without asking.`,
6824
+ isError: true
6825
+ };
6336
6826
  }
6337
6827
  }
6338
6828
  if (this.sessionAutoApprove && dangerLevel === "write") {
@@ -6345,11 +6835,13 @@ var ToolExecutor = class {
6345
6835
  const wasTruncated = content !== rawContent;
6346
6836
  this.printToolResult(call.name, rawContent, false, wasTruncated);
6347
6837
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
6838
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
6348
6839
  return { callId: call.id, content, isError: false };
6349
6840
  } catch (err) {
6350
6841
  const message = err instanceof Error ? err.message : String(err);
6351
6842
  this.printToolResult(call.name, message, true, false);
6352
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 });
6353
6845
  return { callId: call.id, content: message, isError: true };
6354
6846
  }
6355
6847
  }
@@ -6383,11 +6875,13 @@ var ToolExecutor = class {
6383
6875
  const wasTruncated = content !== rawContent;
6384
6876
  this.printToolResult(call.name, rawContent, false, wasTruncated);
6385
6877
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
6878
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
6386
6879
  return { callId: call.id, content, isError: false };
6387
6880
  } catch (err) {
6388
6881
  const message = err instanceof Error ? err.message : String(err);
6389
6882
  this.printToolResult(call.name, message, true, false);
6390
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 });
6391
6885
  return { callId: call.id, content: message, isError: true };
6392
6886
  }
6393
6887
  }
@@ -6422,6 +6916,9 @@ var ToolExecutor = class {
6422
6916
  * 然后让用户 approve all / reject all / 选择性 approve。
6423
6917
  */
6424
6918
  async executeBatchFileWrites(calls) {
6919
+ if (this.permissionProfileName !== "legacy" || this.permissionRules.length > 0) {
6920
+ return Promise.all(calls.map((call) => this.execute(call)));
6921
+ }
6425
6922
  console.log();
6426
6923
  console.log(theme.heading(`\u270E Batch file writes (${calls.length} files):`));
6427
6924
  console.log(theme.dim("\u2500".repeat(50)));
@@ -6495,7 +6992,7 @@ var ToolExecutor = class {
6495
6992
  rl.resume();
6496
6993
  process.stdout.write(prompt);
6497
6994
  this.confirming = true;
6498
- return new Promise((resolve8) => {
6995
+ return new Promise((resolve9) => {
6499
6996
  let completed = false;
6500
6997
  const cleanup = (result) => {
6501
6998
  if (completed) return;
@@ -6505,7 +7002,7 @@ var ToolExecutor = class {
6505
7002
  rl.pause();
6506
7003
  rlAny.output = savedOutput;
6507
7004
  this.confirming = false;
6508
- resolve8(result);
7005
+ resolve9(result);
6509
7006
  };
6510
7007
  const onLine = (line) => {
6511
7008
  const trimmed = line.trim();
@@ -6572,10 +7069,10 @@ var ToolExecutor = class {
6572
7069
  const filePath = String(call.arguments["path"] ?? "");
6573
7070
  const newContent = String(call.arguments["content"] ?? "");
6574
7071
  if (!filePath) return;
6575
- if (existsSync7(filePath)) {
7072
+ if (existsSync8(filePath)) {
6576
7073
  let oldContent;
6577
7074
  try {
6578
- oldContent = readFileSync6(filePath, "utf-8");
7075
+ oldContent = readFileSync7(filePath, "utf-8");
6579
7076
  } catch {
6580
7077
  return;
6581
7078
  }
@@ -6598,7 +7095,7 @@ var ToolExecutor = class {
6598
7095
  }
6599
7096
  } else if (call.name === "edit_file") {
6600
7097
  const filePath = String(call.arguments["path"] ?? "");
6601
- if (!filePath || !existsSync7(filePath)) return;
7098
+ if (!filePath || !existsSync8(filePath)) return;
6602
7099
  const oldStr = call.arguments["old_str"];
6603
7100
  const newStr = call.arguments["new_str"];
6604
7101
  if (oldStr !== void 0) {
@@ -6625,7 +7122,7 @@ var ToolExecutor = class {
6625
7122
  const to = Number(call.arguments["delete_to_line"] ?? from);
6626
7123
  let fileContent;
6627
7124
  try {
6628
- fileContent = readFileSync6(filePath, "utf-8");
7125
+ fileContent = readFileSync7(filePath, "utf-8");
6629
7126
  } catch {
6630
7127
  return;
6631
7128
  }
@@ -6679,7 +7176,7 @@ var ToolExecutor = class {
6679
7176
  rl.resume();
6680
7177
  process.stdout.write(color("Proceed? [y/N] (type y + Enter to confirm) "));
6681
7178
  this.confirming = true;
6682
- return new Promise((resolve8) => {
7179
+ return new Promise((resolve9) => {
6683
7180
  let completed = false;
6684
7181
  const cleanup = (answer) => {
6685
7182
  if (completed) return;
@@ -6689,7 +7186,7 @@ var ToolExecutor = class {
6689
7186
  rl.pause();
6690
7187
  rlAny.output = savedOutput;
6691
7188
  this.confirming = false;
6692
- resolve8(answer === "y");
7189
+ resolve9(answer === "y");
6693
7190
  };
6694
7191
  const onLine = (line) => {
6695
7192
  const trimmed = line.trim();
@@ -6717,11 +7214,11 @@ var ToolExecutor = class {
6717
7214
  };
6718
7215
 
6719
7216
  // src/tools/sensitive-paths.ts
6720
- import { resolve as resolve3, sep as sep2, basename as basename2 } from "path";
7217
+ import { resolve as resolve4, sep as sep2, basename as basename2 } from "path";
6721
7218
  import { homedir as homedir4 } from "os";
6722
7219
  var home = homedir4();
6723
7220
  function norm(p) {
6724
- const abs = resolve3(p);
7221
+ const abs = resolve4(p);
6725
7222
  return process.platform === "win32" ? abs.toLowerCase() : abs;
6726
7223
  }
6727
7224
  function homeRel(p) {
@@ -6860,7 +7357,7 @@ Do NOT split a long document into many write_file(append=true) calls. That patte
6860
7357
  }
6861
7358
  undoStack.push(filePath, `write_file${appendMode ? " (append)" : ""}: ${filePath}`);
6862
7359
  fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
6863
- mkdirSync4(dirname3(filePath), { recursive: true });
7360
+ mkdirSync5(dirname3(filePath), { recursive: true });
6864
7361
  if (appendMode) {
6865
7362
  appendFileSync(filePath, content, encoding);
6866
7363
  } else {
@@ -6880,7 +7377,7 @@ Do NOT split a long document into many write_file(append=true) calls. That patte
6880
7377
  };
6881
7378
 
6882
7379
  // src/tools/builtin/edit-file.ts
6883
- import { readFileSync as readFileSync7, existsSync as existsSync8 } from "fs";
7380
+ import { readFileSync as readFileSync8, existsSync as existsSync9 } from "fs";
6884
7381
 
6885
7382
  // src/tools/builtin/patch-apply.ts
6886
7383
  function parseUnifiedDiff(patch) {
@@ -7241,7 +7738,7 @@ Note: Path can be absolute or relative to cwd.`,
7241
7738
  const filePath = String(args["path"] ?? "");
7242
7739
  const encoding = args["encoding"] ?? "utf-8";
7243
7740
  if (!filePath) throw new ToolError("edit_file", "path is required");
7244
- 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}`);
7245
7742
  const verdict = classifyWritePath(filePath);
7246
7743
  if (verdict.sensitive && subAgentGuard.active) {
7247
7744
  throw new ToolError(
@@ -7249,7 +7746,7 @@ Note: Path can be absolute or relative to cwd.`,
7249
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.`
7250
7747
  );
7251
7748
  }
7252
- const original = readFileSync7(filePath, encoding);
7749
+ const original = readFileSync8(filePath, encoding);
7253
7750
  if (args["patch"] !== void 0) {
7254
7751
  const patchText = String(args["patch"] ?? "");
7255
7752
  const stopOnError = args["stop_on_error"] !== false;
@@ -7407,8 +7904,8 @@ function truncatePreview(str, maxLen = 80) {
7407
7904
  }
7408
7905
 
7409
7906
  // src/tools/builtin/list-dir.ts
7410
- import { readdirSync as readdirSync4, statSync as statSync3, existsSync as existsSync9 } from "fs";
7411
- 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";
7412
7909
  var listDirTool = {
7413
7910
  definition: {
7414
7911
  name: "list_dir",
@@ -7430,7 +7927,7 @@ var listDirTool = {
7430
7927
  async execute(args) {
7431
7928
  const dirPath = String(args["path"] ?? process.cwd());
7432
7929
  const recursive = Boolean(args["recursive"] ?? false);
7433
- if (!existsSync9(dirPath)) {
7930
+ if (!existsSync10(dirPath)) {
7434
7931
  const targetName = basename3(dirPath).toLowerCase();
7435
7932
  const cwd = process.cwd();
7436
7933
  const suggestions = [];
@@ -7488,11 +7985,11 @@ function listRecursive(basePath, indent, recursive, lines) {
7488
7985
  if (entry.isDirectory()) {
7489
7986
  lines.push(`${indent}\u{1F4C1} ${entry.name}/`);
7490
7987
  if (recursive) {
7491
- listRecursive(join4(basePath, entry.name), indent + " ", true, lines);
7988
+ listRecursive(join5(basePath, entry.name), indent + " ", true, lines);
7492
7989
  }
7493
7990
  } else {
7494
7991
  try {
7495
- const stat = statSync3(join4(basePath, entry.name));
7992
+ const stat = statSync3(join5(basePath, entry.name));
7496
7993
  const size = formatSize(stat.size);
7497
7994
  lines.push(`${indent}\u{1F4C4} ${entry.name} (${size})`);
7498
7995
  } catch {
@@ -7508,9 +8005,9 @@ function formatSize(bytes) {
7508
8005
  }
7509
8006
 
7510
8007
  // src/tools/builtin/grep-files.ts
7511
- 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";
7512
8009
  import { readFile } from "fs/promises";
7513
- import { join as join5, relative } from "path";
8010
+ import { join as join6, relative } from "path";
7514
8011
  var grepFilesTool = {
7515
8012
  definition: {
7516
8013
  name: "grep_files",
@@ -7561,7 +8058,7 @@ Supports regex. Automatically skips node_modules, dist, .git directories.`,
7561
8058
  const contextLines = Math.max(0, Number(args["context_lines"] ?? 0));
7562
8059
  const maxResults = Math.max(1, Number(args["max_results"] ?? 50));
7563
8060
  if (!pattern) throw new ToolError("grep_files", "pattern is required");
7564
- 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}`);
7565
8062
  const MAX_PATTERN_LENGTH = 1e3;
7566
8063
  if (pattern.length > MAX_PATTERN_LENGTH) {
7567
8064
  throw new ToolError("grep_files", `Pattern too long (${pattern.length} chars, max ${MAX_PATTERN_LENGTH}). Use a shorter pattern.`);
@@ -7655,11 +8152,11 @@ function collectFilePaths(rootPath, filePattern, maxFiles) {
7655
8152
  if (paths.length >= maxFiles) return;
7656
8153
  if (entry.isDirectory()) {
7657
8154
  if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
7658
- walk(join5(dirPath, entry.name));
8155
+ walk(join6(dirPath, entry.name));
7659
8156
  } else if (entry.isFile()) {
7660
8157
  if (isBinary(entry.name)) continue;
7661
8158
  if (filePattern && !matchesFilePattern(entry.name, filePattern)) continue;
7662
- const fullPath = join5(dirPath, entry.name);
8159
+ const fullPath = join6(dirPath, entry.name);
7663
8160
  try {
7664
8161
  if (statSync4(fullPath).size > 1e6) continue;
7665
8162
  } catch {
@@ -7714,7 +8211,7 @@ function searchInFile(fullPath, displayPath2, regex, contextLines, maxResults, r
7714
8211
  }
7715
8212
  let content;
7716
8213
  try {
7717
- content = readFileSync8(fullPath, "utf-8");
8214
+ content = readFileSync9(fullPath, "utf-8");
7718
8215
  } catch {
7719
8216
  return;
7720
8217
  }
@@ -7745,8 +8242,8 @@ function searchInFile(fullPath, displayPath2, regex, contextLines, maxResults, r
7745
8242
  }
7746
8243
 
7747
8244
  // src/tools/builtin/glob-files.ts
7748
- import { readdirSync as readdirSync6, statSync as statSync5, existsSync as existsSync11 } from "fs";
7749
- 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";
7750
8247
  var globFilesTool = {
7751
8248
  definition: {
7752
8249
  name: "glob_files",
@@ -7779,7 +8276,7 @@ Results sorted by most recent modification time. Automatically skips node_module
7779
8276
  const rootPath = String(args["path"] ?? process.cwd());
7780
8277
  const maxResults = Math.max(1, Number(args["max_results"] ?? 100));
7781
8278
  if (!pattern) throw new ToolError("glob_files", "pattern is required");
7782
- 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}`);
7783
8280
  const regex = globToRegex(pattern);
7784
8281
  const matches = [];
7785
8282
  collectMatchingFiles(rootPath, rootPath, regex, matches, maxResults);
@@ -7856,7 +8353,7 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
7856
8353
  }
7857
8354
  for (const entry of entries) {
7858
8355
  if (results.length >= maxResults) break;
7859
- const fullPath = join6(dirPath, entry.name);
8356
+ const fullPath = join7(dirPath, entry.name);
7860
8357
  if (entry.isDirectory()) {
7861
8358
  if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".")) continue;
7862
8359
  collectMatchingFiles(fullPath, rootPath, regex, results, maxResults);
@@ -7938,7 +8435,7 @@ var runInteractiveTool = {
7938
8435
  PYTHONDONTWRITEBYTECODE: "1"
7939
8436
  };
7940
8437
  const prefixWarnings = [argsTypeWarning, stdinTypeWarning].filter(Boolean).join("");
7941
- return new Promise((resolve8) => {
8438
+ return new Promise((resolve9) => {
7942
8439
  const child = spawn2(executable, cmdArgs.map(String), {
7943
8440
  cwd: process.cwd(),
7944
8441
  env,
@@ -7971,22 +8468,22 @@ var runInteractiveTool = {
7971
8468
  setTimeout(writeNextLine, 400);
7972
8469
  const timer = setTimeout(() => {
7973
8470
  child.kill();
7974
- resolve8(`${prefixWarnings}[Timeout after ${timeout}ms]
8471
+ resolve9(`${prefixWarnings}[Timeout after ${timeout}ms]
7975
8472
  ${buildOutput(stdout, stderr)}`);
7976
8473
  }, timeout);
7977
8474
  child.on("close", (code) => {
7978
8475
  clearTimeout(timer);
7979
8476
  const output = buildOutput(stdout, stderr);
7980
8477
  if (code !== 0 && code !== null) {
7981
- resolve8(`${prefixWarnings}Exit code ${code}:
8478
+ resolve9(`${prefixWarnings}Exit code ${code}:
7982
8479
  ${output}`);
7983
8480
  } else {
7984
- resolve8(`${prefixWarnings}${output || "(no output)"}`);
8481
+ resolve9(`${prefixWarnings}${output || "(no output)"}`);
7985
8482
  }
7986
8483
  });
7987
8484
  child.on("error", (err) => {
7988
8485
  clearTimeout(timer);
7989
- resolve8(
8486
+ resolve9(
7990
8487
  `${prefixWarnings}Failed to start process "${executable}": ${err.message}
7991
8488
  Hint: On Windows, use the full path to the executable, e.g.:
7992
8489
  C:\\Users\\Jinzd\\anaconda3\\envs\\python312\\python.exe`
@@ -8596,7 +9093,7 @@ ${preamble}`;
8596
9093
  }
8597
9094
 
8598
9095
  // src/tools/builtin/save-last-response.ts
8599
- 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";
8600
9097
  import { dirname as dirname4 } from "path";
8601
9098
  var lastResponseStore = { content: "" };
8602
9099
  function cleanupRejectedTeeFile(filePath) {
@@ -8646,7 +9143,7 @@ Any of these triggers means use save_last_response, NOT write_file:
8646
9143
  throw new ToolError("save_last_response", "No content to save: AI has not produced any response yet, or the last response was empty.");
8647
9144
  }
8648
9145
  undoStack.push(filePath, `save_last_response: ${filePath}`);
8649
- mkdirSync5(dirname4(filePath), { recursive: true });
9146
+ mkdirSync6(dirname4(filePath), { recursive: true });
8650
9147
  atomicWriteFileSync(filePath, content);
8651
9148
  const lines = content.split("\n").length;
8652
9149
  return `File saved: ${filePath} (${lines} lines, ${content.length} bytes)`;
@@ -8654,11 +9151,11 @@ Any of these triggers means use save_last_response, NOT write_file:
8654
9151
  };
8655
9152
 
8656
9153
  // src/tools/builtin/save-memory.ts
8657
- import { existsSync as existsSync12, readFileSync as readFileSync9, statSync as statSync6, mkdirSync as mkdirSync6 } from "fs";
8658
- 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";
8659
9156
  import { homedir as homedir5 } from "os";
8660
9157
  function getMemoryFilePath() {
8661
- return join7(homedir5(), CONFIG_DIR_NAME, MEMORY_FILE_NAME);
9158
+ return join8(homedir5(), CONFIG_DIR_NAME, MEMORY_FILE_NAME);
8662
9159
  }
8663
9160
  function formatTimestamp() {
8664
9161
  const now = /* @__PURE__ */ new Date();
@@ -8682,16 +9179,16 @@ var saveMemoryTool = {
8682
9179
  const content = String(args["content"] ?? "").trim();
8683
9180
  if (!content) throw new ToolError("save_memory", "content is required");
8684
9181
  const memoryPath = getMemoryFilePath();
8685
- const configDir = join7(homedir5(), CONFIG_DIR_NAME);
8686
- if (!existsSync12(configDir)) {
8687
- mkdirSync6(configDir, { recursive: true });
9182
+ const configDir = join8(homedir5(), CONFIG_DIR_NAME);
9183
+ if (!existsSync13(configDir)) {
9184
+ mkdirSync7(configDir, { recursive: true });
8688
9185
  }
8689
9186
  const timestamp = formatTimestamp();
8690
9187
  const entry = `
8691
9188
  ## ${timestamp}
8692
9189
  ${content}
8693
9190
  `;
8694
- const previous = existsSync12(memoryPath) ? readFileSync9(memoryPath, "utf-8") : "";
9191
+ const previous = existsSync13(memoryPath) ? readFileSync10(memoryPath, "utf-8") : "";
8695
9192
  atomicWriteFileSync(memoryPath, previous + entry);
8696
9193
  const byteSize = statSync6(memoryPath).size;
8697
9194
  return `Memory saved successfully. File size: ${byteSize} bytes in ${MEMORY_FILE_NAME}`;
@@ -8738,7 +9235,7 @@ function promptUser(rl, question) {
8738
9235
  console.log();
8739
9236
  console.log(chalk4.cyan("\u2753 ") + chalk4.bold(question));
8740
9237
  process.stdout.write(chalk4.cyan("> "));
8741
- return new Promise((resolve8) => {
9238
+ return new Promise((resolve9) => {
8742
9239
  let completed = false;
8743
9240
  const cleanup = (answer) => {
8744
9241
  if (completed) return;
@@ -8748,7 +9245,7 @@ function promptUser(rl, question) {
8748
9245
  rl.pause();
8749
9246
  rlAny.output = savedOutput;
8750
9247
  askUserContext.prompting = false;
8751
- resolve8(answer);
9248
+ resolve9(answer);
8752
9249
  };
8753
9250
  const onLine = (line) => {
8754
9251
  cleanup(line);
@@ -9228,11 +9725,27 @@ var spawnAgentTool = {
9228
9725
  if (!ctx.provider) {
9229
9726
  throw new ToolError("spawn_agent", "provider not initialized (context not injected)");
9230
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";
9231
9743
  const guardWasActive = subAgentGuard.active;
9232
9744
  subAgentGuard.active = true;
9233
9745
  try {
9234
9746
  if (singleTask) {
9235
9747
  const { content, usage } = await runSubAgent(singleTask, maxRounds, null, ctx);
9748
+ subagentStatus = "ok";
9236
9749
  return [
9237
9750
  "## Sub-Agent Result",
9238
9751
  "",
@@ -9261,8 +9774,10 @@ var spawnAgentTool = {
9261
9774
  });
9262
9775
  lines.push("---");
9263
9776
  lines.push(`Total token usage: ${totalIn} input, ${totalOut} output`);
9777
+ subagentStatus = "ok";
9264
9778
  return lines.join("\n");
9265
9779
  } finally {
9780
+ runLifecycleHooks(hookConfig, "SubagentStop", { ...hookPayload, status: subagentStatus }, hookConfigDir);
9266
9781
  subAgentGuard.active = guardWasActive;
9267
9782
  }
9268
9783
  }
@@ -9485,14 +10000,14 @@ var taskStopTool = {
9485
10000
 
9486
10001
  // src/tools/builtin/git-tools.ts
9487
10002
  import { execFileSync as execFileSync2 } from "child_process";
9488
- import { existsSync as existsSync13 } from "fs";
9489
- import { join as join8 } from "path";
10003
+ import { existsSync as existsSync14 } from "fs";
10004
+ import { join as join9 } from "path";
9490
10005
  function assertGitRepo(cwd) {
9491
10006
  let dir = cwd;
9492
10007
  const root = dir.split(/[\\/]/)[0] + (dir.includes("\\") ? "\\" : "/");
9493
10008
  while (dir && dir !== root) {
9494
- if (existsSync13(join8(dir, ".git"))) return;
9495
- const parent = join8(dir, "..");
10009
+ if (existsSync14(join9(dir, ".git"))) return;
10010
+ const parent = join9(dir, "..");
9496
10011
  if (parent === dir) break;
9497
10012
  dir = parent;
9498
10013
  }
@@ -9757,9 +10272,9 @@ ${commitOutput.trim()}`;
9757
10272
  };
9758
10273
 
9759
10274
  // src/tools/builtin/notebook-edit.ts
9760
- import { readFileSync as readFileSync10, existsSync as existsSync14 } from "fs";
10275
+ import { readFileSync as readFileSync11, existsSync as existsSync15 } from "fs";
9761
10276
  import { writeFile } from "fs/promises";
9762
- import { resolve as resolve4, extname as extname2 } from "path";
10277
+ import { resolve as resolve5, extname as extname2 } from "path";
9763
10278
  var notebookEditTool = {
9764
10279
  definition: {
9765
10280
  name: "notebook_edit",
@@ -9808,14 +10323,14 @@ var notebookEditTool = {
9808
10323
  if (!Number.isInteger(cellIndexRaw) || cellIndexRaw < 1) {
9809
10324
  throw new ToolError("notebook_edit", "cell_index must be a positive integer (1-based)");
9810
10325
  }
9811
- const absPath = resolve4(filePath);
10326
+ const absPath = resolve5(filePath);
9812
10327
  if (extname2(absPath).toLowerCase() !== ".ipynb") {
9813
10328
  throw new ToolError("notebook_edit", "path must point to a .ipynb file");
9814
10329
  }
9815
- if (!existsSync14(absPath)) {
10330
+ if (!existsSync15(absPath)) {
9816
10331
  throw new ToolError("notebook_edit", `Notebook not found: ${filePath}`);
9817
10332
  }
9818
- const raw = readFileSync10(absPath, "utf-8");
10333
+ const raw = readFileSync11(absPath, "utf-8");
9819
10334
  const nb = parseNotebook(raw);
9820
10335
  const cellIdx0 = cellIndexRaw - 1;
9821
10336
  undoStack.push(absPath, `notebook_edit (${action}): ${filePath}`);
@@ -10232,8 +10747,8 @@ function estimateToolDefinitionTokens(def) {
10232
10747
 
10233
10748
  // src/tools/registry.ts
10234
10749
  import { pathToFileURL } from "url";
10235
- import { existsSync as existsSync15, mkdirSync as mkdirSync7, readdirSync as readdirSync7 } from "fs";
10236
- 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";
10237
10752
  var ToolRegistry = class {
10238
10753
  tools = /* @__PURE__ */ new Map();
10239
10754
  pluginToolNames = /* @__PURE__ */ new Set();
@@ -10394,9 +10909,9 @@ var ToolRegistry = class {
10394
10909
  * Returns the number of successfully loaded plugins.
10395
10910
  */
10396
10911
  async loadPlugins(pluginsDir, allowPlugins = false) {
10397
- if (!existsSync15(pluginsDir)) {
10912
+ if (!existsSync16(pluginsDir)) {
10398
10913
  try {
10399
- mkdirSync7(pluginsDir, { recursive: true });
10914
+ mkdirSync8(pluginsDir, { recursive: true });
10400
10915
  } catch {
10401
10916
  }
10402
10917
  return 0;
@@ -10420,12 +10935,12 @@ var ToolRegistry = class {
10420
10935
  process.stderr.write(
10421
10936
  `
10422
10937
  [plugins] \u26A0 Loading ${files.length} plugin(s) with FULL system privileges:
10423
- ` + files.map((f) => ` + ${join9(pluginsDir, f)}`).join("\n") + "\n\n"
10938
+ ` + files.map((f) => ` + ${join10(pluginsDir, f)}`).join("\n") + "\n\n"
10424
10939
  );
10425
10940
  let loaded = 0;
10426
10941
  for (const file of files) {
10427
10942
  try {
10428
- const fileUrl = pathToFileURL(join9(pluginsDir, file)).href;
10943
+ const fileUrl = pathToFileURL(join10(pluginsDir, file)).href;
10429
10944
  const mod = await import(fileUrl);
10430
10945
  const tool = mod.tool ?? mod.default?.tool ?? mod.default;
10431
10946
  if (!tool || typeof tool.execute !== "function" || !tool.definition?.name) {
@@ -10587,7 +11102,7 @@ var McpClient = class {
10587
11102
  // 内部方法:JSON-RPC 通信
10588
11103
  // ══════════════════════════════════════════════════════════════════
10589
11104
  sendRequest(method, params) {
10590
- return new Promise((resolve8, reject) => {
11105
+ return new Promise((resolve9, reject) => {
10591
11106
  if (!this.process?.stdin?.writable) {
10592
11107
  return reject(new Error(`MCP server [${this.serverId}] stdin not writable`));
10593
11108
  }
@@ -10611,7 +11126,7 @@ var McpClient = class {
10611
11126
  this.pendingRequests.set(id, {
10612
11127
  resolve: (result) => {
10613
11128
  cleanup();
10614
- resolve8(result);
11129
+ resolve9(result);
10615
11130
  },
10616
11131
  reject: (error) => {
10617
11132
  cleanup();
@@ -10688,13 +11203,13 @@ var McpClient = class {
10688
11203
  }
10689
11204
  /** Promise 超时包装 */
10690
11205
  withTimeout(promise, ms, label) {
10691
- return new Promise((resolve8, reject) => {
11206
+ return new Promise((resolve9, reject) => {
10692
11207
  const timer = setTimeout(() => {
10693
11208
  reject(new Error(`MCP [${this.serverId}] ${label} timed out after ${ms}ms`));
10694
11209
  }, ms);
10695
11210
  promise.then((val) => {
10696
11211
  clearTimeout(timer);
10697
- resolve8(val);
11212
+ resolve9(val);
10698
11213
  }).catch((err) => {
10699
11214
  clearTimeout(timer);
10700
11215
  reject(err);
@@ -10971,11 +11486,11 @@ var McpManager = class {
10971
11486
  };
10972
11487
 
10973
11488
  // src/skills/manager.ts
10974
- import { existsSync as existsSync16, readdirSync as readdirSync8, mkdirSync as mkdirSync8, statSync as statSync7 } from "fs";
10975
- 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";
10976
11491
 
10977
11492
  // src/skills/types.ts
10978
- import { readFileSync as readFileSync11 } from "fs";
11493
+ import { readFileSync as readFileSync12 } from "fs";
10979
11494
  import { basename as basename5 } from "path";
10980
11495
  function parseSimpleYaml(yaml) {
10981
11496
  const result = {};
@@ -10997,7 +11512,7 @@ function parseYamlArray(value) {
10997
11512
  function parseSkillFile(filePath) {
10998
11513
  let raw;
10999
11514
  try {
11000
- raw = readFileSync11(filePath, "utf-8");
11515
+ raw = readFileSync12(filePath, "utf-8");
11001
11516
  } catch {
11002
11517
  return null;
11003
11518
  }
@@ -11040,9 +11555,9 @@ var SkillManager = class {
11040
11555
  /** 发现并加载 skillsDir 下所有 .md 文件,返回加载数量 */
11041
11556
  loadSkills() {
11042
11557
  this.skills.clear();
11043
- if (!existsSync16(this.skillsDir)) {
11558
+ if (!existsSync17(this.skillsDir)) {
11044
11559
  try {
11045
- mkdirSync8(this.skillsDir, { recursive: true });
11560
+ mkdirSync9(this.skillsDir, { recursive: true });
11046
11561
  } catch {
11047
11562
  }
11048
11563
  return 0;
@@ -11055,14 +11570,14 @@ var SkillManager = class {
11055
11570
  }
11056
11571
  for (const entry of entries) {
11057
11572
  let filePath;
11058
- const fullPath = join10(this.skillsDir, entry);
11573
+ const fullPath = join11(this.skillsDir, entry);
11059
11574
  if (entry.endsWith(".md")) {
11060
11575
  filePath = fullPath;
11061
11576
  } else {
11062
11577
  try {
11063
11578
  if (statSync7(fullPath).isDirectory()) {
11064
- const skillMd = join10(fullPath, "SKILL.md");
11065
- if (existsSync16(skillMd)) {
11579
+ const skillMd = join11(fullPath, "SKILL.md");
11580
+ if (existsSync17(skillMd)) {
11066
11581
  filePath = skillMd;
11067
11582
  } else {
11068
11583
  continue;
@@ -11126,7 +11641,8 @@ var SkillManager = class {
11126
11641
 
11127
11642
  // src/web/tool-executor-web.ts
11128
11643
  import { randomUUID as randomUUID2 } from "crypto";
11129
- import { existsSync as existsSync17, readFileSync as readFileSync12 } from "fs";
11644
+ import { tmpdir as tmpdir2 } from "os";
11645
+ import { existsSync as existsSync18, readFileSync as readFileSync13 } from "fs";
11130
11646
  var ToolExecutorWeb = class _ToolExecutorWeb {
11131
11647
  constructor(registry, ws) {
11132
11648
  this.registry = registry;
@@ -11137,6 +11653,12 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11137
11653
  hookConfig;
11138
11654
  permissionRules = [];
11139
11655
  defaultPermission = "confirm";
11656
+ permissionProfileName = "legacy";
11657
+ permissionProfile;
11658
+ allowedPermissionProfiles = [];
11659
+ workspaceRoot = process.cwd();
11660
+ networkPolicy;
11661
+ hookConfigDir = process.cwd();
11140
11662
  /** Pending confirm promises keyed by requestId */
11141
11663
  pendingConfirms = /* @__PURE__ */ new Map();
11142
11664
  /** Pending batch confirm promises */
@@ -11171,6 +11693,12 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11171
11693
  this.hookConfig = opts.hookConfig;
11172
11694
  if (opts.permissionRules) this.permissionRules = opts.permissionRules;
11173
11695
  if (opts.defaultPermission) this.defaultPermission = opts.defaultPermission;
11696
+ if (opts.permissionProfileName) this.permissionProfileName = opts.permissionProfileName;
11697
+ if (opts.permissionProfile) this.permissionProfile = opts.permissionProfile;
11698
+ if (opts.allowedPermissionProfiles) this.allowedPermissionProfiles = opts.allowedPermissionProfiles;
11699
+ if (opts.workspaceRoot) this.workspaceRoot = opts.workspaceRoot;
11700
+ if (opts.networkPolicy) this.networkPolicy = opts.networkPolicy;
11701
+ if (opts.hookConfigDir) this.hookConfigDir = opts.hookConfigDir;
11174
11702
  }
11175
11703
  /** Clear M7 timeout timer for a requestId */
11176
11704
  clearPendingTimer(requestId) {
@@ -11182,33 +11710,33 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11182
11710
  }
11183
11711
  /** Resolve a pending confirm from client response */
11184
11712
  resolveConfirm(requestId, approved) {
11185
- const resolve8 = this.pendingConfirms.get(requestId);
11186
- if (resolve8) {
11713
+ const resolve9 = this.pendingConfirms.get(requestId);
11714
+ if (resolve9) {
11187
11715
  this.clearPendingTimer(requestId);
11188
11716
  this.pendingConfirms.delete(requestId);
11189
11717
  this.confirming = false;
11190
- resolve8(approved);
11718
+ resolve9(approved);
11191
11719
  }
11192
11720
  }
11193
11721
  /** Resolve a pending batch confirm from client response */
11194
11722
  resolveBatchConfirm(requestId, decision) {
11195
- const resolve8 = this.pendingBatchConfirms.get(requestId);
11196
- if (resolve8) {
11723
+ const resolve9 = this.pendingBatchConfirms.get(requestId);
11724
+ if (resolve9) {
11197
11725
  this.clearPendingTimer(requestId);
11198
11726
  this.pendingBatchConfirms.delete(requestId);
11199
11727
  this.confirming = false;
11200
11728
  if (decision === "all" || decision === "none") {
11201
- resolve8(decision);
11729
+ resolve9(decision);
11202
11730
  } else {
11203
- resolve8(new Set(decision));
11731
+ resolve9(new Set(decision));
11204
11732
  }
11205
11733
  }
11206
11734
  }
11207
11735
  /** Cancel all pending confirms (e.g., on disconnect) */
11208
11736
  cancelAll() {
11209
- for (const resolve8 of this.pendingConfirms.values()) resolve8(false);
11737
+ for (const resolve9 of this.pendingConfirms.values()) resolve9(false);
11210
11738
  this.pendingConfirms.clear();
11211
- for (const resolve8 of this.pendingBatchConfirms.values()) resolve8("none");
11739
+ for (const resolve9 of this.pendingBatchConfirms.values()) resolve9("none");
11212
11740
  this.pendingBatchConfirms.clear();
11213
11741
  this.confirming = false;
11214
11742
  }
@@ -11251,9 +11779,9 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11251
11779
  if (call.name === "write_file") {
11252
11780
  const filePath = String(call.arguments["path"] ?? "");
11253
11781
  const newContent = String(call.arguments["content"] ?? "");
11254
- if (filePath && existsSync17(filePath)) {
11782
+ if (filePath && existsSync18(filePath)) {
11255
11783
  try {
11256
- const old = readFileSync12(filePath, "utf-8");
11784
+ const old = readFileSync13(filePath, "utf-8");
11257
11785
  return renderDiff(old, newContent, { filePath });
11258
11786
  } catch {
11259
11787
  }
@@ -11284,8 +11812,8 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11284
11812
  diff: this.getDiffPreview(call)
11285
11813
  };
11286
11814
  this.send(msg);
11287
- return new Promise((resolve8) => {
11288
- this.pendingConfirms.set(requestId, resolve8);
11815
+ return new Promise((resolve9) => {
11816
+ this.pendingConfirms.set(requestId, resolve9);
11289
11817
  this.pendingTimers.set(requestId, setTimeout(() => {
11290
11818
  if (this.pendingConfirms.has(requestId)) {
11291
11819
  this.resolveConfirm(requestId, false);
@@ -11309,8 +11837,8 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11309
11837
  files
11310
11838
  };
11311
11839
  this.send(msg);
11312
- return new Promise((resolve8) => {
11313
- this.pendingBatchConfirms.set(requestId, resolve8);
11840
+ return new Promise((resolve9) => {
11841
+ this.pendingBatchConfirms.set(requestId, resolve9);
11314
11842
  this.pendingTimers.set(requestId, setTimeout(() => {
11315
11843
  if (this.pendingBatchConfirms.has(requestId)) {
11316
11844
  this.resolveBatchConfirm(requestId, "none");
@@ -11327,30 +11855,96 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11327
11855
  return { callId: call.id, content: `Unknown tool: ${call.name}`, isError: true };
11328
11856
  }
11329
11857
  const dangerLevel = getDangerLevel(call.name, call.arguments);
11858
+ let toolCallStarted = false;
11330
11859
  runHook(this.hookConfig?.preToolExecution, {
11331
11860
  tool: call.name,
11332
11861
  dangerLevel,
11333
11862
  args: JSON.stringify(call.arguments).slice(0, 200)
11334
11863
  });
11335
- if (this.permissionRules.length > 0) {
11336
- const action = checkPermission(call.name, call.arguments, dangerLevel, this.permissionRules, this.defaultPermission);
11337
- if (action === "deny") {
11338
- return { callId: call.id, content: `[Permission denied] Tool ${call.name} is blocked by permission rules. Do not retry.`, isError: true };
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
+ }
11884
+ const permission = checkPermissionWithProfile(
11885
+ call.name,
11886
+ call.arguments,
11887
+ dangerLevel,
11888
+ this.permissionRules,
11889
+ this.defaultPermission,
11890
+ {
11891
+ profileName: this.permissionProfileName,
11892
+ profile: this.permissionProfile,
11893
+ allowedProfiles: this.allowedPermissionProfiles,
11894
+ workspaceRoot: this.workspaceRoot,
11895
+ tempDirs: [tmpdir2()]
11339
11896
  }
11340
- if (action === "auto-approve") {
11341
- this.sendToolCallStart(call);
11342
- try {
11343
- const rawContent = await runTool(tool, call.arguments, call.name);
11344
- const content = truncateOutput(rawContent, call.name);
11345
- this.sendToolCallResult(call, rawContent, false);
11346
- runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
11347
- return { callId: call.id, content, isError: false };
11348
- } catch (err) {
11349
- const message = err instanceof Error ? err.message : String(err);
11350
- this.sendToolCallResult(call, message, true);
11351
- runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
11352
- return { callId: call.id, content: message, isError: true };
11353
- }
11897
+ );
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
+ }
11907
+ if (networkPermission?.action === "deny") {
11908
+ const reason = networkPermission.reason ? ` (${networkPermission.reason})` : "";
11909
+ return {
11910
+ callId: call.id,
11911
+ content: `[Permission denied] Tool ${call.name} is blocked by networkPolicy${reason}. Do not retry.`,
11912
+ isError: true
11913
+ };
11914
+ }
11915
+ if (permission.action === "deny") {
11916
+ const reason = permission.reason ? ` (${permission.reason})` : "";
11917
+ return {
11918
+ callId: call.id,
11919
+ content: `[Permission denied] Tool ${call.name} is blocked by permission profile "${permission.profileName}"${reason}. Do not retry.`,
11920
+ isError: true
11921
+ };
11922
+ }
11923
+ if (permission.action === "auto-approve" && networkPermission?.action !== "confirm") {
11924
+ this.sendToolCallStart(call);
11925
+ try {
11926
+ const rawContent = await runTool(tool, call.arguments, call.name);
11927
+ const content = truncateOutput(rawContent, call.name);
11928
+ this.sendToolCallResult(call, rawContent, false);
11929
+ runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
11930
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
11931
+ return { callId: call.id, content, isError: false };
11932
+ } catch (err) {
11933
+ const message = err instanceof Error ? err.message : String(err);
11934
+ this.sendToolCallResult(call, message, true);
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 });
11937
+ return { callId: call.id, content: message, isError: true };
11938
+ }
11939
+ }
11940
+ if (networkPermission?.action === "confirm" && dangerLevel === "safe") {
11941
+ this.sendToolCallStart(call);
11942
+ toolCallStarted = true;
11943
+ const confirmed = await this.confirm(call, "write");
11944
+ if (!confirmed) {
11945
+ const rejectionMsg = `[User cancelled] The user declined network access for ${call.name}. Do not retry without asking.`;
11946
+ this.sendToolCallResult(call, rejectionMsg, true);
11947
+ return { callId: call.id, content: rejectionMsg, isError: true };
11354
11948
  }
11355
11949
  }
11356
11950
  this.sendToolCallStart(call);
@@ -11360,11 +11954,13 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11360
11954
  const content = truncateOutput(rawContent, call.name);
11361
11955
  this.sendToolCallResult(call, rawContent, false);
11362
11956
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
11957
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
11363
11958
  return { callId: call.id, content, isError: false };
11364
11959
  } catch (err) {
11365
11960
  const message = err instanceof Error ? err.message : String(err);
11366
11961
  this.sendToolCallResult(call, message, true);
11367
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 });
11368
11964
  return { callId: call.id, content: message, isError: true };
11369
11965
  }
11370
11966
  }
@@ -11382,11 +11978,13 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11382
11978
  const content = truncateOutput(rawContent, call.name);
11383
11979
  this.sendToolCallResult(call, rawContent, false);
11384
11980
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
11981
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
11385
11982
  return { callId: call.id, content, isError: false };
11386
11983
  } catch (err) {
11387
11984
  const message = err instanceof Error ? err.message : String(err);
11388
11985
  this.sendToolCallResult(call, message, true);
11389
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 });
11390
11988
  return { callId: call.id, content: message, isError: true };
11391
11989
  }
11392
11990
  }
@@ -11409,6 +12007,9 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11409
12007
  return results;
11410
12008
  }
11411
12009
  async executeBatchFileWrites(items) {
12010
+ if (this.permissionProfileName !== "legacy" || this.permissionRules.length > 0) {
12011
+ return Promise.all(items.map(({ call }) => this.execute(call)));
12012
+ }
11412
12013
  const calls = items.map((i) => i.call);
11413
12014
  const decision = this.sessionAutoApprove ? "all" : await this.batchConfirm(calls);
11414
12015
  const results = new Array(calls.length);
@@ -11454,20 +12055,20 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11454
12055
  };
11455
12056
 
11456
12057
  // src/core/system-prompt-builder.ts
11457
- import { existsSync as existsSync19, readFileSync as readFileSync14 } from "fs";
11458
- import { join as join12 } from "path";
12058
+ import { existsSync as existsSync20, readFileSync as readFileSync15 } from "fs";
12059
+ import { join as join13 } from "path";
11459
12060
 
11460
12061
  // src/repl/dev-state.ts
11461
- import { existsSync as existsSync18, readFileSync as readFileSync13, unlinkSync as unlinkSync4, mkdirSync as mkdirSync9 } from "fs";
11462
- 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";
11463
12064
  import { homedir as homedir6 } from "os";
11464
12065
  function getDevStatePath() {
11465
- return join11(homedir6(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
12066
+ return join12(homedir6(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
11466
12067
  }
11467
12068
  function loadDevState() {
11468
12069
  const path3 = getDevStatePath();
11469
- if (!existsSync18(path3)) return null;
11470
- const content = readFileSync13(path3, "utf-8").trim();
12070
+ if (!existsSync19(path3)) return null;
12071
+ const content = readFileSync14(path3, "utf-8").trim();
11471
12072
  return content || null;
11472
12073
  }
11473
12074
 
@@ -11525,9 +12126,9 @@ ${ctx.activeSkill.content}`);
11525
12126
  return { stable: stableParts.join("\n\n---\n\n"), volatile };
11526
12127
  }
11527
12128
  function loadMemoryContent(configDir) {
11528
- const memoryPath = join12(configDir, MEMORY_FILE_NAME);
11529
- if (!existsSync19(memoryPath)) return null;
11530
- 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();
11531
12132
  if (!content) return null;
11532
12133
  if (content.length > MEMORY_MAX_CHARS) {
11533
12134
  content = content.slice(-MEMORY_MAX_CHARS);
@@ -11736,8 +12337,8 @@ function autoTrimSessionIfNeeded(session, sizeLimit = SESSION_SIZE_LIMIT) {
11736
12337
  }
11737
12338
 
11738
12339
  // src/core/context-files.ts
11739
- import { existsSync as existsSync20, readFileSync as readFileSync15 } from "fs";
11740
- import { join as join13, relative as relative3, resolve as resolve5 } 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";
11741
12342
  function uniqueNonEmpty(values) {
11742
12343
  const seen = /* @__PURE__ */ new Set();
11743
12344
  const out = [];
@@ -11760,14 +12361,14 @@ function displayPath(filePath, cwd) {
11760
12361
  return filePath;
11761
12362
  }
11762
12363
  function isInsideOrEqual(parent, child) {
11763
- const rel = relative3(resolve5(parent), resolve5(child));
12364
+ const rel = relative3(resolve6(parent), resolve6(child));
11764
12365
  return rel === "" || !!rel && !rel.startsWith("..") && !rel.startsWith("/") && !rel.startsWith("\\");
11765
12366
  }
11766
12367
  function readContextFile(level, filePath, cwd, maxBytes) {
11767
12368
  const name = filePath.split(/[\\/]/).pop() ?? filePath;
11768
12369
  const shown = displayPath(filePath, cwd);
11769
12370
  try {
11770
- const raw = readFileSync15(filePath);
12371
+ const raw = readFileSync16(filePath);
11771
12372
  const byteCount = raw.byteLength;
11772
12373
  if (byteCount === 0) {
11773
12374
  return {
@@ -11814,8 +12415,8 @@ function readContextFile(level, filePath, cwd, maxBytes) {
11814
12415
  function findFirstContextFile(level, dir, cwd, candidates, maxBytes) {
11815
12416
  const skipped = [];
11816
12417
  for (const candidate of candidates) {
11817
- const filePath = join13(dir, candidate);
11818
- if (!existsSync20(filePath)) continue;
12418
+ const filePath = join14(dir, candidate);
12419
+ if (!existsSync21(filePath)) continue;
11819
12420
  const result = readContextFile(level, filePath, cwd, maxBytes);
11820
12421
  if (result.skipped) skipped.push(result.skipped);
11821
12422
  if (result.layer) return { layer: result.layer, skipped };
@@ -11823,7 +12424,7 @@ function findFirstContextFile(level, dir, cwd, candidates, maxBytes) {
11823
12424
  return { layer: null, skipped };
11824
12425
  }
11825
12426
  function loadContextFiles(options) {
11826
- const cwd = resolve5(options.cwd);
12427
+ const cwd = resolve6(options.cwd);
11827
12428
  const maxBytes = options.maxBytes ?? CONTEXT_FILE_MAX_BYTES;
11828
12429
  const candidates = options.candidates ?? buildContextFileCandidates(options.fallbackFilenames);
11829
12430
  const skipped = [];
@@ -11831,7 +12432,7 @@ function loadContextFiles(options) {
11831
12432
  return { layers: [], mergedContent: "", skipped, totalChars: 0, totalBytes: 0, maxBytes, candidates };
11832
12433
  }
11833
12434
  if (options.setting && options.setting !== "auto") {
11834
- const filePath = resolve5(cwd, String(options.setting));
12435
+ const filePath = resolve6(cwd, String(options.setting));
11835
12436
  if (!isInsideOrEqual(cwd, filePath)) {
11836
12437
  skipped.push({
11837
12438
  level: "single",
@@ -11842,7 +12443,7 @@ function loadContextFiles(options) {
11842
12443
  });
11843
12444
  return { layers: [], mergedContent: "", skipped, totalChars: 0, totalBytes: 0, maxBytes, candidates };
11844
12445
  }
11845
- if (!existsSync20(filePath)) {
12446
+ if (!existsSync21(filePath)) {
11846
12447
  skipped.push({
11847
12448
  level: "single",
11848
12449
  filePath,
@@ -11875,7 +12476,7 @@ function loadContextFiles(options) {
11875
12476
  skipped.push(...result.skipped);
11876
12477
  if (result.layer) layers.push(result.layer);
11877
12478
  }
11878
- if (resolve5(options.cwd) !== resolve5(options.projectRoot)) {
12479
+ if (resolve6(options.cwd) !== resolve6(options.projectRoot)) {
11879
12480
  const result = findFirstContextFile("local", options.cwd, cwd, candidates, maxBytes);
11880
12481
  skipped.push(...result.skipped);
11881
12482
  if (result.layer) layers.push(result.layer);
@@ -11893,13 +12494,13 @@ function loadContextFiles(options) {
11893
12494
  }
11894
12495
 
11895
12496
  // src/web/session-handler.ts
11896
- import { existsSync as existsSync23, readFileSync as readFileSync17, writeFileSync as writeFileSync2, mkdirSync as mkdirSync10, readdirSync as readdirSync10, statSync as statSync9, createWriteStream } from "fs";
11897
- import { join as join17, resolve as resolve6, 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";
11898
12499
 
11899
12500
  // src/tools/git-context.ts
11900
12501
  import { execSync as execSync2 } from "child_process";
11901
- import { existsSync as existsSync21 } from "fs";
11902
- import { join as join14 } from "path";
12502
+ import { existsSync as existsSync22 } from "fs";
12503
+ import { join as join15 } from "path";
11903
12504
  function runGit2(cmd, cwd) {
11904
12505
  try {
11905
12506
  return execSync2(`git ${cmd}`, {
@@ -11916,7 +12517,7 @@ function getGitRoot(cwd = process.cwd()) {
11916
12517
  return runGit2("rev-parse --show-toplevel", cwd);
11917
12518
  }
11918
12519
  function getGitContext(cwd = process.cwd()) {
11919
- if (!existsSync21(join14(cwd, ".git"))) {
12520
+ if (!existsSync22(join15(cwd, ".git"))) {
11920
12521
  const result = runGit2("rev-parse --git-dir", cwd);
11921
12522
  if (!result) return null;
11922
12523
  }
@@ -12045,8 +12646,8 @@ If no security issues found, state "\u2705 No security vulnerabilities detected"
12045
12646
  }
12046
12647
 
12047
12648
  // src/repl/commands/project-init.ts
12048
- import { existsSync as existsSync22, readFileSync as readFileSync16, readdirSync as readdirSync9, statSync as statSync8 } from "fs";
12049
- 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";
12050
12651
  var SCAN_SKIP_DIRS = /* @__PURE__ */ new Set([
12051
12652
  "node_modules",
12052
12653
  ".git",
@@ -12090,11 +12691,11 @@ function scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
12090
12691
  const sorted = filtered.sort((a, b) => {
12091
12692
  let aIsDir = false, bIsDir = false;
12092
12693
  try {
12093
- aIsDir = statSync8(join15(d, a)).isDirectory();
12694
+ aIsDir = statSync8(join16(d, a)).isDirectory();
12094
12695
  } catch {
12095
12696
  }
12096
12697
  try {
12097
- bIsDir = statSync8(join15(d, b)).isDirectory();
12698
+ bIsDir = statSync8(join16(d, b)).isDirectory();
12098
12699
  } catch {
12099
12700
  }
12100
12701
  if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
@@ -12102,7 +12703,7 @@ function scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
12102
12703
  });
12103
12704
  for (let i = 0; i < sorted.length && count < maxEntries; i++) {
12104
12705
  const name = sorted[i];
12105
- const fullPath = join15(d, name);
12706
+ const fullPath = join16(d, name);
12106
12707
  const isLast = i === sorted.length - 1;
12107
12708
  const connector = isLast ? "+-- " : "|-- ";
12108
12709
  let isDir;
@@ -12129,7 +12730,7 @@ function scanProject(cwd) {
12129
12730
  configFiles: [],
12130
12731
  directoryStructure: ""
12131
12732
  };
12132
- const check = (file) => existsSync22(join15(cwd, file));
12733
+ const check = (file) => existsSync23(join16(cwd, file));
12133
12734
  const configCandidates = [
12134
12735
  "package.json",
12135
12736
  "tsconfig.json",
@@ -12156,7 +12757,7 @@ function scanProject(cwd) {
12156
12757
  info.type = "node";
12157
12758
  info.language = check("tsconfig.json") ? "TypeScript" : "JavaScript";
12158
12759
  try {
12159
- const pkg = JSON.parse(readFileSync16(join15(cwd, "package.json"), "utf-8"));
12760
+ const pkg = JSON.parse(readFileSync17(join16(cwd, "package.json"), "utf-8"));
12160
12761
  const scripts = pkg.scripts ?? {};
12161
12762
  info.buildCommand = scripts.build ? "npm run build" : void 0;
12162
12763
  info.testCommand = scripts.test ? "npm test" : void 0;
@@ -12816,7 +13417,7 @@ function resolveRoleProviders(roles, lookup, defaultProvider, defaultModel, avai
12816
13417
  }
12817
13418
 
12818
13419
  // src/hub/persist.ts
12819
- import { join as join16 } from "path";
13420
+ import { join as join17 } from "path";
12820
13421
  function discussionToMessages(state2) {
12821
13422
  const out = [];
12822
13423
  const t0 = state2.messages[0]?.timestamp ?? /* @__PURE__ */ new Date();
@@ -12854,7 +13455,7 @@ async function persistDiscussion(state2, config, defaultProvider, defaultModel)
12854
13455
  session.title = `[Hub] ${state2.topic.slice(0, 48)}`.replace(/\n/g, " ");
12855
13456
  session.titleAiGenerated = true;
12856
13457
  await sm.save();
12857
- return { id: session.id, path: join16(config.getHistoryDir(), `${session.id}.json`) };
13458
+ return { id: session.id, path: join17(config.getHistoryDir(), `${session.id}.json`) };
12858
13459
  }
12859
13460
 
12860
13461
  // src/web/session-handler.ts
@@ -12953,7 +13554,28 @@ var SessionHandler = class {
12953
13554
  const hooks = this.config.get("hooks");
12954
13555
  const permissionRules = this.config.get("permissionRules");
12955
13556
  const defaultPermission = this.config.get("defaultPermission");
12956
- this.toolExecutor.setConfig({ hookConfig: hooks, permissionRules, defaultPermission });
13557
+ const permissionProfileName = this.config.get("defaultPermissionProfile") ?? "legacy";
13558
+ const permissionProfiles = this.config.get("permissionProfiles") ?? {};
13559
+ this.toolExecutor.setConfig({
13560
+ hookConfig: hooks,
13561
+ hookConfigDir: this.config.getConfigDir(),
13562
+ permissionRules,
13563
+ defaultPermission,
13564
+ permissionProfileName,
13565
+ permissionProfile: permissionProfiles[permissionProfileName],
13566
+ allowedPermissionProfiles: this.config.get("allowedPermissionProfiles") ?? [],
13567
+ workspaceRoot: process.cwd(),
13568
+ networkPolicy: this.config.get("networkPolicy")
13569
+ });
13570
+ const permissionProfileWarning = formatPermissionProfileWarning(permissionProfileName);
13571
+ if (permissionProfileWarning) {
13572
+ this.send({ type: "info", message: permissionProfileWarning });
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() });
12957
13579
  this.sendStatus();
12958
13580
  askUserContext.rl = null;
12959
13581
  askUserContext.prompting = false;
@@ -12988,6 +13610,7 @@ var SessionHandler = class {
12988
13610
  messageCount: this.sessions.current?.messages.length ?? 0,
12989
13611
  planMode: this.planMode,
12990
13612
  thinkingMode: this.runtimeThinking ?? false,
13613
+ permissionProfile: this.config.get("defaultPermissionProfile") ?? "legacy",
12991
13614
  tokenUsage: { ...this.sessionTokenUsage },
12992
13615
  costUsd,
12993
13616
  providers: providerList,
@@ -13016,10 +13639,10 @@ var SessionHandler = class {
13016
13639
  return;
13017
13640
  }
13018
13641
  case "ask_user_response": {
13019
- const resolve8 = this.pendingAskUser.get(msg.requestId);
13020
- if (resolve8) {
13642
+ const resolve9 = this.pendingAskUser.get(msg.requestId);
13643
+ if (resolve9) {
13021
13644
  this.pendingAskUser.delete(msg.requestId);
13022
- resolve8(msg.answer);
13645
+ resolve9(msg.answer);
13023
13646
  }
13024
13647
  return;
13025
13648
  }
@@ -13030,10 +13653,10 @@ var SessionHandler = class {
13030
13653
  case "memory_rebuild":
13031
13654
  return this.handleMemoryRebuild(Boolean(msg.full));
13032
13655
  case "auto_pause_response": {
13033
- const resolve8 = this.pendingAutoPause.get(msg.requestId);
13034
- if (resolve8) {
13656
+ const resolve9 = this.pendingAutoPause.get(msg.requestId);
13657
+ if (resolve9) {
13035
13658
  this.pendingAutoPause.delete(msg.requestId);
13036
- resolve8({ action: msg.action, message: msg.message });
13659
+ resolve9({ action: msg.action, message: msg.message });
13037
13660
  }
13038
13661
  return;
13039
13662
  }
@@ -13048,10 +13671,10 @@ var SessionHandler = class {
13048
13671
  this.hubOrchestrator?.abort();
13049
13672
  return;
13050
13673
  case "hub_steer": {
13051
- const resolve8 = this.pendingHubReview.get(msg.requestId);
13052
- if (resolve8) {
13674
+ const resolve9 = this.pendingHubReview.get(msg.requestId);
13675
+ if (resolve9) {
13053
13676
  this.pendingHubReview.delete(msg.requestId);
13054
- resolve8({ action: msg.action, message: msg.message });
13677
+ resolve9({ action: msg.action, message: msg.message });
13055
13678
  }
13056
13679
  return;
13057
13680
  }
@@ -13061,11 +13684,12 @@ var SessionHandler = class {
13061
13684
  }
13062
13685
  }
13063
13686
  onDisconnect() {
13687
+ runLifecycleHooks(this.config.get("hooks") ?? void 0, "Stop", { sessionId: this.sessions.current?.id }, { configDir: this.config.getConfigDir() });
13064
13688
  this.toolExecutor.cancelAll();
13065
13689
  if (this.abortController) this.abortController.abort();
13066
- for (const resolve8 of this.pendingAskUser.values()) resolve8(null);
13690
+ for (const resolve9 of this.pendingAskUser.values()) resolve9(null);
13067
13691
  this.pendingAskUser.clear();
13068
- for (const resolve8 of this.pendingAutoPause.values()) resolve8({ action: "stop" });
13692
+ for (const resolve9 of this.pendingAutoPause.values()) resolve9({ action: "stop" });
13069
13693
  this.pendingAutoPause.clear();
13070
13694
  this.saveIfNeeded();
13071
13695
  }
@@ -13199,9 +13823,9 @@ var SessionHandler = class {
13199
13823
  this.hubOrchestrator = orchestrator;
13200
13824
  orchestrator.onEvent = (event) => this.send({ type: "hub_event", event });
13201
13825
  if (config.humanSteer) {
13202
- orchestrator.onRoundReview = ({ round, maxRounds }) => new Promise((resolve8) => {
13826
+ orchestrator.onRoundReview = ({ round, maxRounds }) => new Promise((resolve9) => {
13203
13827
  const requestId = `hubrev_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
13204
- this.pendingHubReview.set(requestId, resolve8);
13828
+ this.pendingHubReview.set(requestId, resolve9);
13205
13829
  this.send({ type: "hub_review", requestId, round, maxRounds });
13206
13830
  });
13207
13831
  }
@@ -13238,6 +13862,26 @@ var SessionHandler = class {
13238
13862
  try {
13239
13863
  this.ensureSession();
13240
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
+ }
13241
13885
  const ALLOWED_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"]);
13242
13886
  const MAX_IMAGE_SIZE = 10 * 1024 * 1024;
13243
13887
  let msgContent;
@@ -13513,7 +14157,7 @@ Try: /compact to reduce context, /clear to reset, or switch to a larger-context
13513
14157
  this.send({ type: "response_done", content, usage });
13514
14158
  if (pendingTeeSave && isCleanDocumentBody(content)) {
13515
14159
  try {
13516
- mkdirSync10(dirname5(pendingTeeSave), { recursive: true });
14160
+ mkdirSync11(dirname5(pendingTeeSave), { recursive: true });
13517
14161
  const bodyToSave = stripOuterCodeFence(content);
13518
14162
  atomicWriteFileSync(pendingTeeSave, bodyToSave);
13519
14163
  undoStack.push(pendingTeeSave, `save_last_response (deferred): ${pendingTeeSave}`);
@@ -13538,8 +14182,8 @@ Try: /compact to reduce context, /clear to reset, or switch to a larger-context
13538
14182
  onMcpToolUsed: (name) => this.usedMcpToolNames.add(name),
13539
14183
  requestAutoPause: async ({ effectiveRound, maxToolRounds: totalRounds, toolSummary }) => {
13540
14184
  const requestId = `pause_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
13541
- const pauseResp = await new Promise((resolve8) => {
13542
- this.pendingAutoPause.set(requestId, resolve8);
14185
+ const pauseResp = await new Promise((resolve9) => {
14186
+ this.pendingAutoPause.set(requestId, resolve9);
13543
14187
  this.send({
13544
14188
  type: "auto_pause_request",
13545
14189
  requestId,
@@ -13689,7 +14333,7 @@ ${summaryContent}`,
13689
14333
  let isError = false;
13690
14334
  let summary;
13691
14335
  try {
13692
- mkdirSync10(dirname5(saveToFile), { recursive: true });
14336
+ mkdirSync11(dirname5(saveToFile), { recursive: true });
13693
14337
  fileStream = createWriteStream(saveToFile);
13694
14338
  const teeSystemPrompt = stripToolCallReminder(systemPrompt ?? "") + CONTENT_ONLY_STREAM_REMINDER;
13695
14339
  const teeExtraMessages = extraMessages.length > 0 ? [...extraMessages, { role: "user", content: TEE_FINAL_USER_NUDGE }] : [{ role: "user", content: TEE_FINAL_USER_NUDGE }];
@@ -13718,8 +14362,8 @@ ${summaryContent}`,
13718
14362
  }
13719
14363
  if (chunk.done) break;
13720
14364
  }
13721
- await new Promise((resolve8, reject) => {
13722
- fileStream.end((err) => err ? reject(err) : resolve8());
14365
+ await new Promise((resolve9, reject) => {
14366
+ fileStream.end((err) => err ? reject(err) : resolve9());
13723
14367
  });
13724
14368
  const verdict = evaluateTeeContent(fullContent, saveToFile, priorContent);
13725
14369
  if (verdict.kind === "reject") {
@@ -13745,7 +14389,7 @@ ${summaryContent}`,
13745
14389
  } catch (err) {
13746
14390
  if (fileStream) {
13747
14391
  try {
13748
- await new Promise((resolve8) => fileStream.end(() => resolve8()));
14392
+ await new Promise((resolve9) => fileStream.end(() => resolve9()));
13749
14393
  } catch {
13750
14394
  }
13751
14395
  }
@@ -14275,9 +14919,9 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
14275
14919
  let modifiedFiles = 0;
14276
14920
  const diffLines2 = [];
14277
14921
  for (const [filePath, { earliest }] of fileMap) {
14278
- const currentContent = existsSync23(filePath) ? (() => {
14922
+ const currentContent = existsSync24(filePath) ? (() => {
14279
14923
  try {
14280
- return readFileSync17(filePath, "utf-8");
14924
+ return readFileSync18(filePath, "utf-8");
14281
14925
  } catch {
14282
14926
  return null;
14283
14927
  }
@@ -14378,7 +15022,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
14378
15022
  break;
14379
15023
  }
14380
15024
  const sub = args[0]?.toLowerCase();
14381
- const resolve8 = (ref) => {
15025
+ const resolve9 = (ref) => {
14382
15026
  const r = session.resolveBranchRef(ref);
14383
15027
  if (r.ok) return r.id;
14384
15028
  if (r.reason === "ambiguous") {
@@ -14432,7 +15076,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
14432
15076
  this.send({ type: "error", message: "Usage: /branch switch <id|title>" });
14433
15077
  break;
14434
15078
  }
14435
- const id = resolve8(ref);
15079
+ const id = resolve9(ref);
14436
15080
  if (!id) break;
14437
15081
  const ok = session.switchBranch(id);
14438
15082
  if (ok) {
@@ -14452,7 +15096,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
14452
15096
  this.send({ type: "error", message: "Usage: /branch delete <id|title>" });
14453
15097
  break;
14454
15098
  }
14455
- const id = resolve8(ref);
15099
+ const id = resolve9(ref);
14456
15100
  if (!id) break;
14457
15101
  const ok = session.deleteBranch(id);
14458
15102
  if (ok) {
@@ -14471,7 +15115,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
14471
15115
  this.send({ type: "error", message: "Usage: /branch rename <id|title> <new title>" });
14472
15116
  break;
14473
15117
  }
14474
- const id = resolve8(ref);
15118
+ const id = resolve9(ref);
14475
15119
  if (!id) break;
14476
15120
  const ok = session.renameBranch(id, title);
14477
15121
  if (ok) {
@@ -14489,7 +15133,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
14489
15133
  this.send({ type: "error", message: "Usage: /branch diff <id|title>" });
14490
15134
  break;
14491
15135
  }
14492
- const id = resolve8(ref);
15136
+ const id = resolve9(ref);
14493
15137
  if (!id) break;
14494
15138
  const d = session.diffBranches(id);
14495
15139
  if (!d) {
@@ -14531,7 +15175,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
14531
15175
  this.send({ type: "error", message: "Usage: /branch cherry-pick <source-id|title> <msg-index>" });
14532
15176
  break;
14533
15177
  }
14534
- const id = resolve8(ref);
15178
+ const id = resolve9(ref);
14535
15179
  if (!id) break;
14536
15180
  const idx = parseInt(idxStr, 10);
14537
15181
  if (Number.isNaN(idx)) {
@@ -14793,7 +15437,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
14793
15437
  case "test": {
14794
15438
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
14795
15439
  try {
14796
- const { executeTests } = await import("./run-tests-NT2UIUVB.js");
15440
+ const { executeTests } = await import("./run-tests-D7YBY4XB.js");
14797
15441
  const argStr = args.join(" ").trim();
14798
15442
  let testArgs = {};
14799
15443
  if (argStr) {
@@ -14810,9 +15454,9 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
14810
15454
  // ── /init ───────────────────────────────────────────────────────
14811
15455
  case "init": {
14812
15456
  const cwd = process.cwd();
14813
- const targetPath = join17(cwd, "AICLI.md");
15457
+ const targetPath = join18(cwd, "AICLI.md");
14814
15458
  const force = args.includes("--force");
14815
- if (existsSync23(targetPath) && !force) {
15459
+ if (existsSync24(targetPath) && !force) {
14816
15460
  this.send({ type: "info", message: `AICLI.md already exists at ${targetPath}
14817
15461
  Use /init --force to overwrite.` });
14818
15462
  break;
@@ -14845,7 +15489,7 @@ Use /context reload to load it.` });
14845
15489
  lines.push("**Config Files:**");
14846
15490
  lines.push(` Dir: ${configDir}`);
14847
15491
  const checkFile = (label, filePath) => {
14848
- const exists = existsSync23(filePath);
15492
+ const exists = existsSync24(filePath);
14849
15493
  let extra = "";
14850
15494
  if (exists) {
14851
15495
  try {
@@ -14855,9 +15499,9 @@ Use /context reload to load it.` });
14855
15499
  }
14856
15500
  lines.push(` ${exists ? "\u2713" : "\u2013"} ${label.padEnd(14)} ${exists ? filePath + extra : "(not found)"}`);
14857
15501
  };
14858
- checkFile("config.json", join17(configDir, "config.json"));
14859
- checkFile("memory.md", join17(configDir, MEMORY_FILE_NAME));
14860
- 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"));
14861
15505
  lines.push("");
14862
15506
  if (this.mcpManager) {
14863
15507
  lines.push("**MCP Servers:**");
@@ -15044,8 +15688,8 @@ Use /add-dir remove to clear.` : "No directories added.\nUsage: /add-dir <path>
15044
15688
  this.send({ type: "info", message: "\u2713 All added directories removed from context." });
15045
15689
  break;
15046
15690
  }
15047
- const dirPath = resolve6(sub);
15048
- if (!existsSync23(dirPath)) {
15691
+ const dirPath = resolve7(sub);
15692
+ if (!existsSync24(dirPath)) {
15049
15693
  this.send({ type: "error", message: `Directory not found: ${dirPath}` });
15050
15694
  break;
15051
15695
  }
@@ -15061,8 +15705,8 @@ It will be included in AI context for subsequent messages.` });
15061
15705
  // ── /commands ───────────────────────────────────────────────────
15062
15706
  case "commands": {
15063
15707
  const configDir = this.config.getConfigDir();
15064
- const commandsDir = join17(configDir, CUSTOM_COMMANDS_DIR_NAME);
15065
- if (!existsSync23(commandsDir)) {
15708
+ const commandsDir = join18(configDir, CUSTOM_COMMANDS_DIR_NAME);
15709
+ if (!existsSync24(commandsDir)) {
15066
15710
  this.send({ type: "info", message: `No custom commands directory.
15067
15711
  Create: ${commandsDir}/ with .md files.` });
15068
15712
  break;
@@ -15088,7 +15732,7 @@ Add .md files to create commands.` });
15088
15732
  // ── /plugins ────────────────────────────────────────────────────
15089
15733
  case "plugins": {
15090
15734
  const configDir = this.config.getConfigDir();
15091
- const pluginsDir = join17(configDir, PLUGINS_DIR_NAME);
15735
+ const pluginsDir = join18(configDir, PLUGINS_DIR_NAME);
15092
15736
  const pluginTools = this.toolRegistry.listPluginTools();
15093
15737
  const lines = [`\u{1F50C} **Plugins:**`, `Dir: ${pluginsDir}`, ""];
15094
15738
  if (pluginTools.length === 0) {
@@ -15143,6 +15787,21 @@ Add .md files to create commands.` });
15143
15787
  this.send({ type: "info", message: "Not enough messages to compact." });
15144
15788
  return;
15145
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
+ }
15146
15805
  const provider = this.providers.get(this.currentProvider);
15147
15806
  if (!provider) return;
15148
15807
  this.send({ type: "info", message: "Compacting conversation..." });
@@ -15162,7 +15821,15 @@ Add .md files to create commands.` });
15162
15821
  });
15163
15822
  const summaryMsg = { role: "user", content: "[Conversation summary]", timestamp: /* @__PURE__ */ new Date() };
15164
15823
  const ackMsg = { role: "assistant", content: response.content, timestamp: /* @__PURE__ */ new Date() };
15165
- 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
+ );
15166
15833
  this.send({ type: "info", message: `Compacted: removed ${toSummarize.length} messages, kept last ${keepLast}.` });
15167
15834
  this.sendStatus();
15168
15835
  } catch (err) {
@@ -15262,11 +15929,11 @@ Add .md files to create commands.` });
15262
15929
  }
15263
15930
  memoryShow() {
15264
15931
  const configDir = this.config.getConfigDir();
15265
- const memPath = join17(configDir, MEMORY_FILE_NAME);
15932
+ const memPath = join18(configDir, MEMORY_FILE_NAME);
15266
15933
  let content = "";
15267
15934
  try {
15268
- if (existsSync23(memPath)) {
15269
- content = readFileSync17(memPath, "utf-8");
15935
+ if (existsSync24(memPath)) {
15936
+ content = readFileSync18(memPath, "utf-8");
15270
15937
  }
15271
15938
  } catch (err) {
15272
15939
  process.stderr.write(`[web] Failed to read memory file: ${err instanceof Error ? err.message : err}
@@ -15280,11 +15947,11 @@ Add .md files to create commands.` });
15280
15947
  }
15281
15948
  memoryAdd(text) {
15282
15949
  const configDir = this.config.getConfigDir();
15283
- const memPath = join17(configDir, MEMORY_FILE_NAME);
15950
+ const memPath = join18(configDir, MEMORY_FILE_NAME);
15284
15951
  try {
15285
- mkdirSync10(configDir, { recursive: true });
15952
+ mkdirSync11(configDir, { recursive: true });
15286
15953
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ");
15287
- const previous = existsSync23(memPath) ? readFileSync17(memPath, "utf-8") : "";
15954
+ const previous = existsSync24(memPath) ? readFileSync18(memPath, "utf-8") : "";
15288
15955
  const entry = `
15289
15956
  - [${timestamp}] ${text}
15290
15957
  `;
@@ -15296,7 +15963,7 @@ Add .md files to create commands.` });
15296
15963
  }
15297
15964
  memoryClear() {
15298
15965
  const configDir = this.config.getConfigDir();
15299
- const memPath = join17(configDir, MEMORY_FILE_NAME);
15966
+ const memPath = join18(configDir, MEMORY_FILE_NAME);
15300
15967
  try {
15301
15968
  atomicWriteFileSync(memPath, "");
15302
15969
  this.send({ type: "info", message: "\u{1F5D1}\uFE0F Persistent memory cleared." });
@@ -15314,7 +15981,7 @@ Add .md files to create commands.` });
15314
15981
  return;
15315
15982
  }
15316
15983
  try {
15317
- const { searchChatMemory: searchChatMemory2, loadChatIndex: loadChatIndex2 } = await import("./chat-index-UBCWHBLR.js");
15984
+ const { searchChatMemory: searchChatMemory2, loadChatIndex: loadChatIndex2 } = await import("./chat-index-R2E27VXN.js");
15318
15985
  const loaded = loadChatIndex2();
15319
15986
  if (!loaded || loaded.idx.chunks.length === 0) {
15320
15987
  this.send({ type: "memory_hits", query: q, hits: [], indexMissing: true });
@@ -15350,7 +16017,7 @@ Add .md files to create commands.` });
15350
16017
  }
15351
16018
  async handleMemoryStatus() {
15352
16019
  try {
15353
- const { getChatIndexStatus } = await import("./chat-index-UBCWHBLR.js");
16020
+ const { getChatIndexStatus } = await import("./chat-index-R2E27VXN.js");
15354
16021
  const s = getChatIndexStatus();
15355
16022
  this.send({
15356
16023
  type: "memory_status",
@@ -15375,7 +16042,7 @@ Add .md files to create commands.` });
15375
16042
  type: "info",
15376
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"
15377
16044
  });
15378
- const { buildChatIndex } = await import("./chat-index-UBCWHBLR.js");
16045
+ const { buildChatIndex } = await import("./chat-index-R2E27VXN.js");
15379
16046
  const stats = await buildChatIndex({
15380
16047
  full,
15381
16048
  onProgress: (p) => {
@@ -15530,8 +16197,8 @@ async function setupProxy(configProxy) {
15530
16197
  }
15531
16198
 
15532
16199
  // src/web/auth.ts
15533
- import { existsSync as existsSync24, readFileSync as readFileSync18, writeFileSync as writeFileSync3, mkdirSync as mkdirSync11, readdirSync as readdirSync11, copyFileSync } from "fs";
15534
- 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";
15535
16202
  import { createHmac, randomBytes, timingSafeEqual, pbkdf2Sync } from "crypto";
15536
16203
  var USERS_FILE = "users.json";
15537
16204
  var TOKEN_EXPIRY_HOURS = 24;
@@ -15546,7 +16213,7 @@ var AuthManager = class {
15546
16213
  db;
15547
16214
  constructor(baseDir) {
15548
16215
  this.baseDir = baseDir;
15549
- this.usersFile = join18(baseDir, USERS_FILE);
16216
+ this.usersFile = join19(baseDir, USERS_FILE);
15550
16217
  this.db = this.loadOrCreate();
15551
16218
  }
15552
16219
  // ── Public API ─────────────────────────────────────────────────
@@ -15575,9 +16242,9 @@ var AuthManager = class {
15575
16242
  }
15576
16243
  const salt = randomBytes(16).toString("hex");
15577
16244
  const passwordHash = this.hashPassword(password, salt);
15578
- const dataDir = join18(USERS_DIR, username);
15579
- const fullDataDir = join18(this.baseDir, dataDir);
15580
- 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 });
15581
16248
  const user = {
15582
16249
  username,
15583
16250
  passwordHash,
@@ -15692,7 +16359,7 @@ var AuthManager = class {
15692
16359
  getUserDataDir(username) {
15693
16360
  const user = this.db.users.find((u) => u.username === username);
15694
16361
  if (!user) throw new Error(`User not found: ${username}`);
15695
- return join18(this.baseDir, user.dataDir);
16362
+ return join19(this.baseDir, user.dataDir);
15696
16363
  }
15697
16364
  /** List all usernames */
15698
16365
  listUsers() {
@@ -15728,30 +16395,30 @@ var AuthManager = class {
15728
16395
  const err = this.register(username, password);
15729
16396
  if (err) return err;
15730
16397
  const userDir = this.getUserDataDir(username);
15731
- const globalConfig = join18(this.baseDir, "config.json");
15732
- if (existsSync24(globalConfig)) {
16398
+ const globalConfig = join19(this.baseDir, "config.json");
16399
+ if (existsSync25(globalConfig)) {
15733
16400
  try {
15734
- const content = readFileSync18(globalConfig, "utf-8");
15735
- writeFileSync3(join18(userDir, "config.json"), content, "utf-8");
16401
+ const content = readFileSync19(globalConfig, "utf-8");
16402
+ writeFileSync3(join19(userDir, "config.json"), content, "utf-8");
15736
16403
  } catch {
15737
16404
  }
15738
16405
  }
15739
- const globalMemory = join18(this.baseDir, "memory.md");
15740
- if (existsSync24(globalMemory)) {
16406
+ const globalMemory = join19(this.baseDir, "memory.md");
16407
+ if (existsSync25(globalMemory)) {
15741
16408
  try {
15742
- const content = readFileSync18(globalMemory, "utf-8");
15743
- writeFileSync3(join18(userDir, "memory.md"), content, "utf-8");
16409
+ const content = readFileSync19(globalMemory, "utf-8");
16410
+ writeFileSync3(join19(userDir, "memory.md"), content, "utf-8");
15744
16411
  } catch {
15745
16412
  }
15746
16413
  }
15747
- const globalHistory = join18(this.baseDir, "history");
15748
- if (existsSync24(globalHistory)) {
16414
+ const globalHistory = join19(this.baseDir, "history");
16415
+ if (existsSync25(globalHistory)) {
15749
16416
  try {
15750
16417
  const files = readdirSync11(globalHistory).filter((f) => f.endsWith(".json"));
15751
- const userHistory = join18(userDir, "history");
16418
+ const userHistory = join19(userDir, "history");
15752
16419
  for (const f of files) {
15753
16420
  try {
15754
- copyFileSync(join18(globalHistory, f), join18(userHistory, f));
16421
+ copyFileSync(join19(globalHistory, f), join19(userHistory, f));
15755
16422
  } catch {
15756
16423
  }
15757
16424
  }
@@ -15762,9 +16429,9 @@ var AuthManager = class {
15762
16429
  }
15763
16430
  // ── Private methods ────────────────────────────────────────────
15764
16431
  loadOrCreate() {
15765
- if (existsSync24(this.usersFile)) {
16432
+ if (existsSync25(this.usersFile)) {
15766
16433
  try {
15767
- return JSON.parse(readFileSync18(this.usersFile, "utf-8"));
16434
+ return JSON.parse(readFileSync19(this.usersFile, "utf-8"));
15768
16435
  } catch {
15769
16436
  }
15770
16437
  }
@@ -15780,7 +16447,7 @@ var AuthManager = class {
15780
16447
  this.saveDB(this.db);
15781
16448
  }
15782
16449
  saveDB(db) {
15783
- mkdirSync11(this.baseDir, { recursive: true });
16450
+ mkdirSync12(this.baseDir, { recursive: true });
15784
16451
  atomicWriteFileSync(this.usersFile, JSON.stringify(db, null, 2));
15785
16452
  }
15786
16453
  /** Legacy hash — kept only for migrating old users (v0.2.x) */
@@ -15816,7 +16483,7 @@ function getModuleDir() {
15816
16483
  }
15817
16484
  } catch {
15818
16485
  }
15819
- return resolve7(".");
16486
+ return resolve8(".");
15820
16487
  }
15821
16488
  async function startWebServer(options = {}) {
15822
16489
  const port = options.port ?? 3e3;
@@ -15881,8 +16548,8 @@ async function startWebServer(options = {}) {
15881
16548
  }
15882
16549
  }
15883
16550
  let skillManager = null;
15884
- const skillsDir = join19(config.getConfigDir(), SKILLS_DIR_NAME);
15885
- if (existsSync25(skillsDir)) {
16551
+ const skillsDir = join20(config.getConfigDir(), SKILLS_DIR_NAME);
16552
+ if (existsSync26(skillsDir)) {
15886
16553
  skillManager = new SkillManager(skillsDir, config.get("ui").skillSizeWarn);
15887
16554
  skillManager.loadSkills();
15888
16555
  const count = skillManager.listSkills().length;
@@ -15953,18 +16620,18 @@ async function startWebServer(options = {}) {
15953
16620
  next();
15954
16621
  };
15955
16622
  const moduleDir = getModuleDir();
15956
- let clientDir = join19(moduleDir, "web", "client");
15957
- if (!existsSync25(clientDir)) {
15958
- clientDir = join19(moduleDir, "client");
16623
+ let clientDir = join20(moduleDir, "web", "client");
16624
+ if (!existsSync26(clientDir)) {
16625
+ clientDir = join20(moduleDir, "client");
15959
16626
  }
15960
- if (!existsSync25(clientDir)) {
15961
- clientDir = join19(moduleDir, "..", "..", "src", "web", "client");
16627
+ if (!existsSync26(clientDir)) {
16628
+ clientDir = join20(moduleDir, "..", "..", "src", "web", "client");
15962
16629
  }
15963
- if (!existsSync25(clientDir)) {
15964
- clientDir = join19(process.cwd(), "src", "web", "client");
16630
+ if (!existsSync26(clientDir)) {
16631
+ clientDir = join20(process.cwd(), "src", "web", "client");
15965
16632
  }
15966
16633
  console.log(` Static files: ${clientDir}`);
15967
- 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 }));
15968
16635
  app.use(express.static(clientDir));
15969
16636
  app.get("/api/status", (_req, res) => {
15970
16637
  res.json({
@@ -16031,10 +16698,10 @@ async function startWebServer(options = {}) {
16031
16698
  app.get("/api/files", requireAuth, (req, res) => {
16032
16699
  const cwd = process.cwd();
16033
16700
  const prefix = req.query.prefix || "";
16034
- const targetDir = join19(cwd, prefix);
16701
+ const targetDir = join20(cwd, prefix);
16035
16702
  try {
16036
- const canonicalTarget = realpathSync(resolve7(targetDir));
16037
- const canonicalCwd = realpathSync(resolve7(cwd));
16703
+ const canonicalTarget = realpathSync(resolve8(targetDir));
16704
+ const canonicalCwd = realpathSync(resolve8(cwd));
16038
16705
  if (!canonicalTarget.startsWith(canonicalCwd + sep3) && canonicalTarget !== canonicalCwd) {
16039
16706
  res.json({ files: [] });
16040
16707
  return;
@@ -16048,7 +16715,7 @@ async function startWebServer(options = {}) {
16048
16715
  const entries = readdirSync12(targetDir, { withFileTypes: true });
16049
16716
  const files = entries.filter((e) => !SKIP.has(e.name) && !e.name.startsWith(".")).slice(0, 50).map((e) => ({
16050
16717
  name: e.name,
16051
- path: relative4(cwd, join19(targetDir, e.name)).replace(/\\/g, "/"),
16718
+ path: relative4(cwd, join20(targetDir, e.name)).replace(/\\/g, "/"),
16052
16719
  isDir: e.isDirectory()
16053
16720
  }));
16054
16721
  res.json({ files });
@@ -16084,8 +16751,8 @@ async function startWebServer(options = {}) {
16084
16751
  try {
16085
16752
  const authUser = req._authUser;
16086
16753
  const histDir = authUser ? getUserShared(authUser).config.getHistoryDir() : config.getHistoryDir();
16087
- const filePath = join19(histDir, `${id}.json`);
16088
- if (!existsSync25(filePath)) {
16754
+ const filePath = join20(histDir, `${id}.json`);
16755
+ if (!existsSync26(filePath)) {
16089
16756
  res.status(404).json({ error: "Session not found" });
16090
16757
  return;
16091
16758
  }
@@ -16100,7 +16767,7 @@ async function startWebServer(options = {}) {
16100
16767
  res.status(404).json({ error: "Session not found" });
16101
16768
  return;
16102
16769
  }
16103
- const data = JSON.parse(readFileSync19(filePath, "utf-8"));
16770
+ const data = JSON.parse(readFileSync20(filePath, "utf-8"));
16104
16771
  res.json({ session: data });
16105
16772
  } catch (err) {
16106
16773
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
@@ -16113,10 +16780,10 @@ async function startWebServer(options = {}) {
16113
16780
  return;
16114
16781
  }
16115
16782
  const cwd = process.cwd();
16116
- const fullPath = resolve7(join19(cwd, filePath));
16783
+ const fullPath = resolve8(join20(cwd, filePath));
16117
16784
  try {
16118
16785
  const canonicalFull = realpathSync(fullPath);
16119
- const canonicalCwd = realpathSync(resolve7(cwd));
16786
+ const canonicalCwd = realpathSync(resolve8(cwd));
16120
16787
  if (!canonicalFull.startsWith(canonicalCwd + sep3) && canonicalFull !== canonicalCwd) {
16121
16788
  res.json({ error: "Access denied" });
16122
16789
  return;
@@ -16131,7 +16798,7 @@ async function startWebServer(options = {}) {
16131
16798
  res.json({ error: `File too large (${(stat.size / 1024).toFixed(0)} KB, max 512 KB)` });
16132
16799
  return;
16133
16800
  }
16134
- const content = readFileSync19(fullPath, "utf-8");
16801
+ const content = readFileSync20(fullPath, "utf-8");
16135
16802
  res.json({ content, size: stat.size });
16136
16803
  } catch {
16137
16804
  res.json({ error: "Cannot read file" });
@@ -16335,7 +17002,7 @@ async function startWebServer(options = {}) {
16335
17002
  });
16336
17003
  const MAX_PORT_ATTEMPTS = 10;
16337
17004
  let actualPort = port;
16338
- const result = await new Promise((resolve8, reject) => {
17005
+ const result = await new Promise((resolve9, reject) => {
16339
17006
  const tryListen = (attempt) => {
16340
17007
  server.once("error", (err) => {
16341
17008
  if (err.code === "EADDRINUSE" && attempt < MAX_PORT_ATTEMPTS) {
@@ -16366,7 +17033,7 @@ async function startWebServer(options = {}) {
16366
17033
  }
16367
17034
  console.log(` Press Ctrl+C to stop
16368
17035
  `);
16369
- resolve8({ port: actualPort, host, url });
17036
+ resolve9({ port: actualPort, host, url });
16370
17037
  });
16371
17038
  };
16372
17039
  tryListen(1);
@@ -16386,17 +17053,17 @@ function resolveProjectMcpPath() {
16386
17053
  const cwd = process.cwd();
16387
17054
  const gitRoot = getGitRoot(cwd);
16388
17055
  const projectRoot = gitRoot ?? cwd;
16389
- const configPath = join19(projectRoot, MCP_PROJECT_CONFIG_NAME);
16390
- return existsSync25(configPath) ? configPath : null;
17056
+ const configPath = join20(projectRoot, MCP_PROJECT_CONFIG_NAME);
17057
+ return existsSync26(configPath) ? configPath : null;
16391
17058
  }
16392
17059
  function loadProjectMcpConfig() {
16393
17060
  const cwd = process.cwd();
16394
17061
  const gitRoot = getGitRoot(cwd);
16395
17062
  const projectRoot = gitRoot ?? cwd;
16396
- const configPath = join19(projectRoot, MCP_PROJECT_CONFIG_NAME);
16397
- if (!existsSync25(configPath)) return null;
17063
+ const configPath = join20(projectRoot, MCP_PROJECT_CONFIG_NAME);
17064
+ if (!existsSync26(configPath)) return null;
16398
17065
  try {
16399
- const raw = JSON.parse(readFileSync19(configPath, "utf-8"));
17066
+ const raw = JSON.parse(readFileSync20(configPath, "utf-8"));
16400
17067
  return raw.mcpServers ?? raw;
16401
17068
  } catch {
16402
17069
  return null;