jinzd-ai-cli 0.4.211 → 0.4.213

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/README.md +30 -0
  2. package/README.zh-CN.md +30 -0
  3. package/dist/{batch-DE4RXKZD.js → batch-PKY63TME.js} +2 -2
  4. package/dist/{chat-index-UBCWHBLR.js → chat-index-R2E27VXN.js} +1 -1
  5. package/dist/{chunk-JBWA73GK.js → chunk-7NTFMLJJ.js} +1 -1
  6. package/dist/{chunk-C3OU2OPF.js → chunk-FGUKBIYO.js} +2 -2
  7. package/dist/{chunk-BE6ERF7M.js → chunk-G3PW3AOB.js} +1 -1
  8. package/dist/{chunk-E5XCM4A6.js → chunk-IWLVH32D.js} +1 -1
  9. package/dist/{chunk-ZY2N2N6T.js → chunk-J35V3IAH.js} +1 -1
  10. package/dist/{chunk-UOROWTGG.js → chunk-MRGWPCFQ.js} +26 -3
  11. package/dist/{chunk-W7UKO3PS.js → chunk-OQGVGPEK.js} +5 -0
  12. package/dist/{chunk-ZOPYREL5.js → chunk-THNECAAY.js} +464 -67
  13. package/dist/{chunk-6NS6643Y.js → chunk-UDMU4FUF.js} +1 -1
  14. package/dist/{ci-XMUEX526.js → ci-HBZ6CIW5.js} +2 -2
  15. package/dist/{constants-AWTIQIWG.js → constants-CAUBTSFW.js} +1 -1
  16. package/dist/{doctor-cli-SSI6ETFT.js → doctor-cli-5Z5UWYW6.js} +4 -4
  17. package/dist/electron-server.js +794 -241
  18. package/dist/{hub-INUJND2G.js → hub-ZRADBR76.js} +1 -1
  19. package/dist/index.js +205 -19
  20. package/dist/{run-tests-PACN4UYX.js → run-tests-7J3PNWVS.js} +1 -1
  21. package/dist/{run-tests-3MHWUF43.js → run-tests-DSOJBJWA.js} +2 -2
  22. package/dist/{server-7USZJJAH.js → server-PWBWHVYB.js} +4 -4
  23. package/dist/{server-BTSKOPQI.js → server-U4I7RI42.js} +175 -10
  24. package/dist/{task-orchestrator-OV4O25MX.js → task-orchestrator-RUXWRMQV.js} +4 -4
  25. package/dist/{usage-T2P6FTE7.js → usage-2P6TIBTN.js} +2 -2
  26. package/dist/web/client/app.js +1 -1
  27. package/package.json +1 -1
@@ -37,7 +37,7 @@ import {
37
37
  VERSION,
38
38
  buildUserIdentityPrompt,
39
39
  runTestsTool
40
- } from "./chunk-ZY2N2N6T.js";
40
+ } from "./chunk-J35V3IAH.js";
41
41
  import {
42
42
  hasSemanticIndex,
43
43
  semanticSearch
@@ -49,8 +49,9 @@ import "./chunk-IEQAE3QG.js";
49
49
  import {
50
50
  loadChatIndex,
51
51
  redactJson,
52
+ scanString,
52
53
  searchChatMemory
53
- } from "./chunk-W7UKO3PS.js";
54
+ } from "./chunk-OQGVGPEK.js";
54
55
  import "./chunk-JV5N65KN.js";
55
56
  import {
56
57
  atomicWriteFileSync
@@ -60,8 +61,8 @@ import {
60
61
  import express from "express";
61
62
  import { createServer } from "http";
62
63
  import { WebSocketServer } from "ws";
63
- import { join as join19, dirname as dirname6, resolve as resolve8, relative as relative4, sep as sep3 } from "path";
64
- import { existsSync as existsSync25, readFileSync as readFileSync19, readdirSync as readdirSync12, statSync as statSync10, realpathSync } from "fs";
64
+ import { join as join21, dirname as dirname6, resolve as resolve9, relative as relative4, sep as sep3 } from "path";
65
+ import { existsSync as existsSync27, readFileSync as readFileSync21, readdirSync as readdirSync12, statSync as statSync10, realpathSync } from "fs";
65
66
  import { networkInterfaces } from "os";
66
67
 
67
68
  // src/config/config-manager.ts
@@ -208,10 +209,33 @@ var ConfigSchema = z.object({
208
209
  env: z.record(z.string()).optional(),
209
210
  timeout: z.number().default(3e4)
210
211
  })).default({}),
211
- // 工具执行钩子(shell 命令,模板变量:{tool} {dangerLevel} {args} {status})
212
+ // Hooks 生命周期(v0.4.212+):兼容旧 pre/post 字符串,同时支持 events.<EventName> 结构化 JSON hook。
212
213
  hooks: z.object({
214
+ enabled: z.boolean().default(true),
213
215
  preToolExecution: z.string().optional(),
214
- postToolExecution: z.string().optional()
216
+ postToolExecution: z.string().optional(),
217
+ events: z.record(z.union([
218
+ z.string(),
219
+ z.object({
220
+ command: z.string(),
221
+ source: z.enum(["user", "project", "managed"]).default("user"),
222
+ description: z.string().optional(),
223
+ required: z.boolean().default(false),
224
+ timeoutMs: z.number().int().min(100).max(3e4).default(5e3),
225
+ disabled: z.boolean().default(false)
226
+ }),
227
+ z.array(z.union([
228
+ z.string(),
229
+ z.object({
230
+ command: z.string(),
231
+ source: z.enum(["user", "project", "managed"]).default("user"),
232
+ description: z.string().optional(),
233
+ required: z.boolean().default(false),
234
+ timeoutMs: z.number().int().min(100).max(3e4).default(5e3),
235
+ disabled: z.boolean().default(false)
236
+ })
237
+ ]))
238
+ ])).default({})
215
239
  }).optional(),
216
240
  // 网络访问治理(v0.4.211+):默认关闭以保持兼容;开启后统一治理 web/search/MCP/shell 网络出口。
217
241
  networkPolicy: z.object({
@@ -5761,12 +5785,12 @@ ${content}`;
5761
5785
  };
5762
5786
 
5763
5787
  // src/tools/builtin/write-file.ts
5764
- import { appendFileSync, mkdirSync as mkdirSync4 } from "fs";
5788
+ import { appendFileSync, mkdirSync as mkdirSync5 } from "fs";
5765
5789
  import { dirname as dirname3 } from "path";
5766
5790
 
5767
5791
  // src/tools/executor.ts
5768
5792
  import chalk3 from "chalk";
5769
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
5793
+ import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
5770
5794
  import { tmpdir } from "os";
5771
5795
 
5772
5796
  // src/core/readline-internal.ts
@@ -5974,6 +5998,139 @@ function simpleDiff(oldLines, newLines) {
5974
5998
 
5975
5999
  // src/tools/hooks.ts
5976
6000
  import { execSync } from "child_process";
6001
+ import { createHash } from "crypto";
6002
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync5 } from "fs";
6003
+ import { join as join3 } from "path";
6004
+ var TRUST_FILE = "hooks-trust.json";
6005
+ var DEFAULT_TIMEOUT_MS = 5e3;
6006
+ function asArray(entry) {
6007
+ if (!entry) return [];
6008
+ const raw = Array.isArray(entry) ? entry : [entry];
6009
+ return raw.map((item) => typeof item === "string" ? { command: item } : item);
6010
+ }
6011
+ function hookTrustPath(configDir) {
6012
+ return join3(configDir, TRUST_FILE);
6013
+ }
6014
+ function loadHookTrustStore(configDir) {
6015
+ const file = hookTrustPath(configDir);
6016
+ if (!existsSync6(file)) return { records: [] };
6017
+ try {
6018
+ const parsed = JSON.parse(readFileSync5(file, "utf-8"));
6019
+ return { records: Array.isArray(parsed.records) ? parsed.records : [] };
6020
+ } catch {
6021
+ return { records: [] };
6022
+ }
6023
+ }
6024
+ function hashHook(event, command) {
6025
+ return createHash("sha256").update(`${event}
6026
+ ${command}`, "utf-8").digest("hex");
6027
+ }
6028
+ function isTrusted(source, event, hash, store) {
6029
+ if (source === "user" || source === "managed") return true;
6030
+ return store.records.some((r) => r.source === source && r.event === event && r.hash === hash);
6031
+ }
6032
+ function listHooks(config, configDir) {
6033
+ if (!config || config.enabled === false) return [];
6034
+ const store = loadHookTrustStore(configDir);
6035
+ const out = [];
6036
+ const push = (event, hook, index) => {
6037
+ if (!hook.command) return;
6038
+ const source = hook.source ?? "user";
6039
+ const hash = hashHook(event, hook.command);
6040
+ out.push({
6041
+ id: `${event}:${index}`,
6042
+ event,
6043
+ command: hook.command,
6044
+ source,
6045
+ hash,
6046
+ trusted: isTrusted(source, event, hash, store),
6047
+ disabled: hook.disabled === true,
6048
+ description: hook.description
6049
+ });
6050
+ };
6051
+ if (config.preToolExecution) push("PreToolUse", { command: config.preToolExecution }, 0);
6052
+ if (config.postToolExecution) push("PostToolUse", { command: config.postToolExecution }, 0);
6053
+ for (const [event, entry] of Object.entries(config.events ?? {})) {
6054
+ asArray(entry).forEach((hook, i) => push(event, hook, i));
6055
+ }
6056
+ return out;
6057
+ }
6058
+ function getPendingHookTrust(config, configDir) {
6059
+ return listHooks(config, configDir).filter((h) => h.source === "project" && !h.disabled && !h.trusted);
6060
+ }
6061
+ function normalizeDecision(value, hook) {
6062
+ if (!value || typeof value !== "object") return null;
6063
+ const obj = value;
6064
+ const action = obj.action;
6065
+ if (action !== "allow" && action !== "deny" && action !== "ask") return null;
6066
+ return {
6067
+ action,
6068
+ reason: typeof obj.reason === "string" ? obj.reason : void 0,
6069
+ prompt: typeof obj.prompt === "string" ? obj.prompt : void 0,
6070
+ contextAppend: typeof obj.contextAppend === "string" ? obj.contextAppend : void 0,
6071
+ warning: typeof obj.warning === "string" ? obj.warning : void 0,
6072
+ warnings: Array.isArray(obj.warnings) ? obj.warnings.filter((x) => typeof x === "string") : void 0,
6073
+ hook
6074
+ };
6075
+ }
6076
+ function runStructuredHook(hook, descriptor, payload) {
6077
+ if (hook.disabled === true) return null;
6078
+ const timeout = Math.max(100, Math.min(hook.timeoutMs ?? DEFAULT_TIMEOUT_MS, 3e4));
6079
+ try {
6080
+ const stdout = execSync(hook.command, {
6081
+ timeout,
6082
+ stdio: ["pipe", "pipe", "pipe"],
6083
+ encoding: "utf-8",
6084
+ env: {
6085
+ ...process.env,
6086
+ AICLI_HOOK_EVENT: payload.event,
6087
+ AICLI_HOOK_EVENT_JSON: JSON.stringify(payload)
6088
+ }
6089
+ }).trim();
6090
+ if (!stdout) return null;
6091
+ return normalizeDecision(JSON.parse(stdout), descriptor);
6092
+ } catch (err) {
6093
+ process.stderr.write(`\u26A0 Hook failed: ${hook.command.slice(0, 100)}
6094
+ `);
6095
+ if (hook.required) {
6096
+ return { action: "deny", reason: err instanceof Error ? err.message : String(err), hook: descriptor };
6097
+ }
6098
+ return null;
6099
+ }
6100
+ }
6101
+ function runLifecycleHooks(config, event, payload, opts) {
6102
+ if (!config || config.enabled === false) return [];
6103
+ const options = typeof opts === "string" ? { configDir: opts } : opts;
6104
+ const store = loadHookTrustStore(options.configDir);
6105
+ const hooks = asArray(config.events?.[event]);
6106
+ const decisions = [];
6107
+ for (let i = 0; i < hooks.length; i++) {
6108
+ const hook = hooks[i];
6109
+ if (!hook.command || hook.disabled) continue;
6110
+ const source = hook.source ?? "user";
6111
+ const hash = hashHook(event, hook.command);
6112
+ const descriptor = {
6113
+ id: `${event}:${i}`,
6114
+ event,
6115
+ command: hook.command,
6116
+ source,
6117
+ hash,
6118
+ trusted: isTrusted(source, event, hash, store),
6119
+ disabled: false,
6120
+ description: hook.description
6121
+ };
6122
+ if (!descriptor.trusted) {
6123
+ options.onSummary?.(`hook skipped: ${event} project hook requires trust (${hash.slice(0, 12)})`);
6124
+ continue;
6125
+ }
6126
+ const decision = runStructuredHook(hook, descriptor, { event, ...payload });
6127
+ if (decision) {
6128
+ decisions.push(decision);
6129
+ options.onSummary?.(`hook ${event}: ${decision.action}${decision.reason ? ` (${decision.reason})` : ""}`);
6130
+ }
6131
+ }
6132
+ return decisions;
6133
+ }
5977
6134
  function rewriteTemplate(template, isWindows) {
5978
6135
  const ref = (name) => isWindows ? `%${name}%` : `$${name}`;
5979
6136
  return template.replace(/\{tool\}/g, ref("AICLI_HOOK_TOOL")).replace(/\{dangerLevel\}/g, ref("AICLI_HOOK_DANGER_LEVEL")).replace(/\{args\}/g, ref("AICLI_HOOK_ARGS")).replace(/\{status\}/g, ref("AICLI_HOOK_STATUS"));
@@ -5984,7 +6141,7 @@ function runHook(template, vars) {
5984
6141
  const cmd = rewriteTemplate(template, isWindows);
5985
6142
  try {
5986
6143
  execSync(cmd, {
5987
- timeout: 5e3,
6144
+ timeout: DEFAULT_TIMEOUT_MS,
5988
6145
  stdio: ["pipe", "pipe", "pipe"],
5989
6146
  encoding: "utf-8",
5990
6147
  env: {
@@ -6384,8 +6541,8 @@ var theme = new Proxy(DARK_THEME, {
6384
6541
  });
6385
6542
 
6386
6543
  // src/diagnostics/tool-stats.ts
6387
- import { existsSync as existsSync6, readFileSync as readFileSync5, mkdirSync as mkdirSync3 } from "fs";
6388
- import { join as join3, dirname as dirname2 } from "path";
6544
+ import { existsSync as existsSync7, readFileSync as readFileSync6, mkdirSync as mkdirSync4 } from "fs";
6545
+ import { join as join4, dirname as dirname2 } from "path";
6389
6546
  import { homedir as homedir3 } from "os";
6390
6547
  var STATS_FILE_NAME = "tool-stats.json";
6391
6548
  var STATS_VERSION = 1;
@@ -6395,16 +6552,16 @@ var dirty = false;
6395
6552
  var pendingWrites = 0;
6396
6553
  var configDirOverride;
6397
6554
  function statsFilePath() {
6398
- const base = configDirOverride ?? join3(homedir3(), CONFIG_DIR_NAME);
6399
- return join3(base, STATS_FILE_NAME);
6555
+ const base = configDirOverride ?? join4(homedir3(), CONFIG_DIR_NAME);
6556
+ return join4(base, STATS_FILE_NAME);
6400
6557
  }
6401
6558
  function load() {
6402
6559
  const path3 = statsFilePath();
6403
- if (!existsSync6(path3)) {
6560
+ if (!existsSync7(path3)) {
6404
6561
  return { version: STATS_VERSION, startedAt: (/* @__PURE__ */ new Date()).toISOString(), entries: {} };
6405
6562
  }
6406
6563
  try {
6407
- const raw = JSON.parse(readFileSync5(path3, "utf-8"));
6564
+ const raw = JSON.parse(readFileSync6(path3, "utf-8"));
6408
6565
  if (raw.version !== STATS_VERSION || !raw.entries) {
6409
6566
  return { version: STATS_VERSION, startedAt: (/* @__PURE__ */ new Date()).toISOString(), entries: {} };
6410
6567
  }
@@ -6462,7 +6619,7 @@ function flush() {
6462
6619
  if (!dirty || state === null) return;
6463
6620
  const path3 = statsFilePath();
6464
6621
  try {
6465
- mkdirSync3(dirname2(path3), { recursive: true });
6622
+ mkdirSync4(dirname2(path3), { recursive: true });
6466
6623
  atomicWriteFileSync(path3, JSON.stringify(state, null, 2));
6467
6624
  dirty = false;
6468
6625
  pendingWrites = 0;
@@ -6470,6 +6627,157 @@ function flush() {
6470
6627
  }
6471
6628
  }
6472
6629
 
6630
+ // src/tools/action-classifier.ts
6631
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
6632
+ import { isAbsolute as isAbsolute2, join as join5, resolve as resolve4 } from "path";
6633
+ var RECENT_DENIED_LIMIT = 50;
6634
+ var recentlyDenied = [];
6635
+ function recordRecentlyDeniedAutoAction(call, classification) {
6636
+ recentlyDenied.unshift({
6637
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6638
+ tool: call.name,
6639
+ reason: classification.reason,
6640
+ ruleId: classification.ruleId,
6641
+ argsPreview: JSON.stringify(call.arguments).slice(0, 240)
6642
+ });
6643
+ recentlyDenied.splice(RECENT_DENIED_LIMIT);
6644
+ }
6645
+ function getRecentlyDeniedAutoActions() {
6646
+ return [...recentlyDenied];
6647
+ }
6648
+ function clearRecentlyDeniedAutoActions() {
6649
+ recentlyDenied.splice(0, recentlyDenied.length);
6650
+ }
6651
+ function normalizePath(value) {
6652
+ return value.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
6653
+ }
6654
+ function isInside(root, target) {
6655
+ const absoluteRoot = resolve4(root);
6656
+ const absoluteTarget = isAbsolute2(target) ? target : resolve4(root, target);
6657
+ const base = normalizePath(absoluteRoot);
6658
+ const t = normalizePath(absoluteTarget);
6659
+ return t === base || t.startsWith(base + "/");
6660
+ }
6661
+ function getPathArg(call) {
6662
+ const raw = call.arguments.path ?? call.arguments.filePath ?? call.arguments.outputPath ?? call.arguments.file;
6663
+ return typeof raw === "string" && raw.trim() ? raw.trim() : null;
6664
+ }
6665
+ function splitShellWords(command) {
6666
+ const out = [];
6667
+ const re = /"([^"]*)"|'([^']*)'|([^\s]+)/g;
6668
+ let m;
6669
+ while (m = re.exec(command)) out.push(m[1] ?? m[2] ?? m[3] ?? "");
6670
+ return out.filter(Boolean);
6671
+ }
6672
+ function readPackageManifest(root) {
6673
+ const p = join5(root, "package.json");
6674
+ if (!existsSync8(p)) return null;
6675
+ try {
6676
+ return JSON.parse(readFileSync7(p, "utf-8"));
6677
+ } catch {
6678
+ return null;
6679
+ }
6680
+ }
6681
+ function manifestHasDependency(root, name) {
6682
+ const manifest = readPackageManifest(root);
6683
+ if (!manifest) return false;
6684
+ for (const key of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
6685
+ const deps = manifest[key];
6686
+ if (deps && typeof deps === "object" && Object.prototype.hasOwnProperty.call(deps, name)) return true;
6687
+ }
6688
+ return false;
6689
+ }
6690
+ function lockfileMentions(root, name) {
6691
+ for (const file of ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"]) {
6692
+ const p = join5(root, file);
6693
+ if (!existsSync8(p)) continue;
6694
+ try {
6695
+ if (readFileSync7(p, "utf-8").includes(name)) return true;
6696
+ } catch {
6697
+ }
6698
+ }
6699
+ return false;
6700
+ }
6701
+ function getInstallPackages(command) {
6702
+ const words = splitShellWords(command);
6703
+ const cmd = words[0]?.toLowerCase();
6704
+ const sub = words[1]?.toLowerCase();
6705
+ if (!cmd || !sub) return [];
6706
+ const isInstall = cmd === "npm" && (sub === "install" || sub === "i" || sub === "add") || (cmd === "pnpm" || cmd === "yarn") && (sub === "add" || sub === "install");
6707
+ if (!isInstall) return [];
6708
+ return words.slice(2).filter((w) => !w.startsWith("-")).filter((w) => w !== "." && w !== "./").map((w) => w.replace(/^@([^/]+)\/([^@]+)(?:@.+)?$/, "@$1/$2").replace(/^([^@]+)@.+$/, "$1"));
6709
+ }
6710
+ function isPlainGitPush(command) {
6711
+ if (!/^\s*git\s+push\b/i.test(command)) return false;
6712
+ if (/--force|-f\b|\+[^\s]+/i.test(command)) return false;
6713
+ return !/[;&|`$<>\n\r]/.test(command);
6714
+ }
6715
+ function hasSecretExfiltrationShape(command) {
6716
+ return /(?:\.env|config\.json|id_rsa|id_ed25519|AWS_SECRET|AICLI_API_KEY|OPENAI_API_KEY|ANTHROPIC_API_KEY)/i.test(command) && /\b(curl|wget|scp|rsync|nc|netcat|Invoke-WebRequest|Invoke-RestMethod)\b/i.test(command);
6717
+ }
6718
+ var RuleBasedActionClassifier = class {
6719
+ classify(call, dangerLevel, ctx) {
6720
+ if (call.name === "bash") return this.classifyShell(call, dangerLevel, ctx);
6721
+ if (call.name === "web_fetch" || call.name === "web_search" || call.name === "google_search") {
6722
+ return { decision: "auto-approve", reason: "read-only HTTP tool", ruleId: "auto.readonly-http", confidence: "high" };
6723
+ }
6724
+ if (dangerLevel === "safe") {
6725
+ return { decision: "auto-approve", reason: "safe read-only tool", ruleId: "auto.safe-tool", confidence: "high" };
6726
+ }
6727
+ if (dangerLevel === "write") {
6728
+ const p = getPathArg(call);
6729
+ if (p && (isInside(ctx.workspaceRoot, p) || (ctx.tempDirs ?? []).some((root) => isInside(root, p)))) {
6730
+ return { decision: "auto-approve", reason: "explicit write inside workspace/temp roots", ruleId: "auto.workspace-write", confidence: "high" };
6731
+ }
6732
+ return { decision: "confirm", reason: "write action is not proven low-risk", ruleId: "auto.write-confirm", confidence: "medium" };
6733
+ }
6734
+ return { decision: "confirm", reason: "destructive actions require confirmation", ruleId: "auto.destructive-confirm", confidence: "high" };
6735
+ }
6736
+ classifyShell(call, dangerLevel, ctx) {
6737
+ const command = String(call.arguments.command ?? "");
6738
+ if (/\bcurl\b[^\n|;&]*\|\s*(?:sudo\s+)?(?:bash|sh|zsh)\b/i.test(command)) {
6739
+ return { decision: "deny", reason: "curl pipe shell execution is blocked in auto mode", ruleId: "deny.curl-pipe-shell", confidence: "high" };
6740
+ }
6741
+ if (/\bgit\s+push\b/i.test(command) && /--force|-f\b|\+[^\s]+/i.test(command)) {
6742
+ return { decision: "deny", reason: "force push is blocked in auto mode", ruleId: "deny.force-push", confidence: "high" };
6743
+ }
6744
+ if (hasSecretExfiltrationShape(command)) {
6745
+ return { decision: "deny", reason: "possible secret exfiltration is blocked in auto mode", ruleId: "deny.secret-exfiltration", confidence: "high" };
6746
+ }
6747
+ if (/\b(terraform|tofu)\s+destroy\b|\bkubectl\s+delete\b|\bcdk\s+destroy\b/i.test(command)) {
6748
+ return { decision: "confirm", reason: "infrastructure destroy requires confirmation", ruleId: "confirm.iac-destroy", confidence: "high" };
6749
+ }
6750
+ if (/\b(prisma|sequelize|knex|typeorm)\b[^\n]*\b(migrate|migration)\b|\bdb:migrate\b/i.test(command)) {
6751
+ return { decision: "confirm", reason: "database migration requires confirmation", ruleId: "confirm.database-migration", confidence: "high" };
6752
+ }
6753
+ if (/\b(vercel|netlify|flyctl|railway|wrangler|serverless|firebase)\b[^\n]*\bdeploy\b|\bdeploy\b[^\n]*\b(prod|production)\b/i.test(command)) {
6754
+ return { decision: "confirm", reason: "production deployment requires confirmation", ruleId: "confirm.production-deploy", confidence: "high" };
6755
+ }
6756
+ if (/\b(iam|policy|token|secret|api[-_]?key|credential|permission|role)\b/i.test(command)) {
6757
+ return { decision: "confirm", reason: "credential or permission change requires confirmation", ruleId: "confirm.credentials-permissions", confidence: "medium" };
6758
+ }
6759
+ if (/\b(npx|pnpm\s+dlx|yarn\s+dlx)\s+(claude|codex|aider|cursor-agent|gemini)\b/i.test(command)) {
6760
+ return { decision: "confirm", reason: "third-party agent loop requires confirmation", ruleId: "confirm.third-party-agent", confidence: "high" };
6761
+ }
6762
+ if (isPlainGitPush(command)) {
6763
+ return { decision: "auto-approve", reason: "plain git push without force", ruleId: "auto.git-push", confidence: "high" };
6764
+ }
6765
+ const pkgs = getInstallPackages(command);
6766
+ if (pkgs.length > 0) {
6767
+ const allDeclared = pkgs.every((p) => manifestHasDependency(ctx.workspaceRoot, p) || lockfileMentions(ctx.workspaceRoot, p));
6768
+ return allDeclared ? { decision: "auto-approve", reason: "installing dependencies already declared in manifest/lockfile", ruleId: "auto.declared-dependency-install", confidence: "medium" } : { decision: "confirm", reason: "dependency install includes undeclared packages", ruleId: "confirm.undeclared-dependency-install", confidence: "medium" };
6769
+ }
6770
+ if (/\b(curl|wget|Invoke-WebRequest|Invoke-RestMethod)\b/i.test(command) && !/[>|;&`$]/.test(command)) {
6771
+ return { decision: "auto-approve", reason: "read-only HTTP shell command", ruleId: "auto.readonly-http-shell", confidence: "medium" };
6772
+ }
6773
+ if (dangerLevel === "safe") {
6774
+ return { decision: "auto-approve", reason: "safe shell action", ruleId: "auto.safe-shell", confidence: "medium" };
6775
+ }
6776
+ return { decision: "confirm", reason: "shell action is not proven low-risk", ruleId: "confirm.shell-default", confidence: "medium" };
6777
+ }
6778
+ };
6779
+ var defaultActionClassifier = new RuleBasedActionClassifier();
6780
+
6473
6781
  // src/tools/executor.ts
6474
6782
  function buildErrorPreview(content) {
6475
6783
  const meaningful = content.split("\n").filter(
@@ -6509,6 +6817,8 @@ var ToolExecutor = class {
6509
6817
  * destructive 操作仍然必须逐次确认。
6510
6818
  */
6511
6819
  sessionAutoApprove = false;
6820
+ /** Session-level Auto Mode: rule-based low-risk auto approval. */
6821
+ sessionAutoMode = false;
6512
6822
  /**
6513
6823
  * Logical session key used to scope per-session state in stateful tools
6514
6824
  * (currently only `bash`'s persistent cwd). Web mode sets this per-tab so
@@ -6549,6 +6859,7 @@ var ToolExecutor = class {
6549
6859
  allowedPermissionProfiles = [];
6550
6860
  workspaceRoot = process.cwd();
6551
6861
  networkPolicy;
6862
+ hookConfigDir = process.cwd();
6552
6863
  /** 注入 hooks 和 permission rules 配置 */
6553
6864
  setConfig(opts) {
6554
6865
  this.hookConfig = opts.hookConfig;
@@ -6559,6 +6870,7 @@ var ToolExecutor = class {
6559
6870
  if (opts.allowedPermissionProfiles) this.allowedPermissionProfiles = opts.allowedPermissionProfiles;
6560
6871
  if (opts.workspaceRoot) this.workspaceRoot = opts.workspaceRoot;
6561
6872
  if (opts.networkPolicy) this.networkPolicy = opts.networkPolicy;
6873
+ if (opts.hookConfigDir) this.hookConfigDir = opts.hookConfigDir;
6562
6874
  }
6563
6875
  async execute(call) {
6564
6876
  return runWithSessionKey(this.sessionKey, () => this.executeInner(call));
@@ -6579,6 +6891,24 @@ var ToolExecutor = class {
6579
6891
  dangerLevel,
6580
6892
  args: JSON.stringify(call.arguments).slice(0, 200)
6581
6893
  });
6894
+ const preToolDecisions = runLifecycleHooks(
6895
+ this.hookConfig,
6896
+ "PreToolUse",
6897
+ { tool: call.name, dangerLevel, args: call.arguments },
6898
+ { configDir: this.hookConfigDir, onSummary: (msg) => console.log(theme.dim(` [hook] ${msg}`)) }
6899
+ );
6900
+ const preDeny = preToolDecisions.find((d) => d.action === "deny");
6901
+ if (preDeny) {
6902
+ return {
6903
+ callId: call.id,
6904
+ content: `[Hook denied] ${preDeny.reason ?? `PreToolUse blocked ${call.name}`}. Do not retry without asking.`,
6905
+ isError: true
6906
+ };
6907
+ }
6908
+ for (const d of preToolDecisions) {
6909
+ const warnings = [d.warning, ...d.warnings ?? []].filter(Boolean);
6910
+ for (const w of warnings) console.log(theme.warning(` \u26A0 Hook warning: ${w}`));
6911
+ }
6582
6912
  const permission = checkPermissionWithProfile(
6583
6913
  call.name,
6584
6914
  call.arguments,
@@ -6594,6 +6924,14 @@ var ToolExecutor = class {
6594
6924
  }
6595
6925
  );
6596
6926
  const networkPermission = checkNetworkPolicy(call.name, call.arguments, this.networkPolicy);
6927
+ if (networkPermission?.action === "confirm" || permission.action === "confirm") {
6928
+ runLifecycleHooks(
6929
+ this.hookConfig,
6930
+ "PermissionRequest",
6931
+ { tool: call.name, dangerLevel, args: call.arguments, reason: networkPermission?.reason ?? permission.reason },
6932
+ { configDir: this.hookConfigDir, onSummary: (msg) => console.log(theme.dim(" [hook] " + msg)) }
6933
+ );
6934
+ }
6597
6935
  if (networkPermission?.action === "deny") {
6598
6936
  const reason = networkPermission.reason ? ` (${networkPermission.reason})` : "";
6599
6937
  return {
@@ -6618,11 +6956,13 @@ var ToolExecutor = class {
6618
6956
  const wasTruncated = content !== rawContent;
6619
6957
  this.printToolResult(call.name, rawContent, false, wasTruncated);
6620
6958
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
6959
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
6621
6960
  return { callId: call.id, content, isError: false };
6622
6961
  } catch (err) {
6623
6962
  const message = err instanceof Error ? err.message : String(err);
6624
6963
  this.printToolResult(call.name, message, true, false);
6625
6964
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
6965
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
6626
6966
  return { callId: call.id, content: message, isError: true };
6627
6967
  }
6628
6968
  }
@@ -6638,6 +6978,40 @@ var ToolExecutor = class {
6638
6978
  };
6639
6979
  }
6640
6980
  }
6981
+ if (this.sessionAutoMode && networkPermission?.action !== "confirm") {
6982
+ const classification = defaultActionClassifier.classify(call, dangerLevel, {
6983
+ workspaceRoot: this.workspaceRoot,
6984
+ tempDirs: [tmpdir()]
6985
+ });
6986
+ if (classification.decision === "deny") {
6987
+ recordRecentlyDeniedAutoAction(call, classification);
6988
+ return {
6989
+ callId: call.id,
6990
+ content: `[Auto denied] ${classification.reason}. Do not retry without asking.`,
6991
+ isError: true
6992
+ };
6993
+ }
6994
+ if (classification.decision === "auto-approve") {
6995
+ this.printToolCall(call);
6996
+ if (dangerLevel === "write") this.printDiffPreview(call);
6997
+ console.log(theme.warning(` \u26A1 Auto-approved (/auto: ${classification.ruleId})`));
6998
+ try {
6999
+ const rawContent = await runTool(tool, call.arguments, call.name);
7000
+ const content = truncateOutput(rawContent, call.name);
7001
+ const wasTruncated = content !== rawContent;
7002
+ this.printToolResult(call.name, rawContent, false, wasTruncated);
7003
+ runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
7004
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
7005
+ return { callId: call.id, content, isError: false };
7006
+ } catch (err) {
7007
+ const message = err instanceof Error ? err.message : String(err);
7008
+ this.printToolResult(call.name, message, true, false);
7009
+ runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
7010
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
7011
+ return { callId: call.id, content: message, isError: true };
7012
+ }
7013
+ }
7014
+ }
6641
7015
  if (this.sessionAutoApprove && dangerLevel === "write") {
6642
7016
  this.printToolCall(call);
6643
7017
  if (dangerLevel === "write") this.printDiffPreview(call);
@@ -6648,11 +7022,13 @@ var ToolExecutor = class {
6648
7022
  const wasTruncated = content !== rawContent;
6649
7023
  this.printToolResult(call.name, rawContent, false, wasTruncated);
6650
7024
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
7025
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
6651
7026
  return { callId: call.id, content, isError: false };
6652
7027
  } catch (err) {
6653
7028
  const message = err instanceof Error ? err.message : String(err);
6654
7029
  this.printToolResult(call.name, message, true, false);
6655
7030
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
7031
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
6656
7032
  return { callId: call.id, content: message, isError: true };
6657
7033
  }
6658
7034
  }
@@ -6686,11 +7062,13 @@ var ToolExecutor = class {
6686
7062
  const wasTruncated = content !== rawContent;
6687
7063
  this.printToolResult(call.name, rawContent, false, wasTruncated);
6688
7064
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
7065
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
6689
7066
  return { callId: call.id, content, isError: false };
6690
7067
  } catch (err) {
6691
7068
  const message = err instanceof Error ? err.message : String(err);
6692
7069
  this.printToolResult(call.name, message, true, false);
6693
7070
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
7071
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
6694
7072
  return { callId: call.id, content: message, isError: true };
6695
7073
  }
6696
7074
  }
@@ -6801,7 +7179,7 @@ var ToolExecutor = class {
6801
7179
  rl.resume();
6802
7180
  process.stdout.write(prompt);
6803
7181
  this.confirming = true;
6804
- return new Promise((resolve9) => {
7182
+ return new Promise((resolve10) => {
6805
7183
  let completed = false;
6806
7184
  const cleanup = (result) => {
6807
7185
  if (completed) return;
@@ -6811,7 +7189,7 @@ var ToolExecutor = class {
6811
7189
  rl.pause();
6812
7190
  rlAny.output = savedOutput;
6813
7191
  this.confirming = false;
6814
- resolve9(result);
7192
+ resolve10(result);
6815
7193
  };
6816
7194
  const onLine = (line) => {
6817
7195
  const trimmed = line.trim();
@@ -6878,10 +7256,10 @@ var ToolExecutor = class {
6878
7256
  const filePath = String(call.arguments["path"] ?? "");
6879
7257
  const newContent = String(call.arguments["content"] ?? "");
6880
7258
  if (!filePath) return;
6881
- if (existsSync7(filePath)) {
7259
+ if (existsSync9(filePath)) {
6882
7260
  let oldContent;
6883
7261
  try {
6884
- oldContent = readFileSync6(filePath, "utf-8");
7262
+ oldContent = readFileSync8(filePath, "utf-8");
6885
7263
  } catch {
6886
7264
  return;
6887
7265
  }
@@ -6904,7 +7282,7 @@ var ToolExecutor = class {
6904
7282
  }
6905
7283
  } else if (call.name === "edit_file") {
6906
7284
  const filePath = String(call.arguments["path"] ?? "");
6907
- if (!filePath || !existsSync7(filePath)) return;
7285
+ if (!filePath || !existsSync9(filePath)) return;
6908
7286
  const oldStr = call.arguments["old_str"];
6909
7287
  const newStr = call.arguments["new_str"];
6910
7288
  if (oldStr !== void 0) {
@@ -6931,7 +7309,7 @@ var ToolExecutor = class {
6931
7309
  const to = Number(call.arguments["delete_to_line"] ?? from);
6932
7310
  let fileContent;
6933
7311
  try {
6934
- fileContent = readFileSync6(filePath, "utf-8");
7312
+ fileContent = readFileSync8(filePath, "utf-8");
6935
7313
  } catch {
6936
7314
  return;
6937
7315
  }
@@ -6985,7 +7363,7 @@ var ToolExecutor = class {
6985
7363
  rl.resume();
6986
7364
  process.stdout.write(color("Proceed? [y/N] (type y + Enter to confirm) "));
6987
7365
  this.confirming = true;
6988
- return new Promise((resolve9) => {
7366
+ return new Promise((resolve10) => {
6989
7367
  let completed = false;
6990
7368
  const cleanup = (answer) => {
6991
7369
  if (completed) return;
@@ -6995,7 +7373,7 @@ var ToolExecutor = class {
6995
7373
  rl.pause();
6996
7374
  rlAny.output = savedOutput;
6997
7375
  this.confirming = false;
6998
- resolve9(answer === "y");
7376
+ resolve10(answer === "y");
6999
7377
  };
7000
7378
  const onLine = (line) => {
7001
7379
  const trimmed = line.trim();
@@ -7023,11 +7401,11 @@ var ToolExecutor = class {
7023
7401
  };
7024
7402
 
7025
7403
  // src/tools/sensitive-paths.ts
7026
- import { resolve as resolve4, sep as sep2, basename as basename2 } from "path";
7404
+ import { resolve as resolve5, sep as sep2, basename as basename2 } from "path";
7027
7405
  import { homedir as homedir4 } from "os";
7028
7406
  var home = homedir4();
7029
7407
  function norm(p) {
7030
- const abs = resolve4(p);
7408
+ const abs = resolve5(p);
7031
7409
  return process.platform === "win32" ? abs.toLowerCase() : abs;
7032
7410
  }
7033
7411
  function homeRel(p) {
@@ -7166,7 +7544,7 @@ Do NOT split a long document into many write_file(append=true) calls. That patte
7166
7544
  }
7167
7545
  undoStack.push(filePath, `write_file${appendMode ? " (append)" : ""}: ${filePath}`);
7168
7546
  fileCheckpoints.snapshot(filePath, ToolExecutor.currentMessageIndex);
7169
- mkdirSync4(dirname3(filePath), { recursive: true });
7547
+ mkdirSync5(dirname3(filePath), { recursive: true });
7170
7548
  if (appendMode) {
7171
7549
  appendFileSync(filePath, content, encoding);
7172
7550
  } else {
@@ -7186,7 +7564,7 @@ Do NOT split a long document into many write_file(append=true) calls. That patte
7186
7564
  };
7187
7565
 
7188
7566
  // src/tools/builtin/edit-file.ts
7189
- import { readFileSync as readFileSync7, existsSync as existsSync8 } from "fs";
7567
+ import { readFileSync as readFileSync9, existsSync as existsSync10 } from "fs";
7190
7568
 
7191
7569
  // src/tools/builtin/patch-apply.ts
7192
7570
  function parseUnifiedDiff(patch) {
@@ -7547,7 +7925,7 @@ Note: Path can be absolute or relative to cwd.`,
7547
7925
  const filePath = String(args["path"] ?? "");
7548
7926
  const encoding = args["encoding"] ?? "utf-8";
7549
7927
  if (!filePath) throw new ToolError("edit_file", "path is required");
7550
- if (!existsSync8(filePath)) throw new ToolError("edit_file", `File not found: ${filePath}`);
7928
+ if (!existsSync10(filePath)) throw new ToolError("edit_file", `File not found: ${filePath}`);
7551
7929
  const verdict = classifyWritePath(filePath);
7552
7930
  if (verdict.sensitive && subAgentGuard.active) {
7553
7931
  throw new ToolError(
@@ -7555,7 +7933,7 @@ Note: Path can be absolute or relative to cwd.`,
7555
7933
  `Refused: sub-agents cannot edit sensitive paths \u2014 ${verdict.reason}. If this is genuinely needed, ask the parent agent to perform the edit so the user can approve it.`
7556
7934
  );
7557
7935
  }
7558
- const original = readFileSync7(filePath, encoding);
7936
+ const original = readFileSync9(filePath, encoding);
7559
7937
  if (args["patch"] !== void 0) {
7560
7938
  const patchText = String(args["patch"] ?? "");
7561
7939
  const stopOnError = args["stop_on_error"] !== false;
@@ -7713,8 +8091,8 @@ function truncatePreview(str, maxLen = 80) {
7713
8091
  }
7714
8092
 
7715
8093
  // src/tools/builtin/list-dir.ts
7716
- import { readdirSync as readdirSync4, statSync as statSync3, existsSync as existsSync9 } from "fs";
7717
- import { join as join4, basename as basename3 } from "path";
8094
+ import { readdirSync as readdirSync4, statSync as statSync3, existsSync as existsSync11 } from "fs";
8095
+ import { join as join6, basename as basename3 } from "path";
7718
8096
  var listDirTool = {
7719
8097
  definition: {
7720
8098
  name: "list_dir",
@@ -7736,7 +8114,7 @@ var listDirTool = {
7736
8114
  async execute(args) {
7737
8115
  const dirPath = String(args["path"] ?? process.cwd());
7738
8116
  const recursive = Boolean(args["recursive"] ?? false);
7739
- if (!existsSync9(dirPath)) {
8117
+ if (!existsSync11(dirPath)) {
7740
8118
  const targetName = basename3(dirPath).toLowerCase();
7741
8119
  const cwd = process.cwd();
7742
8120
  const suggestions = [];
@@ -7794,11 +8172,11 @@ function listRecursive(basePath, indent, recursive, lines) {
7794
8172
  if (entry.isDirectory()) {
7795
8173
  lines.push(`${indent}\u{1F4C1} ${entry.name}/`);
7796
8174
  if (recursive) {
7797
- listRecursive(join4(basePath, entry.name), indent + " ", true, lines);
8175
+ listRecursive(join6(basePath, entry.name), indent + " ", true, lines);
7798
8176
  }
7799
8177
  } else {
7800
8178
  try {
7801
- const stat = statSync3(join4(basePath, entry.name));
8179
+ const stat = statSync3(join6(basePath, entry.name));
7802
8180
  const size = formatSize(stat.size);
7803
8181
  lines.push(`${indent}\u{1F4C4} ${entry.name} (${size})`);
7804
8182
  } catch {
@@ -7814,9 +8192,9 @@ function formatSize(bytes) {
7814
8192
  }
7815
8193
 
7816
8194
  // src/tools/builtin/grep-files.ts
7817
- import { readdirSync as readdirSync5, readFileSync as readFileSync8, statSync as statSync4, existsSync as existsSync10 } from "fs";
8195
+ import { readdirSync as readdirSync5, readFileSync as readFileSync10, statSync as statSync4, existsSync as existsSync12 } from "fs";
7818
8196
  import { readFile } from "fs/promises";
7819
- import { join as join5, relative } from "path";
8197
+ import { join as join7, relative } from "path";
7820
8198
  var grepFilesTool = {
7821
8199
  definition: {
7822
8200
  name: "grep_files",
@@ -7867,7 +8245,7 @@ Supports regex. Automatically skips node_modules, dist, .git directories.`,
7867
8245
  const contextLines = Math.max(0, Number(args["context_lines"] ?? 0));
7868
8246
  const maxResults = Math.max(1, Number(args["max_results"] ?? 50));
7869
8247
  if (!pattern) throw new ToolError("grep_files", "pattern is required");
7870
- if (!existsSync10(rootPath)) throw new ToolError("grep_files", `Path not found: ${rootPath}`);
8248
+ if (!existsSync12(rootPath)) throw new ToolError("grep_files", `Path not found: ${rootPath}`);
7871
8249
  const MAX_PATTERN_LENGTH = 1e3;
7872
8250
  if (pattern.length > MAX_PATTERN_LENGTH) {
7873
8251
  throw new ToolError("grep_files", `Pattern too long (${pattern.length} chars, max ${MAX_PATTERN_LENGTH}). Use a shorter pattern.`);
@@ -7961,11 +8339,11 @@ function collectFilePaths(rootPath, filePattern, maxFiles) {
7961
8339
  if (paths.length >= maxFiles) return;
7962
8340
  if (entry.isDirectory()) {
7963
8341
  if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
7964
- walk(join5(dirPath, entry.name));
8342
+ walk(join7(dirPath, entry.name));
7965
8343
  } else if (entry.isFile()) {
7966
8344
  if (isBinary(entry.name)) continue;
7967
8345
  if (filePattern && !matchesFilePattern(entry.name, filePattern)) continue;
7968
- const fullPath = join5(dirPath, entry.name);
8346
+ const fullPath = join7(dirPath, entry.name);
7969
8347
  try {
7970
8348
  if (statSync4(fullPath).size > 1e6) continue;
7971
8349
  } catch {
@@ -8020,7 +8398,7 @@ function searchInFile(fullPath, displayPath2, regex, contextLines, maxResults, r
8020
8398
  }
8021
8399
  let content;
8022
8400
  try {
8023
- content = readFileSync8(fullPath, "utf-8");
8401
+ content = readFileSync10(fullPath, "utf-8");
8024
8402
  } catch {
8025
8403
  return;
8026
8404
  }
@@ -8051,8 +8429,8 @@ function searchInFile(fullPath, displayPath2, regex, contextLines, maxResults, r
8051
8429
  }
8052
8430
 
8053
8431
  // src/tools/builtin/glob-files.ts
8054
- import { readdirSync as readdirSync6, statSync as statSync5, existsSync as existsSync11 } from "fs";
8055
- import { join as join6, relative as relative2, basename as basename4 } from "path";
8432
+ import { readdirSync as readdirSync6, statSync as statSync5, existsSync as existsSync13 } from "fs";
8433
+ import { join as join8, relative as relative2, basename as basename4 } from "path";
8056
8434
  var globFilesTool = {
8057
8435
  definition: {
8058
8436
  name: "glob_files",
@@ -8085,7 +8463,7 @@ Results sorted by most recent modification time. Automatically skips node_module
8085
8463
  const rootPath = String(args["path"] ?? process.cwd());
8086
8464
  const maxResults = Math.max(1, Number(args["max_results"] ?? 100));
8087
8465
  if (!pattern) throw new ToolError("glob_files", "pattern is required");
8088
- if (!existsSync11(rootPath)) throw new ToolError("glob_files", `Path not found: ${rootPath}`);
8466
+ if (!existsSync13(rootPath)) throw new ToolError("glob_files", `Path not found: ${rootPath}`);
8089
8467
  const regex = globToRegex(pattern);
8090
8468
  const matches = [];
8091
8469
  collectMatchingFiles(rootPath, rootPath, regex, matches, maxResults);
@@ -8162,7 +8540,7 @@ function collectMatchingFiles(dirPath, rootPath, regex, results, maxResults) {
8162
8540
  }
8163
8541
  for (const entry of entries) {
8164
8542
  if (results.length >= maxResults) break;
8165
- const fullPath = join6(dirPath, entry.name);
8543
+ const fullPath = join8(dirPath, entry.name);
8166
8544
  if (entry.isDirectory()) {
8167
8545
  if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith(".")) continue;
8168
8546
  collectMatchingFiles(fullPath, rootPath, regex, results, maxResults);
@@ -8244,7 +8622,7 @@ var runInteractiveTool = {
8244
8622
  PYTHONDONTWRITEBYTECODE: "1"
8245
8623
  };
8246
8624
  const prefixWarnings = [argsTypeWarning, stdinTypeWarning].filter(Boolean).join("");
8247
- return new Promise((resolve9) => {
8625
+ return new Promise((resolve10) => {
8248
8626
  const child = spawn2(executable, cmdArgs.map(String), {
8249
8627
  cwd: process.cwd(),
8250
8628
  env,
@@ -8277,22 +8655,22 @@ var runInteractiveTool = {
8277
8655
  setTimeout(writeNextLine, 400);
8278
8656
  const timer = setTimeout(() => {
8279
8657
  child.kill();
8280
- resolve9(`${prefixWarnings}[Timeout after ${timeout}ms]
8658
+ resolve10(`${prefixWarnings}[Timeout after ${timeout}ms]
8281
8659
  ${buildOutput(stdout, stderr)}`);
8282
8660
  }, timeout);
8283
8661
  child.on("close", (code) => {
8284
8662
  clearTimeout(timer);
8285
8663
  const output = buildOutput(stdout, stderr);
8286
8664
  if (code !== 0 && code !== null) {
8287
- resolve9(`${prefixWarnings}Exit code ${code}:
8665
+ resolve10(`${prefixWarnings}Exit code ${code}:
8288
8666
  ${output}`);
8289
8667
  } else {
8290
- resolve9(`${prefixWarnings}${output || "(no output)"}`);
8668
+ resolve10(`${prefixWarnings}${output || "(no output)"}`);
8291
8669
  }
8292
8670
  });
8293
8671
  child.on("error", (err) => {
8294
8672
  clearTimeout(timer);
8295
- resolve9(
8673
+ resolve10(
8296
8674
  `${prefixWarnings}Failed to start process "${executable}": ${err.message}
8297
8675
  Hint: On Windows, use the full path to the executable, e.g.:
8298
8676
  C:\\Users\\Jinzd\\anaconda3\\envs\\python312\\python.exe`
@@ -8902,7 +9280,7 @@ ${preamble}`;
8902
9280
  }
8903
9281
 
8904
9282
  // src/tools/builtin/save-last-response.ts
8905
- import { mkdirSync as mkdirSync5, unlinkSync as unlinkSync3, rmdirSync as rmdirSync2 } from "fs";
9283
+ import { mkdirSync as mkdirSync6, unlinkSync as unlinkSync3, rmdirSync as rmdirSync2 } from "fs";
8906
9284
  import { dirname as dirname4 } from "path";
8907
9285
  var lastResponseStore = { content: "" };
8908
9286
  function cleanupRejectedTeeFile(filePath) {
@@ -8952,7 +9330,7 @@ Any of these triggers means use save_last_response, NOT write_file:
8952
9330
  throw new ToolError("save_last_response", "No content to save: AI has not produced any response yet, or the last response was empty.");
8953
9331
  }
8954
9332
  undoStack.push(filePath, `save_last_response: ${filePath}`);
8955
- mkdirSync5(dirname4(filePath), { recursive: true });
9333
+ mkdirSync6(dirname4(filePath), { recursive: true });
8956
9334
  atomicWriteFileSync(filePath, content);
8957
9335
  const lines = content.split("\n").length;
8958
9336
  return `File saved: ${filePath} (${lines} lines, ${content.length} bytes)`;
@@ -8960,11 +9338,11 @@ Any of these triggers means use save_last_response, NOT write_file:
8960
9338
  };
8961
9339
 
8962
9340
  // src/tools/builtin/save-memory.ts
8963
- import { existsSync as existsSync12, readFileSync as readFileSync9, statSync as statSync6, mkdirSync as mkdirSync6 } from "fs";
8964
- import { join as join7 } from "path";
9341
+ import { existsSync as existsSync14, readFileSync as readFileSync11, statSync as statSync6, mkdirSync as mkdirSync7 } from "fs";
9342
+ import { join as join9 } from "path";
8965
9343
  import { homedir as homedir5 } from "os";
8966
9344
  function getMemoryFilePath() {
8967
- return join7(homedir5(), CONFIG_DIR_NAME, MEMORY_FILE_NAME);
9345
+ return join9(homedir5(), CONFIG_DIR_NAME, MEMORY_FILE_NAME);
8968
9346
  }
8969
9347
  function formatTimestamp() {
8970
9348
  const now = /* @__PURE__ */ new Date();
@@ -8988,16 +9366,16 @@ var saveMemoryTool = {
8988
9366
  const content = String(args["content"] ?? "").trim();
8989
9367
  if (!content) throw new ToolError("save_memory", "content is required");
8990
9368
  const memoryPath = getMemoryFilePath();
8991
- const configDir = join7(homedir5(), CONFIG_DIR_NAME);
8992
- if (!existsSync12(configDir)) {
8993
- mkdirSync6(configDir, { recursive: true });
9369
+ const configDir = join9(homedir5(), CONFIG_DIR_NAME);
9370
+ if (!existsSync14(configDir)) {
9371
+ mkdirSync7(configDir, { recursive: true });
8994
9372
  }
8995
9373
  const timestamp = formatTimestamp();
8996
9374
  const entry = `
8997
9375
  ## ${timestamp}
8998
9376
  ${content}
8999
9377
  `;
9000
- const previous = existsSync12(memoryPath) ? readFileSync9(memoryPath, "utf-8") : "";
9378
+ const previous = existsSync14(memoryPath) ? readFileSync11(memoryPath, "utf-8") : "";
9001
9379
  atomicWriteFileSync(memoryPath, previous + entry);
9002
9380
  const byteSize = statSync6(memoryPath).size;
9003
9381
  return `Memory saved successfully. File size: ${byteSize} bytes in ${MEMORY_FILE_NAME}`;
@@ -9044,7 +9422,7 @@ function promptUser(rl, question) {
9044
9422
  console.log();
9045
9423
  console.log(chalk4.cyan("\u2753 ") + chalk4.bold(question));
9046
9424
  process.stdout.write(chalk4.cyan("> "));
9047
- return new Promise((resolve9) => {
9425
+ return new Promise((resolve10) => {
9048
9426
  let completed = false;
9049
9427
  const cleanup = (answer) => {
9050
9428
  if (completed) return;
@@ -9054,7 +9432,7 @@ function promptUser(rl, question) {
9054
9432
  rl.pause();
9055
9433
  rlAny.output = savedOutput;
9056
9434
  askUserContext.prompting = false;
9057
- resolve9(answer);
9435
+ resolve10(answer);
9058
9436
  };
9059
9437
  const onLine = (line) => {
9060
9438
  cleanup(line);
@@ -9534,11 +9912,27 @@ var spawnAgentTool = {
9534
9912
  if (!ctx.provider) {
9535
9913
  throw new ToolError("spawn_agent", "provider not initialized (context not injected)");
9536
9914
  }
9915
+ const hookConfig = ctx.configManager?.get("hooks") ?? void 0;
9916
+ const hookConfigDir = ctx.configManager?.getConfigDir() ?? process.cwd();
9917
+ const taskCount = singleTask ? 1 : tasksArr.length;
9918
+ const hookPayload = {
9919
+ task: singleTask || void 0,
9920
+ tasks: tasksArr.length > 0 ? tasksArr : void 0,
9921
+ taskCount,
9922
+ maxRounds
9923
+ };
9924
+ const startDecisions = runLifecycleHooks(hookConfig, "SubagentStart", hookPayload, hookConfigDir);
9925
+ const startDeny = startDecisions.find((d) => d.action === "deny");
9926
+ if (startDeny) {
9927
+ throw new ToolError("spawn_agent", "SubagentStart hook denied: " + (startDeny.reason ?? "denied"));
9928
+ }
9929
+ let subagentStatus = "error";
9537
9930
  const guardWasActive = subAgentGuard.active;
9538
9931
  subAgentGuard.active = true;
9539
9932
  try {
9540
9933
  if (singleTask) {
9541
9934
  const { content, usage } = await runSubAgent(singleTask, maxRounds, null, ctx);
9935
+ subagentStatus = "ok";
9542
9936
  return [
9543
9937
  "## Sub-Agent Result",
9544
9938
  "",
@@ -9567,8 +9961,10 @@ var spawnAgentTool = {
9567
9961
  });
9568
9962
  lines.push("---");
9569
9963
  lines.push(`Total token usage: ${totalIn} input, ${totalOut} output`);
9964
+ subagentStatus = "ok";
9570
9965
  return lines.join("\n");
9571
9966
  } finally {
9967
+ runLifecycleHooks(hookConfig, "SubagentStop", { ...hookPayload, status: subagentStatus }, hookConfigDir);
9572
9968
  subAgentGuard.active = guardWasActive;
9573
9969
  }
9574
9970
  }
@@ -9791,14 +10187,14 @@ var taskStopTool = {
9791
10187
 
9792
10188
  // src/tools/builtin/git-tools.ts
9793
10189
  import { execFileSync as execFileSync2 } from "child_process";
9794
- import { existsSync as existsSync13 } from "fs";
9795
- import { join as join8 } from "path";
10190
+ import { existsSync as existsSync15 } from "fs";
10191
+ import { join as join10 } from "path";
9796
10192
  function assertGitRepo(cwd) {
9797
10193
  let dir = cwd;
9798
10194
  const root = dir.split(/[\\/]/)[0] + (dir.includes("\\") ? "\\" : "/");
9799
10195
  while (dir && dir !== root) {
9800
- if (existsSync13(join8(dir, ".git"))) return;
9801
- const parent = join8(dir, "..");
10196
+ if (existsSync15(join10(dir, ".git"))) return;
10197
+ const parent = join10(dir, "..");
9802
10198
  if (parent === dir) break;
9803
10199
  dir = parent;
9804
10200
  }
@@ -10063,9 +10459,9 @@ ${commitOutput.trim()}`;
10063
10459
  };
10064
10460
 
10065
10461
  // src/tools/builtin/notebook-edit.ts
10066
- import { readFileSync as readFileSync10, existsSync as existsSync14 } from "fs";
10462
+ import { readFileSync as readFileSync12, existsSync as existsSync16 } from "fs";
10067
10463
  import { writeFile } from "fs/promises";
10068
- import { resolve as resolve5, extname as extname2 } from "path";
10464
+ import { resolve as resolve6, extname as extname2 } from "path";
10069
10465
  var notebookEditTool = {
10070
10466
  definition: {
10071
10467
  name: "notebook_edit",
@@ -10114,14 +10510,14 @@ var notebookEditTool = {
10114
10510
  if (!Number.isInteger(cellIndexRaw) || cellIndexRaw < 1) {
10115
10511
  throw new ToolError("notebook_edit", "cell_index must be a positive integer (1-based)");
10116
10512
  }
10117
- const absPath = resolve5(filePath);
10513
+ const absPath = resolve6(filePath);
10118
10514
  if (extname2(absPath).toLowerCase() !== ".ipynb") {
10119
10515
  throw new ToolError("notebook_edit", "path must point to a .ipynb file");
10120
10516
  }
10121
- if (!existsSync14(absPath)) {
10517
+ if (!existsSync16(absPath)) {
10122
10518
  throw new ToolError("notebook_edit", `Notebook not found: ${filePath}`);
10123
10519
  }
10124
- const raw = readFileSync10(absPath, "utf-8");
10520
+ const raw = readFileSync12(absPath, "utf-8");
10125
10521
  const nb = parseNotebook(raw);
10126
10522
  const cellIdx0 = cellIndexRaw - 1;
10127
10523
  undoStack.push(absPath, `notebook_edit (${action}): ${filePath}`);
@@ -10538,8 +10934,8 @@ function estimateToolDefinitionTokens(def) {
10538
10934
 
10539
10935
  // src/tools/registry.ts
10540
10936
  import { pathToFileURL } from "url";
10541
- import { existsSync as existsSync15, mkdirSync as mkdirSync7, readdirSync as readdirSync7 } from "fs";
10542
- import { join as join9 } from "path";
10937
+ import { existsSync as existsSync17, mkdirSync as mkdirSync8, readdirSync as readdirSync7 } from "fs";
10938
+ import { join as join11 } from "path";
10543
10939
  var ToolRegistry = class {
10544
10940
  tools = /* @__PURE__ */ new Map();
10545
10941
  pluginToolNames = /* @__PURE__ */ new Set();
@@ -10700,9 +11096,9 @@ var ToolRegistry = class {
10700
11096
  * Returns the number of successfully loaded plugins.
10701
11097
  */
10702
11098
  async loadPlugins(pluginsDir, allowPlugins = false) {
10703
- if (!existsSync15(pluginsDir)) {
11099
+ if (!existsSync17(pluginsDir)) {
10704
11100
  try {
10705
- mkdirSync7(pluginsDir, { recursive: true });
11101
+ mkdirSync8(pluginsDir, { recursive: true });
10706
11102
  } catch {
10707
11103
  }
10708
11104
  return 0;
@@ -10726,12 +11122,12 @@ var ToolRegistry = class {
10726
11122
  process.stderr.write(
10727
11123
  `
10728
11124
  [plugins] \u26A0 Loading ${files.length} plugin(s) with FULL system privileges:
10729
- ` + files.map((f) => ` + ${join9(pluginsDir, f)}`).join("\n") + "\n\n"
11125
+ ` + files.map((f) => ` + ${join11(pluginsDir, f)}`).join("\n") + "\n\n"
10730
11126
  );
10731
11127
  let loaded = 0;
10732
11128
  for (const file of files) {
10733
11129
  try {
10734
- const fileUrl = pathToFileURL(join9(pluginsDir, file)).href;
11130
+ const fileUrl = pathToFileURL(join11(pluginsDir, file)).href;
10735
11131
  const mod = await import(fileUrl);
10736
11132
  const tool = mod.tool ?? mod.default?.tool ?? mod.default;
10737
11133
  if (!tool || typeof tool.execute !== "function" || !tool.definition?.name) {
@@ -10893,7 +11289,7 @@ var McpClient = class {
10893
11289
  // 内部方法:JSON-RPC 通信
10894
11290
  // ══════════════════════════════════════════════════════════════════
10895
11291
  sendRequest(method, params) {
10896
- return new Promise((resolve9, reject) => {
11292
+ return new Promise((resolve10, reject) => {
10897
11293
  if (!this.process?.stdin?.writable) {
10898
11294
  return reject(new Error(`MCP server [${this.serverId}] stdin not writable`));
10899
11295
  }
@@ -10917,7 +11313,7 @@ var McpClient = class {
10917
11313
  this.pendingRequests.set(id, {
10918
11314
  resolve: (result) => {
10919
11315
  cleanup();
10920
- resolve9(result);
11316
+ resolve10(result);
10921
11317
  },
10922
11318
  reject: (error) => {
10923
11319
  cleanup();
@@ -10994,13 +11390,13 @@ var McpClient = class {
10994
11390
  }
10995
11391
  /** Promise 超时包装 */
10996
11392
  withTimeout(promise, ms, label) {
10997
- return new Promise((resolve9, reject) => {
11393
+ return new Promise((resolve10, reject) => {
10998
11394
  const timer = setTimeout(() => {
10999
11395
  reject(new Error(`MCP [${this.serverId}] ${label} timed out after ${ms}ms`));
11000
11396
  }, ms);
11001
11397
  promise.then((val) => {
11002
11398
  clearTimeout(timer);
11003
- resolve9(val);
11399
+ resolve10(val);
11004
11400
  }).catch((err) => {
11005
11401
  clearTimeout(timer);
11006
11402
  reject(err);
@@ -11277,11 +11673,11 @@ var McpManager = class {
11277
11673
  };
11278
11674
 
11279
11675
  // src/skills/manager.ts
11280
- import { existsSync as existsSync16, readdirSync as readdirSync8, mkdirSync as mkdirSync8, statSync as statSync7 } from "fs";
11281
- import { join as join10 } from "path";
11676
+ import { existsSync as existsSync18, readdirSync as readdirSync8, mkdirSync as mkdirSync9, statSync as statSync7 } from "fs";
11677
+ import { join as join12 } from "path";
11282
11678
 
11283
11679
  // src/skills/types.ts
11284
- import { readFileSync as readFileSync11 } from "fs";
11680
+ import { readFileSync as readFileSync13 } from "fs";
11285
11681
  import { basename as basename5 } from "path";
11286
11682
  function parseSimpleYaml(yaml) {
11287
11683
  const result = {};
@@ -11303,7 +11699,7 @@ function parseYamlArray(value) {
11303
11699
  function parseSkillFile(filePath) {
11304
11700
  let raw;
11305
11701
  try {
11306
- raw = readFileSync11(filePath, "utf-8");
11702
+ raw = readFileSync13(filePath, "utf-8");
11307
11703
  } catch {
11308
11704
  return null;
11309
11705
  }
@@ -11346,9 +11742,9 @@ var SkillManager = class {
11346
11742
  /** 发现并加载 skillsDir 下所有 .md 文件,返回加载数量 */
11347
11743
  loadSkills() {
11348
11744
  this.skills.clear();
11349
- if (!existsSync16(this.skillsDir)) {
11745
+ if (!existsSync18(this.skillsDir)) {
11350
11746
  try {
11351
- mkdirSync8(this.skillsDir, { recursive: true });
11747
+ mkdirSync9(this.skillsDir, { recursive: true });
11352
11748
  } catch {
11353
11749
  }
11354
11750
  return 0;
@@ -11361,14 +11757,14 @@ var SkillManager = class {
11361
11757
  }
11362
11758
  for (const entry of entries) {
11363
11759
  let filePath;
11364
- const fullPath = join10(this.skillsDir, entry);
11760
+ const fullPath = join12(this.skillsDir, entry);
11365
11761
  if (entry.endsWith(".md")) {
11366
11762
  filePath = fullPath;
11367
11763
  } else {
11368
11764
  try {
11369
11765
  if (statSync7(fullPath).isDirectory()) {
11370
- const skillMd = join10(fullPath, "SKILL.md");
11371
- if (existsSync16(skillMd)) {
11766
+ const skillMd = join12(fullPath, "SKILL.md");
11767
+ if (existsSync18(skillMd)) {
11372
11768
  filePath = skillMd;
11373
11769
  } else {
11374
11770
  continue;
@@ -11433,7 +11829,7 @@ var SkillManager = class {
11433
11829
  // src/web/tool-executor-web.ts
11434
11830
  import { randomUUID as randomUUID2 } from "crypto";
11435
11831
  import { tmpdir as tmpdir2 } from "os";
11436
- import { existsSync as existsSync17, readFileSync as readFileSync12 } from "fs";
11832
+ import { existsSync as existsSync19, readFileSync as readFileSync14 } from "fs";
11437
11833
  var ToolExecutorWeb = class _ToolExecutorWeb {
11438
11834
  constructor(registry, ws) {
11439
11835
  this.registry = registry;
@@ -11449,6 +11845,7 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11449
11845
  allowedPermissionProfiles = [];
11450
11846
  workspaceRoot = process.cwd();
11451
11847
  networkPolicy;
11848
+ hookConfigDir = process.cwd();
11452
11849
  /** Pending confirm promises keyed by requestId */
11453
11850
  pendingConfirms = /* @__PURE__ */ new Map();
11454
11851
  /** Pending batch confirm promises */
@@ -11461,6 +11858,8 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11461
11858
  confirming = false;
11462
11859
  /** Session-level auto-approve toggle (/yolo command) */
11463
11860
  sessionAutoApprove = false;
11861
+ /** Session-level Auto Mode: rule-based low-risk auto approval. */
11862
+ sessionAutoMode = false;
11464
11863
  /** Track tool start times for duration calculation */
11465
11864
  toolStartTimes = /* @__PURE__ */ new Map();
11466
11865
  /**
@@ -11488,6 +11887,7 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11488
11887
  if (opts.allowedPermissionProfiles) this.allowedPermissionProfiles = opts.allowedPermissionProfiles;
11489
11888
  if (opts.workspaceRoot) this.workspaceRoot = opts.workspaceRoot;
11490
11889
  if (opts.networkPolicy) this.networkPolicy = opts.networkPolicy;
11890
+ if (opts.hookConfigDir) this.hookConfigDir = opts.hookConfigDir;
11491
11891
  }
11492
11892
  /** Clear M7 timeout timer for a requestId */
11493
11893
  clearPendingTimer(requestId) {
@@ -11499,33 +11899,33 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11499
11899
  }
11500
11900
  /** Resolve a pending confirm from client response */
11501
11901
  resolveConfirm(requestId, approved) {
11502
- const resolve9 = this.pendingConfirms.get(requestId);
11503
- if (resolve9) {
11902
+ const resolve10 = this.pendingConfirms.get(requestId);
11903
+ if (resolve10) {
11504
11904
  this.clearPendingTimer(requestId);
11505
11905
  this.pendingConfirms.delete(requestId);
11506
11906
  this.confirming = false;
11507
- resolve9(approved);
11907
+ resolve10(approved);
11508
11908
  }
11509
11909
  }
11510
11910
  /** Resolve a pending batch confirm from client response */
11511
11911
  resolveBatchConfirm(requestId, decision) {
11512
- const resolve9 = this.pendingBatchConfirms.get(requestId);
11513
- if (resolve9) {
11912
+ const resolve10 = this.pendingBatchConfirms.get(requestId);
11913
+ if (resolve10) {
11514
11914
  this.clearPendingTimer(requestId);
11515
11915
  this.pendingBatchConfirms.delete(requestId);
11516
11916
  this.confirming = false;
11517
11917
  if (decision === "all" || decision === "none") {
11518
- resolve9(decision);
11918
+ resolve10(decision);
11519
11919
  } else {
11520
- resolve9(new Set(decision));
11920
+ resolve10(new Set(decision));
11521
11921
  }
11522
11922
  }
11523
11923
  }
11524
11924
  /** Cancel all pending confirms (e.g., on disconnect) */
11525
11925
  cancelAll() {
11526
- for (const resolve9 of this.pendingConfirms.values()) resolve9(false);
11926
+ for (const resolve10 of this.pendingConfirms.values()) resolve10(false);
11527
11927
  this.pendingConfirms.clear();
11528
- for (const resolve9 of this.pendingBatchConfirms.values()) resolve9("none");
11928
+ for (const resolve10 of this.pendingBatchConfirms.values()) resolve10("none");
11529
11929
  this.pendingBatchConfirms.clear();
11530
11930
  this.confirming = false;
11531
11931
  }
@@ -11536,7 +11936,6 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11536
11936
  }
11537
11937
  sendToolCallStart(call) {
11538
11938
  const dangerLevel = getDangerLevel(call.name, call.arguments);
11539
- let toolCallStarted = false;
11540
11939
  const startTime = Date.now();
11541
11940
  this.toolStartTimes.set(call.id, startTime);
11542
11941
  const msg = {
@@ -11569,9 +11968,9 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11569
11968
  if (call.name === "write_file") {
11570
11969
  const filePath = String(call.arguments["path"] ?? "");
11571
11970
  const newContent = String(call.arguments["content"] ?? "");
11572
- if (filePath && existsSync17(filePath)) {
11971
+ if (filePath && existsSync19(filePath)) {
11573
11972
  try {
11574
- const old = readFileSync12(filePath, "utf-8");
11973
+ const old = readFileSync14(filePath, "utf-8");
11575
11974
  return renderDiff(old, newContent, { filePath });
11576
11975
  } catch {
11577
11976
  }
@@ -11602,8 +12001,8 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11602
12001
  diff: this.getDiffPreview(call)
11603
12002
  };
11604
12003
  this.send(msg);
11605
- return new Promise((resolve9) => {
11606
- this.pendingConfirms.set(requestId, resolve9);
12004
+ return new Promise((resolve10) => {
12005
+ this.pendingConfirms.set(requestId, resolve10);
11607
12006
  this.pendingTimers.set(requestId, setTimeout(() => {
11608
12007
  if (this.pendingConfirms.has(requestId)) {
11609
12008
  this.resolveConfirm(requestId, false);
@@ -11627,8 +12026,8 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11627
12026
  files
11628
12027
  };
11629
12028
  this.send(msg);
11630
- return new Promise((resolve9) => {
11631
- this.pendingBatchConfirms.set(requestId, resolve9);
12029
+ return new Promise((resolve10) => {
12030
+ this.pendingBatchConfirms.set(requestId, resolve10);
11632
12031
  this.pendingTimers.set(requestId, setTimeout(() => {
11633
12032
  if (this.pendingBatchConfirms.has(requestId)) {
11634
12033
  this.resolveBatchConfirm(requestId, "none");
@@ -11651,6 +12050,26 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11651
12050
  dangerLevel,
11652
12051
  args: JSON.stringify(call.arguments).slice(0, 200)
11653
12052
  });
12053
+ const preToolDecisions = runLifecycleHooks(
12054
+ this.hookConfig,
12055
+ "PreToolUse",
12056
+ { tool: call.name, dangerLevel, args: call.arguments },
12057
+ { configDir: this.hookConfigDir, onSummary: (message) => this.send({ type: "info", message: `[hook] ${message}` }) }
12058
+ );
12059
+ const preDeny = preToolDecisions.find((d) => d.action === "deny");
12060
+ if (preDeny) {
12061
+ const result = {
12062
+ callId: call.id,
12063
+ content: `[Hook denied] ${preDeny.reason ?? `PreToolUse blocked ${call.name}`}. Do not retry without asking.`,
12064
+ isError: true
12065
+ };
12066
+ this.sendToolCallResult(call, result.content, true);
12067
+ return result;
12068
+ }
12069
+ for (const d of preToolDecisions) {
12070
+ const warnings = [d.warning, ...d.warnings ?? []].filter(Boolean);
12071
+ for (const warning of warnings) this.send({ type: "info", message: `[hook] ${warning}` });
12072
+ }
11654
12073
  const permission = checkPermissionWithProfile(
11655
12074
  call.name,
11656
12075
  call.arguments,
@@ -11666,6 +12085,14 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11666
12085
  }
11667
12086
  );
11668
12087
  const networkPermission = checkNetworkPolicy(call.name, call.arguments, this.networkPolicy);
12088
+ if (networkPermission?.action === "confirm" || permission.action === "confirm") {
12089
+ runLifecycleHooks(
12090
+ this.hookConfig,
12091
+ "PermissionRequest",
12092
+ { tool: call.name, dangerLevel, args: call.arguments, reason: networkPermission?.reason ?? permission.reason },
12093
+ { configDir: this.hookConfigDir, onSummary: (message) => this.send({ type: "info", message: "[hook] " + message }) }
12094
+ );
12095
+ }
11669
12096
  if (networkPermission?.action === "deny") {
11670
12097
  const reason = networkPermission.reason ? ` (${networkPermission.reason})` : "";
11671
12098
  return {
@@ -11689,11 +12116,13 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11689
12116
  const content = truncateOutput(rawContent, call.name);
11690
12117
  this.sendToolCallResult(call, rawContent, false);
11691
12118
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
12119
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
11692
12120
  return { callId: call.id, content, isError: false };
11693
12121
  } catch (err) {
11694
12122
  const message = err instanceof Error ? err.message : String(err);
11695
12123
  this.sendToolCallResult(call, message, true);
11696
12124
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
12125
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
11697
12126
  return { callId: call.id, content: message, isError: true };
11698
12127
  }
11699
12128
  }
@@ -11708,17 +12137,48 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11708
12137
  }
11709
12138
  }
11710
12139
  this.sendToolCallStart(call);
12140
+ if (this.sessionAutoMode && networkPermission?.action !== "confirm") {
12141
+ const classification = defaultActionClassifier.classify(call, dangerLevel, {
12142
+ workspaceRoot: this.workspaceRoot,
12143
+ tempDirs: [tmpdir2()]
12144
+ });
12145
+ if (classification.decision === "deny") {
12146
+ recordRecentlyDeniedAutoAction(call, classification);
12147
+ const rejectionMsg = `[Auto denied] ${classification.reason}. Do not retry without asking.`;
12148
+ this.sendToolCallResult(call, rejectionMsg, true);
12149
+ return { callId: call.id, content: rejectionMsg, isError: true };
12150
+ }
12151
+ if (classification.decision === "auto-approve") {
12152
+ this.send({ type: "info", message: `\u26A1 Auto-approved (/auto: ${classification.ruleId})` });
12153
+ try {
12154
+ const rawContent = await runTool(tool, call.arguments, call.name);
12155
+ const content = truncateOutput(rawContent, call.name);
12156
+ this.sendToolCallResult(call, rawContent, false);
12157
+ runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
12158
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
12159
+ return { callId: call.id, content, isError: false };
12160
+ } catch (err) {
12161
+ const message = err instanceof Error ? err.message : String(err);
12162
+ this.sendToolCallResult(call, message, true);
12163
+ runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
12164
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
12165
+ return { callId: call.id, content: message, isError: true };
12166
+ }
12167
+ }
12168
+ }
11711
12169
  if (this.sessionAutoApprove && dangerLevel === "write") {
11712
12170
  try {
11713
12171
  const rawContent = await runTool(tool, call.arguments, call.name);
11714
12172
  const content = truncateOutput(rawContent, call.name);
11715
12173
  this.sendToolCallResult(call, rawContent, false);
11716
12174
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
12175
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
11717
12176
  return { callId: call.id, content, isError: false };
11718
12177
  } catch (err) {
11719
12178
  const message = err instanceof Error ? err.message : String(err);
11720
12179
  this.sendToolCallResult(call, message, true);
11721
12180
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
12181
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
11722
12182
  return { callId: call.id, content: message, isError: true };
11723
12183
  }
11724
12184
  }
@@ -11736,11 +12196,13 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11736
12196
  const content = truncateOutput(rawContent, call.name);
11737
12197
  this.sendToolCallResult(call, rawContent, false);
11738
12198
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
12199
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
11739
12200
  return { callId: call.id, content, isError: false };
11740
12201
  } catch (err) {
11741
12202
  const message = err instanceof Error ? err.message : String(err);
11742
12203
  this.sendToolCallResult(call, message, true);
11743
12204
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
12205
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
11744
12206
  return { callId: call.id, content: message, isError: true };
11745
12207
  }
11746
12208
  }
@@ -11811,20 +12273,20 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
11811
12273
  };
11812
12274
 
11813
12275
  // src/core/system-prompt-builder.ts
11814
- import { existsSync as existsSync19, readFileSync as readFileSync14 } from "fs";
11815
- import { join as join12 } from "path";
12276
+ import { existsSync as existsSync21, readFileSync as readFileSync16 } from "fs";
12277
+ import { join as join14 } from "path";
11816
12278
 
11817
12279
  // src/repl/dev-state.ts
11818
- import { existsSync as existsSync18, readFileSync as readFileSync13, unlinkSync as unlinkSync4, mkdirSync as mkdirSync9 } from "fs";
11819
- import { join as join11 } from "path";
12280
+ import { existsSync as existsSync20, readFileSync as readFileSync15, unlinkSync as unlinkSync4, mkdirSync as mkdirSync10 } from "fs";
12281
+ import { join as join13 } from "path";
11820
12282
  import { homedir as homedir6 } from "os";
11821
12283
  function getDevStatePath() {
11822
- return join11(homedir6(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
12284
+ return join13(homedir6(), CONFIG_DIR_NAME, DEV_STATE_FILE_NAME);
11823
12285
  }
11824
12286
  function loadDevState() {
11825
12287
  const path3 = getDevStatePath();
11826
- if (!existsSync18(path3)) return null;
11827
- const content = readFileSync13(path3, "utf-8").trim();
12288
+ if (!existsSync20(path3)) return null;
12289
+ const content = readFileSync15(path3, "utf-8").trim();
11828
12290
  return content || null;
11829
12291
  }
11830
12292
 
@@ -11882,9 +12344,9 @@ ${ctx.activeSkill.content}`);
11882
12344
  return { stable: stableParts.join("\n\n---\n\n"), volatile };
11883
12345
  }
11884
12346
  function loadMemoryContent(configDir) {
11885
- const memoryPath = join12(configDir, MEMORY_FILE_NAME);
11886
- if (!existsSync19(memoryPath)) return null;
11887
- let content = readFileSync14(memoryPath, "utf-8").trim();
12347
+ const memoryPath = join14(configDir, MEMORY_FILE_NAME);
12348
+ if (!existsSync21(memoryPath)) return null;
12349
+ let content = readFileSync16(memoryPath, "utf-8").trim();
11888
12350
  if (!content) return null;
11889
12351
  if (content.length > MEMORY_MAX_CHARS) {
11890
12352
  content = content.slice(-MEMORY_MAX_CHARS);
@@ -12093,8 +12555,8 @@ function autoTrimSessionIfNeeded(session, sizeLimit = SESSION_SIZE_LIMIT) {
12093
12555
  }
12094
12556
 
12095
12557
  // src/core/context-files.ts
12096
- import { existsSync as existsSync20, readFileSync as readFileSync15 } from "fs";
12097
- import { join as join13, relative as relative3, resolve as resolve6 } from "path";
12558
+ import { existsSync as existsSync22, readFileSync as readFileSync17 } from "fs";
12559
+ import { join as join15, relative as relative3, resolve as resolve7 } from "path";
12098
12560
  function uniqueNonEmpty(values) {
12099
12561
  const seen = /* @__PURE__ */ new Set();
12100
12562
  const out = [];
@@ -12117,14 +12579,14 @@ function displayPath(filePath, cwd) {
12117
12579
  return filePath;
12118
12580
  }
12119
12581
  function isInsideOrEqual(parent, child) {
12120
- const rel = relative3(resolve6(parent), resolve6(child));
12582
+ const rel = relative3(resolve7(parent), resolve7(child));
12121
12583
  return rel === "" || !!rel && !rel.startsWith("..") && !rel.startsWith("/") && !rel.startsWith("\\");
12122
12584
  }
12123
12585
  function readContextFile(level, filePath, cwd, maxBytes) {
12124
12586
  const name = filePath.split(/[\\/]/).pop() ?? filePath;
12125
12587
  const shown = displayPath(filePath, cwd);
12126
12588
  try {
12127
- const raw = readFileSync15(filePath);
12589
+ const raw = readFileSync17(filePath);
12128
12590
  const byteCount = raw.byteLength;
12129
12591
  if (byteCount === 0) {
12130
12592
  return {
@@ -12171,8 +12633,8 @@ function readContextFile(level, filePath, cwd, maxBytes) {
12171
12633
  function findFirstContextFile(level, dir, cwd, candidates, maxBytes) {
12172
12634
  const skipped = [];
12173
12635
  for (const candidate of candidates) {
12174
- const filePath = join13(dir, candidate);
12175
- if (!existsSync20(filePath)) continue;
12636
+ const filePath = join15(dir, candidate);
12637
+ if (!existsSync22(filePath)) continue;
12176
12638
  const result = readContextFile(level, filePath, cwd, maxBytes);
12177
12639
  if (result.skipped) skipped.push(result.skipped);
12178
12640
  if (result.layer) return { layer: result.layer, skipped };
@@ -12180,7 +12642,7 @@ function findFirstContextFile(level, dir, cwd, candidates, maxBytes) {
12180
12642
  return { layer: null, skipped };
12181
12643
  }
12182
12644
  function loadContextFiles(options) {
12183
- const cwd = resolve6(options.cwd);
12645
+ const cwd = resolve7(options.cwd);
12184
12646
  const maxBytes = options.maxBytes ?? CONTEXT_FILE_MAX_BYTES;
12185
12647
  const candidates = options.candidates ?? buildContextFileCandidates(options.fallbackFilenames);
12186
12648
  const skipped = [];
@@ -12188,7 +12650,7 @@ function loadContextFiles(options) {
12188
12650
  return { layers: [], mergedContent: "", skipped, totalChars: 0, totalBytes: 0, maxBytes, candidates };
12189
12651
  }
12190
12652
  if (options.setting && options.setting !== "auto") {
12191
- const filePath = resolve6(cwd, String(options.setting));
12653
+ const filePath = resolve7(cwd, String(options.setting));
12192
12654
  if (!isInsideOrEqual(cwd, filePath)) {
12193
12655
  skipped.push({
12194
12656
  level: "single",
@@ -12199,7 +12661,7 @@ function loadContextFiles(options) {
12199
12661
  });
12200
12662
  return { layers: [], mergedContent: "", skipped, totalChars: 0, totalBytes: 0, maxBytes, candidates };
12201
12663
  }
12202
- if (!existsSync20(filePath)) {
12664
+ if (!existsSync22(filePath)) {
12203
12665
  skipped.push({
12204
12666
  level: "single",
12205
12667
  filePath,
@@ -12232,7 +12694,7 @@ function loadContextFiles(options) {
12232
12694
  skipped.push(...result.skipped);
12233
12695
  if (result.layer) layers.push(result.layer);
12234
12696
  }
12235
- if (resolve6(options.cwd) !== resolve6(options.projectRoot)) {
12697
+ if (resolve7(options.cwd) !== resolve7(options.projectRoot)) {
12236
12698
  const result = findFirstContextFile("local", options.cwd, cwd, candidates, maxBytes);
12237
12699
  skipped.push(...result.skipped);
12238
12700
  if (result.layer) layers.push(result.layer);
@@ -12250,13 +12712,13 @@ function loadContextFiles(options) {
12250
12712
  }
12251
12713
 
12252
12714
  // src/web/session-handler.ts
12253
- import { existsSync as existsSync23, readFileSync as readFileSync17, writeFileSync as writeFileSync2, mkdirSync as mkdirSync10, readdirSync as readdirSync10, statSync as statSync9, createWriteStream } from "fs";
12254
- import { join as join17, resolve as resolve7, dirname as dirname5 } from "path";
12715
+ import { existsSync as existsSync25, readFileSync as readFileSync19, writeFileSync as writeFileSync2, mkdirSync as mkdirSync11, readdirSync as readdirSync10, statSync as statSync9, createWriteStream } from "fs";
12716
+ import { join as join19, resolve as resolve8, dirname as dirname5 } from "path";
12255
12717
 
12256
12718
  // src/tools/git-context.ts
12257
12719
  import { execSync as execSync2 } from "child_process";
12258
- import { existsSync as existsSync21 } from "fs";
12259
- import { join as join14 } from "path";
12720
+ import { existsSync as existsSync23 } from "fs";
12721
+ import { join as join16 } from "path";
12260
12722
  function runGit2(cmd, cwd) {
12261
12723
  try {
12262
12724
  return execSync2(`git ${cmd}`, {
@@ -12273,7 +12735,7 @@ function getGitRoot(cwd = process.cwd()) {
12273
12735
  return runGit2("rev-parse --show-toplevel", cwd);
12274
12736
  }
12275
12737
  function getGitContext(cwd = process.cwd()) {
12276
- if (!existsSync21(join14(cwd, ".git"))) {
12738
+ if (!existsSync23(join16(cwd, ".git"))) {
12277
12739
  const result = runGit2("rev-parse --git-dir", cwd);
12278
12740
  if (!result) return null;
12279
12741
  }
@@ -12402,8 +12864,8 @@ If no security issues found, state "\u2705 No security vulnerabilities detected"
12402
12864
  }
12403
12865
 
12404
12866
  // src/repl/commands/project-init.ts
12405
- import { existsSync as existsSync22, readFileSync as readFileSync16, readdirSync as readdirSync9, statSync as statSync8 } from "fs";
12406
- import { join as join15 } from "path";
12867
+ import { existsSync as existsSync24, readFileSync as readFileSync18, readdirSync as readdirSync9, statSync as statSync8 } from "fs";
12868
+ import { join as join17 } from "path";
12407
12869
  var SCAN_SKIP_DIRS = /* @__PURE__ */ new Set([
12408
12870
  "node_modules",
12409
12871
  ".git",
@@ -12447,11 +12909,11 @@ function scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
12447
12909
  const sorted = filtered.sort((a, b) => {
12448
12910
  let aIsDir = false, bIsDir = false;
12449
12911
  try {
12450
- aIsDir = statSync8(join15(d, a)).isDirectory();
12912
+ aIsDir = statSync8(join17(d, a)).isDirectory();
12451
12913
  } catch {
12452
12914
  }
12453
12915
  try {
12454
- bIsDir = statSync8(join15(d, b)).isDirectory();
12916
+ bIsDir = statSync8(join17(d, b)).isDirectory();
12455
12917
  } catch {
12456
12918
  }
12457
12919
  if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
@@ -12459,7 +12921,7 @@ function scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
12459
12921
  });
12460
12922
  for (let i = 0; i < sorted.length && count < maxEntries; i++) {
12461
12923
  const name = sorted[i];
12462
- const fullPath = join15(d, name);
12924
+ const fullPath = join17(d, name);
12463
12925
  const isLast = i === sorted.length - 1;
12464
12926
  const connector = isLast ? "+-- " : "|-- ";
12465
12927
  let isDir;
@@ -12486,7 +12948,7 @@ function scanProject(cwd) {
12486
12948
  configFiles: [],
12487
12949
  directoryStructure: ""
12488
12950
  };
12489
- const check = (file) => existsSync22(join15(cwd, file));
12951
+ const check = (file) => existsSync24(join17(cwd, file));
12490
12952
  const configCandidates = [
12491
12953
  "package.json",
12492
12954
  "tsconfig.json",
@@ -12513,7 +12975,7 @@ function scanProject(cwd) {
12513
12975
  info.type = "node";
12514
12976
  info.language = check("tsconfig.json") ? "TypeScript" : "JavaScript";
12515
12977
  try {
12516
- const pkg = JSON.parse(readFileSync16(join15(cwd, "package.json"), "utf-8"));
12978
+ const pkg = JSON.parse(readFileSync18(join17(cwd, "package.json"), "utf-8"));
12517
12979
  const scripts = pkg.scripts ?? {};
12518
12980
  info.buildCommand = scripts.build ? "npm run build" : void 0;
12519
12981
  info.testCommand = scripts.test ? "npm test" : void 0;
@@ -13173,7 +13635,7 @@ function resolveRoleProviders(roles, lookup, defaultProvider, defaultModel, avai
13173
13635
  }
13174
13636
 
13175
13637
  // src/hub/persist.ts
13176
- import { join as join16 } from "path";
13638
+ import { join as join18 } from "path";
13177
13639
  function discussionToMessages(state2) {
13178
13640
  const out = [];
13179
13641
  const t0 = state2.messages[0]?.timestamp ?? /* @__PURE__ */ new Date();
@@ -13211,7 +13673,7 @@ async function persistDiscussion(state2, config, defaultProvider, defaultModel)
13211
13673
  session.title = `[Hub] ${state2.topic.slice(0, 48)}`.replace(/\n/g, " ");
13212
13674
  session.titleAiGenerated = true;
13213
13675
  await sm.save();
13214
- return { id: session.id, path: join16(config.getHistoryDir(), `${session.id}.json`) };
13676
+ return { id: session.id, path: join18(config.getHistoryDir(), `${session.id}.json`) };
13215
13677
  }
13216
13678
 
13217
13679
  // src/web/session-handler.ts
@@ -13314,6 +13776,7 @@ var SessionHandler = class {
13314
13776
  const permissionProfiles = this.config.get("permissionProfiles") ?? {};
13315
13777
  this.toolExecutor.setConfig({
13316
13778
  hookConfig: hooks,
13779
+ hookConfigDir: this.config.getConfigDir(),
13317
13780
  permissionRules,
13318
13781
  defaultPermission,
13319
13782
  permissionProfileName,
@@ -13326,6 +13789,11 @@ var SessionHandler = class {
13326
13789
  if (permissionProfileWarning) {
13327
13790
  this.send({ type: "info", message: permissionProfileWarning });
13328
13791
  }
13792
+ const pendingHooks = getPendingHookTrust(hooks, this.config.getConfigDir());
13793
+ if (pendingHooks.length > 0) {
13794
+ this.send({ type: "info", message: `${pendingHooks.length} project hook(s) require trust. Use /hooks list and /hooks trust <id>.` });
13795
+ }
13796
+ runLifecycleHooks(hooks, "SessionStart", { source: "web" }, { configDir: this.config.getConfigDir() });
13329
13797
  this.sendStatus();
13330
13798
  askUserContext.rl = null;
13331
13799
  askUserContext.prompting = false;
@@ -13389,10 +13857,10 @@ var SessionHandler = class {
13389
13857
  return;
13390
13858
  }
13391
13859
  case "ask_user_response": {
13392
- const resolve9 = this.pendingAskUser.get(msg.requestId);
13393
- if (resolve9) {
13860
+ const resolve10 = this.pendingAskUser.get(msg.requestId);
13861
+ if (resolve10) {
13394
13862
  this.pendingAskUser.delete(msg.requestId);
13395
- resolve9(msg.answer);
13863
+ resolve10(msg.answer);
13396
13864
  }
13397
13865
  return;
13398
13866
  }
@@ -13403,10 +13871,10 @@ var SessionHandler = class {
13403
13871
  case "memory_rebuild":
13404
13872
  return this.handleMemoryRebuild(Boolean(msg.full));
13405
13873
  case "auto_pause_response": {
13406
- const resolve9 = this.pendingAutoPause.get(msg.requestId);
13407
- if (resolve9) {
13874
+ const resolve10 = this.pendingAutoPause.get(msg.requestId);
13875
+ if (resolve10) {
13408
13876
  this.pendingAutoPause.delete(msg.requestId);
13409
- resolve9({ action: msg.action, message: msg.message });
13877
+ resolve10({ action: msg.action, message: msg.message });
13410
13878
  }
13411
13879
  return;
13412
13880
  }
@@ -13421,10 +13889,10 @@ var SessionHandler = class {
13421
13889
  this.hubOrchestrator?.abort();
13422
13890
  return;
13423
13891
  case "hub_steer": {
13424
- const resolve9 = this.pendingHubReview.get(msg.requestId);
13425
- if (resolve9) {
13892
+ const resolve10 = this.pendingHubReview.get(msg.requestId);
13893
+ if (resolve10) {
13426
13894
  this.pendingHubReview.delete(msg.requestId);
13427
- resolve9({ action: msg.action, message: msg.message });
13895
+ resolve10({ action: msg.action, message: msg.message });
13428
13896
  }
13429
13897
  return;
13430
13898
  }
@@ -13434,11 +13902,12 @@ var SessionHandler = class {
13434
13902
  }
13435
13903
  }
13436
13904
  onDisconnect() {
13905
+ runLifecycleHooks(this.config.get("hooks") ?? void 0, "Stop", { sessionId: this.sessions.current?.id }, { configDir: this.config.getConfigDir() });
13437
13906
  this.toolExecutor.cancelAll();
13438
13907
  if (this.abortController) this.abortController.abort();
13439
- for (const resolve9 of this.pendingAskUser.values()) resolve9(null);
13908
+ for (const resolve10 of this.pendingAskUser.values()) resolve10(null);
13440
13909
  this.pendingAskUser.clear();
13441
- for (const resolve9 of this.pendingAutoPause.values()) resolve9({ action: "stop" });
13910
+ for (const resolve10 of this.pendingAutoPause.values()) resolve10({ action: "stop" });
13442
13911
  this.pendingAutoPause.clear();
13443
13912
  this.saveIfNeeded();
13444
13913
  }
@@ -13572,9 +14041,9 @@ var SessionHandler = class {
13572
14041
  this.hubOrchestrator = orchestrator;
13573
14042
  orchestrator.onEvent = (event) => this.send({ type: "hub_event", event });
13574
14043
  if (config.humanSteer) {
13575
- orchestrator.onRoundReview = ({ round, maxRounds }) => new Promise((resolve9) => {
14044
+ orchestrator.onRoundReview = ({ round, maxRounds }) => new Promise((resolve10) => {
13576
14045
  const requestId = `hubrev_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
13577
- this.pendingHubReview.set(requestId, resolve9);
14046
+ this.pendingHubReview.set(requestId, resolve10);
13578
14047
  this.send({ type: "hub_review", requestId, round, maxRounds });
13579
14048
  });
13580
14049
  }
@@ -13611,6 +14080,26 @@ var SessionHandler = class {
13611
14080
  try {
13612
14081
  this.ensureSession();
13613
14082
  const session = this.sessions.current;
14083
+ const promptDecisions = runLifecycleHooks(
14084
+ this.config.get("hooks") ?? void 0,
14085
+ "UserPromptSubmit",
14086
+ { sessionId: session.id, prompt: content },
14087
+ { configDir: this.config.getConfigDir(), onSummary: (message) => this.send({ type: "info", message: "[hook] " + message }) }
14088
+ );
14089
+ const promptDeny = promptDecisions.find((d) => d.action === "deny");
14090
+ if (promptDeny) {
14091
+ this.send({ type: "error", message: "Prompt blocked by hook: " + (promptDeny.reason ?? "UserPromptSubmit denied") });
14092
+ return;
14093
+ }
14094
+ for (const d of promptDecisions) {
14095
+ const warnings = [d.warning, ...d.warnings ?? []].filter(Boolean);
14096
+ for (const warning of warnings) this.send({ type: "info", message: "[hook] " + warning });
14097
+ }
14098
+ const secretHits = scanString(content, { customRegexes: [] });
14099
+ if (secretHits.length > 0) {
14100
+ const kinds = [...new Set(secretHits.map((h) => h.kind))].join(", ");
14101
+ this.send({ type: "info", message: "Possible secret in prompt (" + kinds + ")." });
14102
+ }
13614
14103
  const ALLOWED_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"]);
13615
14104
  const MAX_IMAGE_SIZE = 10 * 1024 * 1024;
13616
14105
  let msgContent;
@@ -13886,7 +14375,7 @@ Try: /compact to reduce context, /clear to reset, or switch to a larger-context
13886
14375
  this.send({ type: "response_done", content, usage });
13887
14376
  if (pendingTeeSave && isCleanDocumentBody(content)) {
13888
14377
  try {
13889
- mkdirSync10(dirname5(pendingTeeSave), { recursive: true });
14378
+ mkdirSync11(dirname5(pendingTeeSave), { recursive: true });
13890
14379
  const bodyToSave = stripOuterCodeFence(content);
13891
14380
  atomicWriteFileSync(pendingTeeSave, bodyToSave);
13892
14381
  undoStack.push(pendingTeeSave, `save_last_response (deferred): ${pendingTeeSave}`);
@@ -13911,8 +14400,8 @@ Try: /compact to reduce context, /clear to reset, or switch to a larger-context
13911
14400
  onMcpToolUsed: (name) => this.usedMcpToolNames.add(name),
13912
14401
  requestAutoPause: async ({ effectiveRound, maxToolRounds: totalRounds, toolSummary }) => {
13913
14402
  const requestId = `pause_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
13914
- const pauseResp = await new Promise((resolve9) => {
13915
- this.pendingAutoPause.set(requestId, resolve9);
14403
+ const pauseResp = await new Promise((resolve10) => {
14404
+ this.pendingAutoPause.set(requestId, resolve10);
13916
14405
  this.send({
13917
14406
  type: "auto_pause_request",
13918
14407
  requestId,
@@ -14062,7 +14551,7 @@ ${summaryContent}`,
14062
14551
  let isError = false;
14063
14552
  let summary;
14064
14553
  try {
14065
- mkdirSync10(dirname5(saveToFile), { recursive: true });
14554
+ mkdirSync11(dirname5(saveToFile), { recursive: true });
14066
14555
  fileStream = createWriteStream(saveToFile);
14067
14556
  const teeSystemPrompt = stripToolCallReminder(systemPrompt ?? "") + CONTENT_ONLY_STREAM_REMINDER;
14068
14557
  const teeExtraMessages = extraMessages.length > 0 ? [...extraMessages, { role: "user", content: TEE_FINAL_USER_NUDGE }] : [{ role: "user", content: TEE_FINAL_USER_NUDGE }];
@@ -14091,8 +14580,8 @@ ${summaryContent}`,
14091
14580
  }
14092
14581
  if (chunk.done) break;
14093
14582
  }
14094
- await new Promise((resolve9, reject) => {
14095
- fileStream.end((err) => err ? reject(err) : resolve9());
14583
+ await new Promise((resolve10, reject) => {
14584
+ fileStream.end((err) => err ? reject(err) : resolve10());
14096
14585
  });
14097
14586
  const verdict = evaluateTeeContent(fullContent, saveToFile, priorContent);
14098
14587
  if (verdict.kind === "reject") {
@@ -14118,7 +14607,7 @@ ${summaryContent}`,
14118
14607
  } catch (err) {
14119
14608
  if (fileStream) {
14120
14609
  try {
14121
- await new Promise((resolve9) => fileStream.end(() => resolve9()));
14610
+ await new Promise((resolve10) => fileStream.end(() => resolve10()));
14122
14611
  } catch {
14123
14612
  }
14124
14613
  }
@@ -14548,6 +15037,47 @@ ${activated.meta.description || ""}` });
14548
15037
  break;
14549
15038
  }
14550
15039
  // ── /yolo ──────────────────────────────────────────────────────
15040
+ case "auto": {
15041
+ const sub = args[0]?.toLowerCase() ?? "status";
15042
+ if (sub === "on") {
15043
+ this.toolExecutor.sessionAutoMode = true;
15044
+ this.send({ type: "info", message: "\u26A1 Auto Mode ON \u2014 low-risk actions may auto-run; high-risk actions still confirm or deny. Auto Mode is session-scoped." });
15045
+ } else if (sub === "off") {
15046
+ this.toolExecutor.sessionAutoMode = false;
15047
+ this.send({ type: "info", message: "Auto Mode disabled." });
15048
+ } else if (sub === "status") {
15049
+ this.send({ type: "info", message: `Auto Mode: ${this.toolExecutor.sessionAutoMode ? "on" : "off"}
15050
+ Scope: current session only
15051
+ Denied: /permissions recently-denied` });
15052
+ } else {
15053
+ this.send({ type: "error", message: "Usage: /auto [on|off|status]" });
15054
+ }
15055
+ this.sendStatus();
15056
+ break;
15057
+ }
15058
+ case "permissions": {
15059
+ const sub = args[0]?.toLowerCase() ?? "recently-denied";
15060
+ if (sub === "recently-denied") {
15061
+ const rows = getRecentlyDeniedAutoActions();
15062
+ if (rows.length === 0) {
15063
+ this.send({ type: "info", message: "No recently denied Auto Mode actions." });
15064
+ } else {
15065
+ const lines = [`Recently denied Auto Mode actions (${rows.length})`, ""];
15066
+ for (const r of rows.slice(0, 20)) {
15067
+ lines.push(`- ${r.timestamp} ${r.tool} ${r.ruleId}`);
15068
+ lines.push(` ${r.reason}`);
15069
+ lines.push(` ${r.argsPreview}`);
15070
+ }
15071
+ this.send({ type: "info", message: lines.join("\n") });
15072
+ }
15073
+ } else if (sub === "clear-denied") {
15074
+ clearRecentlyDeniedAutoActions();
15075
+ this.send({ type: "info", message: "Cleared recently denied Auto Mode actions." });
15076
+ } else {
15077
+ this.send({ type: "error", message: "Usage: /permissions recently-denied|clear-denied" });
15078
+ }
15079
+ break;
15080
+ }
14551
15081
  case "yolo": {
14552
15082
  const sub = args[0]?.toLowerCase();
14553
15083
  if (sub === "off") {
@@ -14648,9 +15178,9 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
14648
15178
  let modifiedFiles = 0;
14649
15179
  const diffLines2 = [];
14650
15180
  for (const [filePath, { earliest }] of fileMap) {
14651
- const currentContent = existsSync23(filePath) ? (() => {
15181
+ const currentContent = existsSync25(filePath) ? (() => {
14652
15182
  try {
14653
- return readFileSync17(filePath, "utf-8");
15183
+ return readFileSync19(filePath, "utf-8");
14654
15184
  } catch {
14655
15185
  return null;
14656
15186
  }
@@ -14751,7 +15281,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
14751
15281
  break;
14752
15282
  }
14753
15283
  const sub = args[0]?.toLowerCase();
14754
- const resolve9 = (ref) => {
15284
+ const resolve10 = (ref) => {
14755
15285
  const r = session.resolveBranchRef(ref);
14756
15286
  if (r.ok) return r.id;
14757
15287
  if (r.reason === "ambiguous") {
@@ -14805,7 +15335,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
14805
15335
  this.send({ type: "error", message: "Usage: /branch switch <id|title>" });
14806
15336
  break;
14807
15337
  }
14808
- const id = resolve9(ref);
15338
+ const id = resolve10(ref);
14809
15339
  if (!id) break;
14810
15340
  const ok = session.switchBranch(id);
14811
15341
  if (ok) {
@@ -14825,7 +15355,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
14825
15355
  this.send({ type: "error", message: "Usage: /branch delete <id|title>" });
14826
15356
  break;
14827
15357
  }
14828
- const id = resolve9(ref);
15358
+ const id = resolve10(ref);
14829
15359
  if (!id) break;
14830
15360
  const ok = session.deleteBranch(id);
14831
15361
  if (ok) {
@@ -14844,7 +15374,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
14844
15374
  this.send({ type: "error", message: "Usage: /branch rename <id|title> <new title>" });
14845
15375
  break;
14846
15376
  }
14847
- const id = resolve9(ref);
15377
+ const id = resolve10(ref);
14848
15378
  if (!id) break;
14849
15379
  const ok = session.renameBranch(id, title);
14850
15380
  if (ok) {
@@ -14862,7 +15392,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
14862
15392
  this.send({ type: "error", message: "Usage: /branch diff <id|title>" });
14863
15393
  break;
14864
15394
  }
14865
- const id = resolve9(ref);
15395
+ const id = resolve10(ref);
14866
15396
  if (!id) break;
14867
15397
  const d = session.diffBranches(id);
14868
15398
  if (!d) {
@@ -14904,7 +15434,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
14904
15434
  this.send({ type: "error", message: "Usage: /branch cherry-pick <source-id|title> <msg-index>" });
14905
15435
  break;
14906
15436
  }
14907
- const id = resolve9(ref);
15437
+ const id = resolve10(ref);
14908
15438
  if (!id) break;
14909
15439
  const idx = parseInt(idxStr, 10);
14910
15440
  if (Number.isNaN(idx)) {
@@ -15166,7 +15696,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
15166
15696
  case "test": {
15167
15697
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
15168
15698
  try {
15169
- const { executeTests } = await import("./run-tests-PACN4UYX.js");
15699
+ const { executeTests } = await import("./run-tests-7J3PNWVS.js");
15170
15700
  const argStr = args.join(" ").trim();
15171
15701
  let testArgs = {};
15172
15702
  if (argStr) {
@@ -15183,9 +15713,9 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
15183
15713
  // ── /init ───────────────────────────────────────────────────────
15184
15714
  case "init": {
15185
15715
  const cwd = process.cwd();
15186
- const targetPath = join17(cwd, "AICLI.md");
15716
+ const targetPath = join19(cwd, "AICLI.md");
15187
15717
  const force = args.includes("--force");
15188
- if (existsSync23(targetPath) && !force) {
15718
+ if (existsSync25(targetPath) && !force) {
15189
15719
  this.send({ type: "info", message: `AICLI.md already exists at ${targetPath}
15190
15720
  Use /init --force to overwrite.` });
15191
15721
  break;
@@ -15218,7 +15748,7 @@ Use /context reload to load it.` });
15218
15748
  lines.push("**Config Files:**");
15219
15749
  lines.push(` Dir: ${configDir}`);
15220
15750
  const checkFile = (label, filePath) => {
15221
- const exists = existsSync23(filePath);
15751
+ const exists = existsSync25(filePath);
15222
15752
  let extra = "";
15223
15753
  if (exists) {
15224
15754
  try {
@@ -15228,9 +15758,9 @@ Use /context reload to load it.` });
15228
15758
  }
15229
15759
  lines.push(` ${exists ? "\u2713" : "\u2013"} ${label.padEnd(14)} ${exists ? filePath + extra : "(not found)"}`);
15230
15760
  };
15231
- checkFile("config.json", join17(configDir, "config.json"));
15232
- checkFile("memory.md", join17(configDir, MEMORY_FILE_NAME));
15233
- checkFile("dev-state.md", join17(configDir, "dev-state.md"));
15761
+ checkFile("config.json", join19(configDir, "config.json"));
15762
+ checkFile("memory.md", join19(configDir, MEMORY_FILE_NAME));
15763
+ checkFile("dev-state.md", join19(configDir, "dev-state.md"));
15234
15764
  lines.push("");
15235
15765
  if (this.mcpManager) {
15236
15766
  lines.push("**MCP Servers:**");
@@ -15417,8 +15947,8 @@ Use /add-dir remove to clear.` : "No directories added.\nUsage: /add-dir <path>
15417
15947
  this.send({ type: "info", message: "\u2713 All added directories removed from context." });
15418
15948
  break;
15419
15949
  }
15420
- const dirPath = resolve7(sub);
15421
- if (!existsSync23(dirPath)) {
15950
+ const dirPath = resolve8(sub);
15951
+ if (!existsSync25(dirPath)) {
15422
15952
  this.send({ type: "error", message: `Directory not found: ${dirPath}` });
15423
15953
  break;
15424
15954
  }
@@ -15434,8 +15964,8 @@ It will be included in AI context for subsequent messages.` });
15434
15964
  // ── /commands ───────────────────────────────────────────────────
15435
15965
  case "commands": {
15436
15966
  const configDir = this.config.getConfigDir();
15437
- const commandsDir = join17(configDir, CUSTOM_COMMANDS_DIR_NAME);
15438
- if (!existsSync23(commandsDir)) {
15967
+ const commandsDir = join19(configDir, CUSTOM_COMMANDS_DIR_NAME);
15968
+ if (!existsSync25(commandsDir)) {
15439
15969
  this.send({ type: "info", message: `No custom commands directory.
15440
15970
  Create: ${commandsDir}/ with .md files.` });
15441
15971
  break;
@@ -15461,7 +15991,7 @@ Add .md files to create commands.` });
15461
15991
  // ── /plugins ────────────────────────────────────────────────────
15462
15992
  case "plugins": {
15463
15993
  const configDir = this.config.getConfigDir();
15464
- const pluginsDir = join17(configDir, PLUGINS_DIR_NAME);
15994
+ const pluginsDir = join19(configDir, PLUGINS_DIR_NAME);
15465
15995
  const pluginTools = this.toolRegistry.listPluginTools();
15466
15996
  const lines = [`\u{1F50C} **Plugins:**`, `Dir: ${pluginsDir}`, ""];
15467
15997
  if (pluginTools.length === 0) {
@@ -15516,6 +16046,21 @@ Add .md files to create commands.` });
15516
16046
  this.send({ type: "info", message: "Not enough messages to compact." });
15517
16047
  return;
15518
16048
  }
16049
+ const preDecisions = runLifecycleHooks(
16050
+ this.config.get("hooks") ?? void 0,
16051
+ "PreCompact",
16052
+ { sessionId: session.id, messageCount: session.messages.length, instruction },
16053
+ { configDir: this.config.getConfigDir(), onSummary: (message) => this.send({ type: "info", message: "[hook] " + message }) }
16054
+ );
16055
+ const preDeny = preDecisions.find((d) => d.action === "deny");
16056
+ if (preDeny) {
16057
+ this.send({ type: "error", message: "Compact blocked by hook: " + (preDeny.reason ?? "PreCompact denied") });
16058
+ return;
16059
+ }
16060
+ for (const d of preDecisions) {
16061
+ const warnings = [d.warning, ...d.warnings ?? []].filter(Boolean);
16062
+ for (const warning of warnings) this.send({ type: "info", message: "[hook] " + warning });
16063
+ }
15519
16064
  const provider = this.providers.get(this.currentProvider);
15520
16065
  if (!provider) return;
15521
16066
  this.send({ type: "info", message: "Compacting conversation..." });
@@ -15535,7 +16080,15 @@ Add .md files to create commands.` });
15535
16080
  });
15536
16081
  const summaryMsg = { role: "user", content: "[Conversation summary]", timestamp: /* @__PURE__ */ new Date() };
15537
16082
  const ackMsg = { role: "assistant", content: response.content, timestamp: /* @__PURE__ */ new Date() };
15538
- session.compact(summaryMsg, ackMsg, keepLast);
16083
+ const beforeCount = session.messages.length;
16084
+ const removedCount = session.compact(summaryMsg, ackMsg, keepLast);
16085
+ const afterCount = session.messages.length;
16086
+ runLifecycleHooks(
16087
+ this.config.get("hooks") ?? void 0,
16088
+ "PostCompact",
16089
+ { sessionId: session.id, beforeCount, afterCount, removedCount, keepLast, instruction },
16090
+ { configDir: this.config.getConfigDir(), onSummary: (message) => this.send({ type: "info", message: "[hook] " + message }) }
16091
+ );
15539
16092
  this.send({ type: "info", message: `Compacted: removed ${toSummarize.length} messages, kept last ${keepLast}.` });
15540
16093
  this.sendStatus();
15541
16094
  } catch (err) {
@@ -15635,11 +16188,11 @@ Add .md files to create commands.` });
15635
16188
  }
15636
16189
  memoryShow() {
15637
16190
  const configDir = this.config.getConfigDir();
15638
- const memPath = join17(configDir, MEMORY_FILE_NAME);
16191
+ const memPath = join19(configDir, MEMORY_FILE_NAME);
15639
16192
  let content = "";
15640
16193
  try {
15641
- if (existsSync23(memPath)) {
15642
- content = readFileSync17(memPath, "utf-8");
16194
+ if (existsSync25(memPath)) {
16195
+ content = readFileSync19(memPath, "utf-8");
15643
16196
  }
15644
16197
  } catch (err) {
15645
16198
  process.stderr.write(`[web] Failed to read memory file: ${err instanceof Error ? err.message : err}
@@ -15653,11 +16206,11 @@ Add .md files to create commands.` });
15653
16206
  }
15654
16207
  memoryAdd(text) {
15655
16208
  const configDir = this.config.getConfigDir();
15656
- const memPath = join17(configDir, MEMORY_FILE_NAME);
16209
+ const memPath = join19(configDir, MEMORY_FILE_NAME);
15657
16210
  try {
15658
- mkdirSync10(configDir, { recursive: true });
16211
+ mkdirSync11(configDir, { recursive: true });
15659
16212
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace("T", " ");
15660
- const previous = existsSync23(memPath) ? readFileSync17(memPath, "utf-8") : "";
16213
+ const previous = existsSync25(memPath) ? readFileSync19(memPath, "utf-8") : "";
15661
16214
  const entry = `
15662
16215
  - [${timestamp}] ${text}
15663
16216
  `;
@@ -15669,7 +16222,7 @@ Add .md files to create commands.` });
15669
16222
  }
15670
16223
  memoryClear() {
15671
16224
  const configDir = this.config.getConfigDir();
15672
- const memPath = join17(configDir, MEMORY_FILE_NAME);
16225
+ const memPath = join19(configDir, MEMORY_FILE_NAME);
15673
16226
  try {
15674
16227
  atomicWriteFileSync(memPath, "");
15675
16228
  this.send({ type: "info", message: "\u{1F5D1}\uFE0F Persistent memory cleared." });
@@ -15687,7 +16240,7 @@ Add .md files to create commands.` });
15687
16240
  return;
15688
16241
  }
15689
16242
  try {
15690
- const { searchChatMemory: searchChatMemory2, loadChatIndex: loadChatIndex2 } = await import("./chat-index-UBCWHBLR.js");
16243
+ const { searchChatMemory: searchChatMemory2, loadChatIndex: loadChatIndex2 } = await import("./chat-index-R2E27VXN.js");
15691
16244
  const loaded = loadChatIndex2();
15692
16245
  if (!loaded || loaded.idx.chunks.length === 0) {
15693
16246
  this.send({ type: "memory_hits", query: q, hits: [], indexMissing: true });
@@ -15723,7 +16276,7 @@ Add .md files to create commands.` });
15723
16276
  }
15724
16277
  async handleMemoryStatus() {
15725
16278
  try {
15726
- const { getChatIndexStatus } = await import("./chat-index-UBCWHBLR.js");
16279
+ const { getChatIndexStatus } = await import("./chat-index-R2E27VXN.js");
15727
16280
  const s = getChatIndexStatus();
15728
16281
  this.send({
15729
16282
  type: "memory_status",
@@ -15748,7 +16301,7 @@ Add .md files to create commands.` });
15748
16301
  type: "info",
15749
16302
  message: full ? "\u{1F9E0} Rebuilding chat memory index (this may take a while on first run \u2014 ~117 MB embedder)." : "\u{1F9E0} Refreshing chat memory index (incremental)\u2026"
15750
16303
  });
15751
- const { buildChatIndex } = await import("./chat-index-UBCWHBLR.js");
16304
+ const { buildChatIndex } = await import("./chat-index-R2E27VXN.js");
15752
16305
  const stats = await buildChatIndex({
15753
16306
  full,
15754
16307
  onProgress: (p) => {
@@ -15903,8 +16456,8 @@ async function setupProxy(configProxy) {
15903
16456
  }
15904
16457
 
15905
16458
  // src/web/auth.ts
15906
- import { existsSync as existsSync24, readFileSync as readFileSync18, writeFileSync as writeFileSync3, mkdirSync as mkdirSync11, readdirSync as readdirSync11, copyFileSync } from "fs";
15907
- import { join as join18 } from "path";
16459
+ import { existsSync as existsSync26, readFileSync as readFileSync20, writeFileSync as writeFileSync3, mkdirSync as mkdirSync12, readdirSync as readdirSync11, copyFileSync } from "fs";
16460
+ import { join as join20 } from "path";
15908
16461
  import { createHmac, randomBytes, timingSafeEqual, pbkdf2Sync } from "crypto";
15909
16462
  var USERS_FILE = "users.json";
15910
16463
  var TOKEN_EXPIRY_HOURS = 24;
@@ -15919,7 +16472,7 @@ var AuthManager = class {
15919
16472
  db;
15920
16473
  constructor(baseDir) {
15921
16474
  this.baseDir = baseDir;
15922
- this.usersFile = join18(baseDir, USERS_FILE);
16475
+ this.usersFile = join20(baseDir, USERS_FILE);
15923
16476
  this.db = this.loadOrCreate();
15924
16477
  }
15925
16478
  // ── Public API ─────────────────────────────────────────────────
@@ -15948,9 +16501,9 @@ var AuthManager = class {
15948
16501
  }
15949
16502
  const salt = randomBytes(16).toString("hex");
15950
16503
  const passwordHash = this.hashPassword(password, salt);
15951
- const dataDir = join18(USERS_DIR, username);
15952
- const fullDataDir = join18(this.baseDir, dataDir);
15953
- mkdirSync11(join18(fullDataDir, "history"), { recursive: true });
16504
+ const dataDir = join20(USERS_DIR, username);
16505
+ const fullDataDir = join20(this.baseDir, dataDir);
16506
+ mkdirSync12(join20(fullDataDir, "history"), { recursive: true });
15954
16507
  const user = {
15955
16508
  username,
15956
16509
  passwordHash,
@@ -16065,7 +16618,7 @@ var AuthManager = class {
16065
16618
  getUserDataDir(username) {
16066
16619
  const user = this.db.users.find((u) => u.username === username);
16067
16620
  if (!user) throw new Error(`User not found: ${username}`);
16068
- return join18(this.baseDir, user.dataDir);
16621
+ return join20(this.baseDir, user.dataDir);
16069
16622
  }
16070
16623
  /** List all usernames */
16071
16624
  listUsers() {
@@ -16101,30 +16654,30 @@ var AuthManager = class {
16101
16654
  const err = this.register(username, password);
16102
16655
  if (err) return err;
16103
16656
  const userDir = this.getUserDataDir(username);
16104
- const globalConfig = join18(this.baseDir, "config.json");
16105
- if (existsSync24(globalConfig)) {
16657
+ const globalConfig = join20(this.baseDir, "config.json");
16658
+ if (existsSync26(globalConfig)) {
16106
16659
  try {
16107
- const content = readFileSync18(globalConfig, "utf-8");
16108
- writeFileSync3(join18(userDir, "config.json"), content, "utf-8");
16660
+ const content = readFileSync20(globalConfig, "utf-8");
16661
+ writeFileSync3(join20(userDir, "config.json"), content, "utf-8");
16109
16662
  } catch {
16110
16663
  }
16111
16664
  }
16112
- const globalMemory = join18(this.baseDir, "memory.md");
16113
- if (existsSync24(globalMemory)) {
16665
+ const globalMemory = join20(this.baseDir, "memory.md");
16666
+ if (existsSync26(globalMemory)) {
16114
16667
  try {
16115
- const content = readFileSync18(globalMemory, "utf-8");
16116
- writeFileSync3(join18(userDir, "memory.md"), content, "utf-8");
16668
+ const content = readFileSync20(globalMemory, "utf-8");
16669
+ writeFileSync3(join20(userDir, "memory.md"), content, "utf-8");
16117
16670
  } catch {
16118
16671
  }
16119
16672
  }
16120
- const globalHistory = join18(this.baseDir, "history");
16121
- if (existsSync24(globalHistory)) {
16673
+ const globalHistory = join20(this.baseDir, "history");
16674
+ if (existsSync26(globalHistory)) {
16122
16675
  try {
16123
16676
  const files = readdirSync11(globalHistory).filter((f) => f.endsWith(".json"));
16124
- const userHistory = join18(userDir, "history");
16677
+ const userHistory = join20(userDir, "history");
16125
16678
  for (const f of files) {
16126
16679
  try {
16127
- copyFileSync(join18(globalHistory, f), join18(userHistory, f));
16680
+ copyFileSync(join20(globalHistory, f), join20(userHistory, f));
16128
16681
  } catch {
16129
16682
  }
16130
16683
  }
@@ -16135,9 +16688,9 @@ var AuthManager = class {
16135
16688
  }
16136
16689
  // ── Private methods ────────────────────────────────────────────
16137
16690
  loadOrCreate() {
16138
- if (existsSync24(this.usersFile)) {
16691
+ if (existsSync26(this.usersFile)) {
16139
16692
  try {
16140
- return JSON.parse(readFileSync18(this.usersFile, "utf-8"));
16693
+ return JSON.parse(readFileSync20(this.usersFile, "utf-8"));
16141
16694
  } catch {
16142
16695
  }
16143
16696
  }
@@ -16153,7 +16706,7 @@ var AuthManager = class {
16153
16706
  this.saveDB(this.db);
16154
16707
  }
16155
16708
  saveDB(db) {
16156
- mkdirSync11(this.baseDir, { recursive: true });
16709
+ mkdirSync12(this.baseDir, { recursive: true });
16157
16710
  atomicWriteFileSync(this.usersFile, JSON.stringify(db, null, 2));
16158
16711
  }
16159
16712
  /** Legacy hash — kept only for migrating old users (v0.2.x) */
@@ -16189,7 +16742,7 @@ function getModuleDir() {
16189
16742
  }
16190
16743
  } catch {
16191
16744
  }
16192
- return resolve8(".");
16745
+ return resolve9(".");
16193
16746
  }
16194
16747
  async function startWebServer(options = {}) {
16195
16748
  const port = options.port ?? 3e3;
@@ -16254,8 +16807,8 @@ async function startWebServer(options = {}) {
16254
16807
  }
16255
16808
  }
16256
16809
  let skillManager = null;
16257
- const skillsDir = join19(config.getConfigDir(), SKILLS_DIR_NAME);
16258
- if (existsSync25(skillsDir)) {
16810
+ const skillsDir = join21(config.getConfigDir(), SKILLS_DIR_NAME);
16811
+ if (existsSync27(skillsDir)) {
16259
16812
  skillManager = new SkillManager(skillsDir, config.get("ui").skillSizeWarn);
16260
16813
  skillManager.loadSkills();
16261
16814
  const count = skillManager.listSkills().length;
@@ -16326,18 +16879,18 @@ async function startWebServer(options = {}) {
16326
16879
  next();
16327
16880
  };
16328
16881
  const moduleDir = getModuleDir();
16329
- let clientDir = join19(moduleDir, "web", "client");
16330
- if (!existsSync25(clientDir)) {
16331
- clientDir = join19(moduleDir, "client");
16882
+ let clientDir = join21(moduleDir, "web", "client");
16883
+ if (!existsSync27(clientDir)) {
16884
+ clientDir = join21(moduleDir, "client");
16332
16885
  }
16333
- if (!existsSync25(clientDir)) {
16334
- clientDir = join19(moduleDir, "..", "..", "src", "web", "client");
16886
+ if (!existsSync27(clientDir)) {
16887
+ clientDir = join21(moduleDir, "..", "..", "src", "web", "client");
16335
16888
  }
16336
- if (!existsSync25(clientDir)) {
16337
- clientDir = join19(process.cwd(), "src", "web", "client");
16889
+ if (!existsSync27(clientDir)) {
16890
+ clientDir = join21(process.cwd(), "src", "web", "client");
16338
16891
  }
16339
16892
  console.log(` Static files: ${clientDir}`);
16340
- app.use("/vendor", express.static(join19(clientDir, "vendor"), { maxAge: "1y", immutable: true }));
16893
+ app.use("/vendor", express.static(join21(clientDir, "vendor"), { maxAge: "1y", immutable: true }));
16341
16894
  app.use(express.static(clientDir));
16342
16895
  app.get("/api/status", (_req, res) => {
16343
16896
  res.json({
@@ -16404,10 +16957,10 @@ async function startWebServer(options = {}) {
16404
16957
  app.get("/api/files", requireAuth, (req, res) => {
16405
16958
  const cwd = process.cwd();
16406
16959
  const prefix = req.query.prefix || "";
16407
- const targetDir = join19(cwd, prefix);
16960
+ const targetDir = join21(cwd, prefix);
16408
16961
  try {
16409
- const canonicalTarget = realpathSync(resolve8(targetDir));
16410
- const canonicalCwd = realpathSync(resolve8(cwd));
16962
+ const canonicalTarget = realpathSync(resolve9(targetDir));
16963
+ const canonicalCwd = realpathSync(resolve9(cwd));
16411
16964
  if (!canonicalTarget.startsWith(canonicalCwd + sep3) && canonicalTarget !== canonicalCwd) {
16412
16965
  res.json({ files: [] });
16413
16966
  return;
@@ -16421,7 +16974,7 @@ async function startWebServer(options = {}) {
16421
16974
  const entries = readdirSync12(targetDir, { withFileTypes: true });
16422
16975
  const files = entries.filter((e) => !SKIP.has(e.name) && !e.name.startsWith(".")).slice(0, 50).map((e) => ({
16423
16976
  name: e.name,
16424
- path: relative4(cwd, join19(targetDir, e.name)).replace(/\\/g, "/"),
16977
+ path: relative4(cwd, join21(targetDir, e.name)).replace(/\\/g, "/"),
16425
16978
  isDir: e.isDirectory()
16426
16979
  }));
16427
16980
  res.json({ files });
@@ -16457,8 +17010,8 @@ async function startWebServer(options = {}) {
16457
17010
  try {
16458
17011
  const authUser = req._authUser;
16459
17012
  const histDir = authUser ? getUserShared(authUser).config.getHistoryDir() : config.getHistoryDir();
16460
- const filePath = join19(histDir, `${id}.json`);
16461
- if (!existsSync25(filePath)) {
17013
+ const filePath = join21(histDir, `${id}.json`);
17014
+ if (!existsSync27(filePath)) {
16462
17015
  res.status(404).json({ error: "Session not found" });
16463
17016
  return;
16464
17017
  }
@@ -16473,7 +17026,7 @@ async function startWebServer(options = {}) {
16473
17026
  res.status(404).json({ error: "Session not found" });
16474
17027
  return;
16475
17028
  }
16476
- const data = JSON.parse(readFileSync19(filePath, "utf-8"));
17029
+ const data = JSON.parse(readFileSync21(filePath, "utf-8"));
16477
17030
  res.json({ session: data });
16478
17031
  } catch (err) {
16479
17032
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
@@ -16486,10 +17039,10 @@ async function startWebServer(options = {}) {
16486
17039
  return;
16487
17040
  }
16488
17041
  const cwd = process.cwd();
16489
- const fullPath = resolve8(join19(cwd, filePath));
17042
+ const fullPath = resolve9(join21(cwd, filePath));
16490
17043
  try {
16491
17044
  const canonicalFull = realpathSync(fullPath);
16492
- const canonicalCwd = realpathSync(resolve8(cwd));
17045
+ const canonicalCwd = realpathSync(resolve9(cwd));
16493
17046
  if (!canonicalFull.startsWith(canonicalCwd + sep3) && canonicalFull !== canonicalCwd) {
16494
17047
  res.json({ error: "Access denied" });
16495
17048
  return;
@@ -16504,7 +17057,7 @@ async function startWebServer(options = {}) {
16504
17057
  res.json({ error: `File too large (${(stat.size / 1024).toFixed(0)} KB, max 512 KB)` });
16505
17058
  return;
16506
17059
  }
16507
- const content = readFileSync19(fullPath, "utf-8");
17060
+ const content = readFileSync21(fullPath, "utf-8");
16508
17061
  res.json({ content, size: stat.size });
16509
17062
  } catch {
16510
17063
  res.json({ error: "Cannot read file" });
@@ -16708,7 +17261,7 @@ async function startWebServer(options = {}) {
16708
17261
  });
16709
17262
  const MAX_PORT_ATTEMPTS = 10;
16710
17263
  let actualPort = port;
16711
- const result = await new Promise((resolve9, reject) => {
17264
+ const result = await new Promise((resolve10, reject) => {
16712
17265
  const tryListen = (attempt) => {
16713
17266
  server.once("error", (err) => {
16714
17267
  if (err.code === "EADDRINUSE" && attempt < MAX_PORT_ATTEMPTS) {
@@ -16739,7 +17292,7 @@ async function startWebServer(options = {}) {
16739
17292
  }
16740
17293
  console.log(` Press Ctrl+C to stop
16741
17294
  `);
16742
- resolve9({ port: actualPort, host, url });
17295
+ resolve10({ port: actualPort, host, url });
16743
17296
  });
16744
17297
  };
16745
17298
  tryListen(1);
@@ -16759,17 +17312,17 @@ function resolveProjectMcpPath() {
16759
17312
  const cwd = process.cwd();
16760
17313
  const gitRoot = getGitRoot(cwd);
16761
17314
  const projectRoot = gitRoot ?? cwd;
16762
- const configPath = join19(projectRoot, MCP_PROJECT_CONFIG_NAME);
16763
- return existsSync25(configPath) ? configPath : null;
17315
+ const configPath = join21(projectRoot, MCP_PROJECT_CONFIG_NAME);
17316
+ return existsSync27(configPath) ? configPath : null;
16764
17317
  }
16765
17318
  function loadProjectMcpConfig() {
16766
17319
  const cwd = process.cwd();
16767
17320
  const gitRoot = getGitRoot(cwd);
16768
17321
  const projectRoot = gitRoot ?? cwd;
16769
- const configPath = join19(projectRoot, MCP_PROJECT_CONFIG_NAME);
16770
- if (!existsSync25(configPath)) return null;
17322
+ const configPath = join21(projectRoot, MCP_PROJECT_CONFIG_NAME);
17323
+ if (!existsSync27(configPath)) return null;
16771
17324
  try {
16772
- const raw = JSON.parse(readFileSync19(configPath, "utf-8"));
17325
+ const raw = JSON.parse(readFileSync21(configPath, "utf-8"));
16773
17326
  return raw.mcpServers ?? raw;
16774
17327
  } catch {
16775
17328
  return null;