jinzd-ai-cli 0.4.211 → 0.4.212

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/README.md +23 -0
  2. package/README.zh-CN.md +23 -0
  3. package/dist/{batch-DE4RXKZD.js → batch-WIKFEOAZ.js} +2 -2
  4. package/dist/{chat-index-UBCWHBLR.js → chat-index-R2E27VXN.js} +1 -1
  5. package/dist/{chunk-E5XCM4A6.js → chunk-GBBVCZ4W.js} +1 -1
  6. package/dist/{chunk-C3OU2OPF.js → chunk-HW2ALWQJ.js} +2 -2
  7. package/dist/{chunk-6NS6643Y.js → chunk-HX6N6QEY.js} +1 -1
  8. package/dist/{chunk-JBWA73GK.js → chunk-IFZX26K7.js} +1 -1
  9. package/dist/{chunk-ZY2N2N6T.js → chunk-KE4B3NOQ.js} +1 -1
  10. package/dist/{chunk-BE6ERF7M.js → chunk-LU6FBJQ5.js} +1 -1
  11. package/dist/{chunk-ZOPYREL5.js → chunk-N5LB3PPL.js} +258 -52
  12. package/dist/{chunk-UOROWTGG.js → chunk-NYCBOVNF.js} +26 -3
  13. package/dist/{chunk-W7UKO3PS.js → chunk-OQGVGPEK.js} +5 -0
  14. package/dist/{ci-XMUEX526.js → ci-X3LNUFZV.js} +2 -2
  15. package/dist/{constants-AWTIQIWG.js → constants-KDWG4KR4.js} +1 -1
  16. package/dist/{doctor-cli-SSI6ETFT.js → doctor-cli-TJTWIW7U.js} +4 -4
  17. package/dist/electron-server.js +468 -174
  18. package/dist/{hub-INUJND2G.js → hub-JWSV4MKC.js} +1 -1
  19. package/dist/index.js +142 -17
  20. package/dist/{run-tests-PACN4UYX.js → run-tests-D7YBY4XB.js} +1 -1
  21. package/dist/{run-tests-3MHWUF43.js → run-tests-H4Q2PRX6.js} +2 -2
  22. package/dist/{server-7USZJJAH.js → server-OFKGDRYI.js} +4 -4
  23. package/dist/{server-BTSKOPQI.js → server-VNCN7AY5.js} +99 -10
  24. package/dist/{task-orchestrator-OV4O25MX.js → task-orchestrator-AL2E642M.js} +4 -4
  25. package/dist/{usage-T2P6FTE7.js → usage-LB6REWPO.js} +2 -2
  26. package/dist/web/client/app.js +1 -1
  27. package/package.json +1 -1
@@ -154,7 +154,7 @@ ${content}`);
154
154
  }
155
155
  }
156
156
  async function runTaskMode(config, providers, configManager, topic) {
157
- const { TaskOrchestrator } = await import("./task-orchestrator-OV4O25MX.js");
157
+ const { TaskOrchestrator } = await import("./task-orchestrator-AL2E642M.js");
158
158
  const orchestrator = new TaskOrchestrator(config, providers, configManager);
159
159
  let interrupted = false;
160
160
  const onSigint = () => {
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  scanProject,
16
16
  sessionHasMeaningfulContent,
17
17
  setupProxy
18
- } from "./chunk-C3OU2OPF.js";
18
+ } from "./chunk-HW2ALWQJ.js";
19
19
  import {
20
20
  buildReviewPrompt,
21
21
  buildSecurityReviewPrompt
@@ -27,23 +27,28 @@ import {
27
27
  cleanupRejectedTeeFile,
28
28
  estimateTokens,
29
29
  formatPermissionProfileWarning,
30
+ getPendingHookTrust,
30
31
  googleSearchContext,
31
32
  initTheme,
32
33
  isInterrupted,
33
34
  lastResponseStore,
35
+ listHooks,
34
36
  renderDiff,
35
37
  requestInterrupt,
36
38
  resetInterrupt,
37
39
  rlInternal,
40
+ runLifecycleHooks,
38
41
  setContextWindow,
39
42
  setMaxOutputCap,
40
43
  spawnAgentContext,
41
44
  theme,
42
- undoStack
43
- } from "./chunk-ZOPYREL5.js";
45
+ trustHook,
46
+ undoStack,
47
+ untrustHook
48
+ } from "./chunk-N5LB3PPL.js";
44
49
  import "./chunk-T2NL5ZIA.js";
45
50
  import "./chunk-BXP6YZ2P.js";
46
- import "./chunk-6NS6643Y.js";
51
+ import "./chunk-HX6N6QEY.js";
47
52
  import {
48
53
  SessionManager,
49
54
  getContentText
@@ -60,7 +65,7 @@ import {
60
65
  getConfigDirUsage,
61
66
  listRecentCrashes,
62
67
  writeCrashLog
63
- } from "./chunk-BE6ERF7M.js";
68
+ } from "./chunk-LU6FBJQ5.js";
64
69
  import {
65
70
  ProviderRegistry
66
71
  } from "./chunk-QMXC327F.js";
@@ -69,7 +74,7 @@ import {
69
74
  getTopFailingTools,
70
75
  getTopUsedTools,
71
76
  installFlushOnExit
72
- } from "./chunk-JBWA73GK.js";
77
+ } from "./chunk-IFZX26K7.js";
73
78
  import {
74
79
  CONTENT_ONLY_STREAM_REMINDER,
75
80
  TEE_FINAL_USER_NUDGE,
@@ -91,7 +96,7 @@ import {
91
96
  } from "./chunk-XPBEJB27.js";
92
97
  import {
93
98
  ConfigManager
94
- } from "./chunk-UOROWTGG.js";
99
+ } from "./chunk-NYCBOVNF.js";
95
100
  import {
96
101
  AuthError,
97
102
  ProviderError,
@@ -117,7 +122,7 @@ import {
117
122
  SKILLS_DIR_NAME,
118
123
  VERSION,
119
124
  buildUserIdentityPrompt
120
- } from "./chunk-E5XCM4A6.js";
125
+ } from "./chunk-GBBVCZ4W.js";
121
126
  import {
122
127
  formatGitContextForPrompt,
123
128
  getGitContext,
@@ -934,6 +939,7 @@ function createDefaultCommands() {
934
939
  ["/config [set|get|show]", "Config wizard / get / set / show all"],
935
940
  ["/profile [show|set|clear]", "View or edit your identity"],
936
941
  ["/security [status|scan|on|off]", "Sensitive-data redaction"],
942
+ ["/hooks [list|trust|untrust]", "Lifecycle hooks and project hook trust"],
937
943
  ["/doctor", "Health check (API keys, config, MCP, crashes, tool stats)"],
938
944
  ["/bug [--copy]", "Generate bug report template"],
939
945
  ["/about", "About ai-cli & author info"],
@@ -1356,6 +1362,7 @@ No commands match "${filter}".
1356
1362
  console.log(` Ctx Win : ${ctxStr}`);
1357
1363
  console.log(" PermProf : " + (ctx.config.get("defaultPermissionProfile") ?? "legacy"));
1358
1364
  console.log(" Network : " + (ctx.config.get("networkPolicy")?.enabled ?? false ? "on" : "off"));
1365
+ console.log(" Hooks : " + getPendingHookTrust(ctx.config.get("hooks") ?? void 0, ctx.config.getConfigDir()).length + " pending trust");
1359
1366
  console.log(` Messages : ${session?.messages.length ?? 0}`);
1360
1367
  console.log(` Session : ${session?.id.slice(0, 8) ?? "none"}`);
1361
1368
  if (layers.length > 0) {
@@ -1649,7 +1656,7 @@ No tools match "${filter}".
1649
1656
  const { join: join5 } = await import("path");
1650
1657
  const { existsSync: existsSync5 } = await import("fs");
1651
1658
  const { getGitRoot: getGitRoot2 } = await import("./git-context-EXOEHQSF.js");
1652
- const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-AWTIQIWG.js");
1659
+ const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-KDWG4KR4.js");
1653
1660
  const { approveProject, hashMcpFile } = await import("./project-trust-NKYHL3VZ.js");
1654
1661
  const cwd = process.cwd();
1655
1662
  const projectRoot = getGitRoot2(cwd) ?? cwd;
@@ -2708,7 +2715,7 @@ ${hint}` : "")
2708
2715
  usage: "/test [command|filter]",
2709
2716
  async execute(args, ctx) {
2710
2717
  try {
2711
- const { executeTests } = await import("./run-tests-3MHWUF43.js");
2718
+ const { executeTests } = await import("./run-tests-H4Q2PRX6.js");
2712
2719
  const argStr = args.join(" ").trim();
2713
2720
  let testArgs = {};
2714
2721
  if (argStr) {
@@ -3287,6 +3294,71 @@ Summary: ${fileMap.size} file(s) \u2014 ${newFiles} new, ${modifiedFiles} modifi
3287
3294
  }
3288
3295
  }
3289
3296
  },
3297
+ {
3298
+ name: "hooks",
3299
+ description: "Manage lifecycle hooks and project hook trust",
3300
+ usage: "/hooks [list|inspect <id>|trust <id>|untrust <id>|disable]",
3301
+ execute(args, ctx) {
3302
+ const sub = (args[0] || "list").toLowerCase();
3303
+ const hooks = listHooks(ctx.config.get("hooks") ?? void 0, ctx.config.getConfigDir());
3304
+ const findHook = (id) => hooks.find((h) => h.id === id || h.hash.startsWith(id ?? ""));
3305
+ if (sub === "list") {
3306
+ console.log(theme.heading(" Hooks"));
3307
+ if (hooks.length === 0) {
3308
+ console.log(theme.dim(" No hooks configured."));
3309
+ } else {
3310
+ for (const h of hooks) {
3311
+ const trust = h.disabled ? theme.dim("disabled") : h.trusted ? theme.success("trusted") : theme.warning("pending");
3312
+ console.log(` ${h.id.padEnd(18)} ${h.source.padEnd(7)} ${trust} ${h.hash.slice(0, 12)} ${theme.dim(h.command.slice(0, 80))}`);
3313
+ }
3314
+ }
3315
+ console.log();
3316
+ return;
3317
+ }
3318
+ if (sub === "inspect") {
3319
+ const h = findHook(args[1]);
3320
+ if (!h) {
3321
+ console.log(theme.warning(" Hook not found. Use /hooks list."));
3322
+ return;
3323
+ }
3324
+ console.log(theme.heading(" Hook"));
3325
+ console.log(` id : ${h.id}`);
3326
+ console.log(` event : ${h.event}`);
3327
+ console.log(` source : ${h.source}`);
3328
+ console.log(` hash : ${h.hash}`);
3329
+ console.log(` trusted : ${h.trusted ? "yes" : "no"}`);
3330
+ console.log(` disabled: ${h.disabled ? "yes" : "no"}`);
3331
+ console.log(` command : ${h.command}`);
3332
+ console.log();
3333
+ return;
3334
+ }
3335
+ if (sub === "trust" || sub === "untrust") {
3336
+ const h = findHook(args[1]);
3337
+ if (!h) {
3338
+ console.log(theme.warning(" Hook not found. Use /hooks list."));
3339
+ return;
3340
+ }
3341
+ if (sub === "trust") {
3342
+ trustHook(ctx.config.getConfigDir(), h);
3343
+ console.log(theme.success(` \u2713 trusted ${h.id} (${h.hash.slice(0, 12)})`));
3344
+ } else {
3345
+ untrustHook(ctx.config.getConfigDir(), h);
3346
+ console.log(theme.success(` \u2713 untrusted ${h.id}`));
3347
+ }
3348
+ console.log();
3349
+ return;
3350
+ }
3351
+ if (sub === "disable") {
3352
+ const cfg = ctx.config.get("hooks") ?? {};
3353
+ ctx.config.set("hooks", { ...cfg, enabled: false });
3354
+ console.log(theme.success(" \u2713 hooks disabled in user config"));
3355
+ console.log();
3356
+ return;
3357
+ }
3358
+ console.log(theme.warning(" Usage: /hooks [list|inspect <id>|trust <id>|untrust <id>|disable]"));
3359
+ console.log();
3360
+ }
3361
+ },
3290
3362
  {
3291
3363
  name: "security",
3292
3364
  description: "Sensitive-data redaction \u2014 status, scan sessions, toggle on/off",
@@ -3306,6 +3378,7 @@ Summary: ${fileMap.size} file(s) \u2014 ${newFiles} new, ${modifiedFiles} modifi
3306
3378
  console.log(` mode : ${security.mode}`);
3307
3379
  console.log(" profile : " + (ctx.config.get("defaultPermissionProfile") ?? "legacy"));
3308
3380
  console.log(" network : " + (ctx.config.get("networkPolicy")?.enabled ?? false ? "on" : "off"));
3381
+ console.log(" hooks : " + getPendingHookTrust(ctx.config.get("hooks") ?? void 0, ctx.config.getConfigDir()).length + " pending trust");
3309
3382
  console.log(` patterns : ${DEFAULT_PATTERNS.length} built-in + ${security.customPatterns?.length ?? 0} custom`);
3310
3383
  console.log(` last save : ${ctx.sessions.lastRedactionHits} hit(s) redacted`);
3311
3384
  console.log();
@@ -4465,6 +4538,7 @@ var Repl = class {
4465
4538
  const permissionProfiles = this.config.get("permissionProfiles") ?? {};
4466
4539
  this.toolExecutor.setConfig({
4467
4540
  hookConfig: this.config.get("hooks") ?? void 0,
4541
+ hookConfigDir: this.config.getConfigDir(),
4468
4542
  permissionRules: this.config.get("permissionRules") ?? [],
4469
4543
  defaultPermission: this.config.get("defaultPermission") ?? "confirm",
4470
4544
  permissionProfileName,
@@ -4473,6 +4547,11 @@ var Repl = class {
4473
4547
  workspaceRoot: process.cwd(),
4474
4548
  networkPolicy: this.config.get("networkPolicy")
4475
4549
  });
4550
+ const pendingHooks = getPendingHookTrust(this.config.get("hooks") ?? void 0, this.config.getConfigDir());
4551
+ if (pendingHooks.length > 0) {
4552
+ process.stderr.write(theme.warning(" \u26A0 " + pendingHooks.length + " project hook(s) require trust. Run /hooks list and /hooks trust <id>.\n"));
4553
+ }
4554
+ runLifecycleHooks(this.config.get("hooks") ?? void 0, "SessionStart", { source: "repl" }, { configDir: this.config.getConfigDir() });
4476
4555
  if (this.config.get("alwaysYolo") === true) {
4477
4556
  this.toolExecutor.sessionAutoApprove = true;
4478
4557
  }
@@ -4944,6 +5023,21 @@ ${skillContent}`);
4944
5023
  );
4945
5024
  return false;
4946
5025
  }
5026
+ const preDecisions = runLifecycleHooks(
5027
+ this.config.get("hooks") ?? void 0,
5028
+ "PreCompact",
5029
+ { sessionId: session.id, messageCount: session.messages.length, instruction },
5030
+ { configDir: this.config.getConfigDir(), onSummary: (msg) => process.stdout.write(theme.dim(" [hook] " + msg) + "\n") }
5031
+ );
5032
+ const preDeny = preDecisions.find((d) => d.action === "deny");
5033
+ if (preDeny) {
5034
+ process.stdout.write(theme.warning(" \u26A0 Compact blocked by hook: " + (preDeny.reason ?? "PreCompact denied") + "\n"));
5035
+ return false;
5036
+ }
5037
+ for (const d of preDecisions) {
5038
+ const warnings = [d.warning, ...d.warnings ?? []].filter(Boolean);
5039
+ for (const w of warnings) process.stdout.write(theme.warning(" \u26A0 Hook warning: " + w + "\n"));
5040
+ }
4947
5041
  const provider = this.providers.get(this.currentProvider);
4948
5042
  const modelParams = this.getModelParams();
4949
5043
  const spinner = this.renderer.showSpinner("Compacting session...");
@@ -5005,6 +5099,12 @@ ${response.content.trim()}
5005
5099
  const tokensBefore = ctxWindow > 0 ? this.estimateConversationTokens() : 0;
5006
5100
  const removedCount = session.compact(summaryMsg, ackMsg, COMPACT_KEEP_LAST);
5007
5101
  const afterCount = session.messages.length;
5102
+ runLifecycleHooks(
5103
+ this.config.get("hooks") ?? void 0,
5104
+ "PostCompact",
5105
+ { sessionId: session.id, beforeCount, afterCount, removedCount, keepLast: COMPACT_KEEP_LAST, instruction },
5106
+ { configDir: this.config.getConfigDir(), onSummary: (msg) => process.stdout.write(theme.dim(" [hook] " + msg) + "\n") }
5107
+ );
5008
5108
  const tokensAfter = ctxWindow > 0 ? this.estimateConversationTokens() : 0;
5009
5109
  const pctBefore = ctxWindow > 0 ? Math.round(tokensBefore / ctxWindow * 100) : 0;
5010
5110
  const pctAfter = ctxWindow > 0 ? Math.round(tokensAfter / ctxWindow * 100) : 0;
@@ -5440,8 +5540,32 @@ Session '${this.resumeSessionId}' not found.
5440
5540
  });
5441
5541
  });
5442
5542
  }
5543
+ runUserPromptHooks(sessionId, userInput) {
5544
+ const promptDecisions = runLifecycleHooks(
5545
+ this.config.get("hooks") ?? void 0,
5546
+ "UserPromptSubmit",
5547
+ { sessionId, prompt: userInput },
5548
+ { configDir: this.config.getConfigDir(), onSummary: (msg) => process.stdout.write(theme.dim(" [hook] " + msg) + "\n") }
5549
+ );
5550
+ const promptDeny = promptDecisions.find((d) => d.action === "deny");
5551
+ if (promptDeny) {
5552
+ process.stdout.write(theme.warning(" \u26A0 Prompt blocked by hook: " + (promptDeny.reason ?? "UserPromptSubmit denied") + "\n"));
5553
+ return false;
5554
+ }
5555
+ for (const d of promptDecisions) {
5556
+ const warnings = [d.warning, ...d.warnings ?? []].filter(Boolean);
5557
+ for (const w of warnings) process.stdout.write(theme.warning(" \u26A0 Hook warning: " + w + "\n"));
5558
+ }
5559
+ const secretHits = scanString(userInput, { customRegexes: [] });
5560
+ if (secretHits.length > 0) {
5561
+ const kinds = [...new Set(secretHits.map((h) => h.kind))].join(", ");
5562
+ process.stdout.write(theme.warning(" \u26A0 Possible secret in prompt (" + kinds + "). It will be sent unless you interrupt now.\n"));
5563
+ }
5564
+ return true;
5565
+ }
5443
5566
  async handleChat(userInput) {
5444
5567
  const session = this.sessions.current;
5568
+ if (!this.runUserPromptHooks(session.id, userInput)) return;
5445
5569
  const { parts, hasImage, refs } = parseAtReferences(userInput, process.cwd());
5446
5570
  for (const ref of refs) {
5447
5571
  if (ref.type === "notfound") {
@@ -6906,6 +7030,7 @@ Tip: You can continue the conversation by asking the AI to proceed.`
6906
7030
  void this.shutdownAsync();
6907
7031
  }
6908
7032
  async shutdownAsync() {
7033
+ runLifecycleHooks(this.config.get("hooks") ?? void 0, "Stop", { sessionId: this.sessions.current?.id }, { configDir: this.config.getConfigDir() });
6909
7034
  this.running = false;
6910
7035
  const sessionId = this.sessions.current?.id;
6911
7036
  if (sessionId) {
@@ -7020,7 +7145,7 @@ program.command("web").description("Start Web UI server with browser-based chat
7020
7145
  console.error("Error: Invalid port number. Must be between 1 and 65535.");
7021
7146
  process.exit(1);
7022
7147
  }
7023
- const { startWebServer } = await import("./server-BTSKOPQI.js");
7148
+ const { startWebServer } = await import("./server-VNCN7AY5.js");
7024
7149
  await startWebServer({ port, host: options.host });
7025
7150
  });
7026
7151
  program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | logout-all <name> | migrate <name>)").action(async (action, username) => {
@@ -7187,16 +7312,16 @@ program.command("sessions").description("List recent conversation sessions").opt
7187
7312
  console.log(footer + "\n");
7188
7313
  });
7189
7314
  program.command("usage").description("Show token + cost usage grouped by provider/model (cross-session)").option("--days <n>", "Only the last N days (inclusive of today)").option("--month <ym>", "Only a specific month, format YYYY-MM (e.g. 2026-06)").option("--json", "Output as JSON (for scripting)").action(async (options) => {
7190
- const { runUsageCli } = await import("./usage-T2P6FTE7.js");
7315
+ const { runUsageCli } = await import("./usage-LB6REWPO.js");
7191
7316
  await runUsageCli(options);
7192
7317
  });
7193
7318
  program.command("doctor").description("Health check: API keys, config, MCP, recent crashes, tool usage, disk usage").option("--json", "Output as JSON (for scripting)").option("--reset-stats", "Reset accumulated tool usage statistics").action(async (options) => {
7194
- const { runDoctorCli } = await import("./doctor-cli-SSI6ETFT.js");
7319
+ const { runDoctorCli } = await import("./doctor-cli-TJTWIW7U.js");
7195
7320
  await runDoctorCli({ json: !!options.json, resetStats: !!options.resetStats });
7196
7321
  });
7197
7322
  program.command("batch <action> [arg] [arg2]").description("Anthropic Message Batches: submit | list | status <id> | results <id> [out] | cancel <id>").option("--dry-run", "Parse and validate input without submitting (submit only)").action(async (action, arg, arg2, options) => {
7198
7323
  try {
7199
- const batch = await import("./batch-DE4RXKZD.js");
7324
+ const batch = await import("./batch-WIKFEOAZ.js");
7200
7325
  switch (action) {
7201
7326
  case "submit":
7202
7327
  if (!arg) {
@@ -7239,7 +7364,7 @@ program.command("batch <action> [arg] [arg2]").description("Anthropic Message Ba
7239
7364
  }
7240
7365
  });
7241
7366
  program.command("mcp-serve").description("Start an MCP server over STDIO, exposing aicli's built-in tools to Claude Desktop / Cursor / other MCP clients").option("--allow-destructive", "Allow bash / run_interactive / task_create (always destructive in MCP mode)").option("--allow-outside-cwd", "Allow tool path arguments to escape the sandbox root \u2014 disabled by default").option("--tools <list>", "Comma-separated whitelist of tools to expose (default: all eligible tools)").option("--cwd <path>", "Working directory AND sandbox root (default: current directory)").action(async (options) => {
7242
- const { startMcpServer } = await import("./server-7USZJJAH.js");
7367
+ const { startMcpServer } = await import("./server-OFKGDRYI.js");
7243
7368
  await startMcpServer({
7244
7369
  allowDestructive: !!options.allowDestructive,
7245
7370
  allowOutsideCwd: !!options.allowOutsideCwd,
@@ -7248,7 +7373,7 @@ program.command("mcp-serve").description("Start an MCP server over STDIO, exposi
7248
7373
  });
7249
7374
  });
7250
7375
  program.command("ci").description("Headless PR review (code + security) \u2014 reads git/gh diff, optionally posts to PR. Designed for GitHub Actions.").option("--pr <num>", "PR number; diff fetched via `gh pr diff <num>`", (v) => parseInt(v, 10)).option("--base <ref>", "Base ref for `git diff <ref>...HEAD` (ignored when --pr set)").option("--post", "Post review as a PR comment (requires gh CLI + GH_TOKEN, needs --pr)").option("--no-update", "Always create a new comment instead of updating the previous aicli review").option("--skip-code", "Skip the code review section").option("--skip-security", "Skip the security review section").option("--detailed", "Use the detailed code-review prompt").option("--max-diff <n>", "Max diff chars sent to the model (default 30000)", (v) => parseInt(v, 10)).option("--provider <id>", "Override provider (default: config.defaultProvider)").option("--model <id>", "Override model").option("--dry-run", "Print result to stdout instead of posting (overrides --post)").action(async (options) => {
7251
- const { runCi } = await import("./ci-XMUEX526.js");
7376
+ const { runCi } = await import("./ci-X3LNUFZV.js");
7252
7377
  const result = await runCi({
7253
7378
  pr: options.pr,
7254
7379
  base: options.base,
@@ -7394,7 +7519,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
7394
7519
  }),
7395
7520
  config.get("customProviders")
7396
7521
  );
7397
- const { startHub } = await import("./hub-INUJND2G.js");
7522
+ const { startHub } = await import("./hub-JWSV4MKC.js");
7398
7523
  await startHub(
7399
7524
  {
7400
7525
  topic: topic ?? "",
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  executeTests,
3
3
  runTestsTool
4
- } from "./chunk-ZY2N2N6T.js";
4
+ } from "./chunk-KE4B3NOQ.js";
5
5
  export {
6
6
  executeTests,
7
7
  runTestsTool
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-6NS6643Y.js";
6
- import "./chunk-E5XCM4A6.js";
5
+ } from "./chunk-HX6N6QEY.js";
6
+ import "./chunk-GBBVCZ4W.js";
7
7
  export {
8
8
  executeTests,
9
9
  runTestsTool
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ToolRegistry
4
- } from "./chunk-ZOPYREL5.js";
4
+ } from "./chunk-N5LB3PPL.js";
5
5
  import "./chunk-T2NL5ZIA.js";
6
6
  import "./chunk-BXP6YZ2P.js";
7
- import "./chunk-6NS6643Y.js";
7
+ import "./chunk-HX6N6QEY.js";
8
8
  import {
9
9
  runTool
10
- } from "./chunk-JBWA73GK.js";
10
+ } from "./chunk-IFZX26K7.js";
11
11
  import {
12
12
  getDangerLevel,
13
13
  schemaToJsonSchema
@@ -15,7 +15,7 @@ import {
15
15
  import "./chunk-TZQHYZKT.js";
16
16
  import {
17
17
  VERSION
18
- } from "./chunk-E5XCM4A6.js";
18
+ } from "./chunk-GBBVCZ4W.js";
19
19
  import "./chunk-4BKXL7SM.js";
20
20
  import "./chunk-TB4W4Y4T.js";
21
21
  import "./chunk-KHYD3WXE.js";
@@ -24,7 +24,7 @@ import {
24
24
  scanDirTree,
25
25
  scanProject,
26
26
  setupProxy
27
- } from "./chunk-C3OU2OPF.js";
27
+ } from "./chunk-HW2ALWQJ.js";
28
28
  import {
29
29
  buildReviewPrompt,
30
30
  buildSecurityReviewPrompt
@@ -38,10 +38,12 @@ import {
38
38
  cleanupRejectedTeeFile,
39
39
  estimateTokens,
40
40
  formatPermissionProfileWarning,
41
+ getPendingHookTrust,
41
42
  googleSearchContext,
42
43
  groupCallsByPhase,
43
44
  renderDiff,
44
45
  runHook,
46
+ runLifecycleHooks,
45
47
  runSafePhases,
46
48
  runWithSessionKey,
47
49
  setContextWindow,
@@ -49,10 +51,10 @@ import {
49
51
  spawnAgentContext,
50
52
  truncateOutput,
51
53
  undoStack
52
- } from "./chunk-ZOPYREL5.js";
54
+ } from "./chunk-N5LB3PPL.js";
53
55
  import "./chunk-T2NL5ZIA.js";
54
56
  import "./chunk-BXP6YZ2P.js";
55
- import "./chunk-6NS6643Y.js";
57
+ import "./chunk-HX6N6QEY.js";
56
58
  import {
57
59
  SessionManager,
58
60
  getContentText
@@ -66,7 +68,7 @@ import {
66
68
  } from "./chunk-QMXC327F.js";
67
69
  import {
68
70
  runTool
69
- } from "./chunk-JBWA73GK.js";
71
+ } from "./chunk-IFZX26K7.js";
70
72
  import {
71
73
  CONTENT_ONLY_STREAM_REMINDER,
72
74
  TEE_FINAL_USER_NUDGE,
@@ -85,7 +87,7 @@ import {
85
87
  } from "./chunk-XPBEJB27.js";
86
88
  import {
87
89
  ConfigManager
88
- } from "./chunk-UOROWTGG.js";
90
+ } from "./chunk-NYCBOVNF.js";
89
91
  import "./chunk-TZQHYZKT.js";
90
92
  import {
91
93
  AGENTIC_BEHAVIOR_GUIDELINE,
@@ -104,7 +106,7 @@ import {
104
106
  SKILLS_DIR_NAME,
105
107
  VERSION,
106
108
  buildUserIdentityPrompt
107
- } from "./chunk-E5XCM4A6.js";
109
+ } from "./chunk-GBBVCZ4W.js";
108
110
  import {
109
111
  formatGitContextForPrompt,
110
112
  getGitContext,
@@ -113,7 +115,9 @@ import {
113
115
  import "./chunk-4BKXL7SM.js";
114
116
  import "./chunk-TB4W4Y4T.js";
115
117
  import "./chunk-KHYD3WXE.js";
116
- import "./chunk-SLSWPBK3.js";
118
+ import {
119
+ scanString
120
+ } from "./chunk-SLSWPBK3.js";
117
121
  import "./chunk-K3CF65QH.js";
118
122
  import "./chunk-CKH4KQ4E.js";
119
123
  import {
@@ -147,6 +151,7 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
147
151
  allowedPermissionProfiles = [];
148
152
  workspaceRoot = process.cwd();
149
153
  networkPolicy;
154
+ hookConfigDir = process.cwd();
150
155
  /** Pending confirm promises keyed by requestId */
151
156
  pendingConfirms = /* @__PURE__ */ new Map();
152
157
  /** Pending batch confirm promises */
@@ -186,6 +191,7 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
186
191
  if (opts.allowedPermissionProfiles) this.allowedPermissionProfiles = opts.allowedPermissionProfiles;
187
192
  if (opts.workspaceRoot) this.workspaceRoot = opts.workspaceRoot;
188
193
  if (opts.networkPolicy) this.networkPolicy = opts.networkPolicy;
194
+ if (opts.hookConfigDir) this.hookConfigDir = opts.hookConfigDir;
189
195
  }
190
196
  /** Clear M7 timeout timer for a requestId */
191
197
  clearPendingTimer(requestId) {
@@ -234,7 +240,6 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
234
240
  }
235
241
  sendToolCallStart(call) {
236
242
  const dangerLevel = getDangerLevel(call.name, call.arguments);
237
- let toolCallStarted = false;
238
243
  const startTime = Date.now();
239
244
  this.toolStartTimes.set(call.id, startTime);
240
245
  const msg = {
@@ -349,6 +354,26 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
349
354
  dangerLevel,
350
355
  args: JSON.stringify(call.arguments).slice(0, 200)
351
356
  });
357
+ const preToolDecisions = runLifecycleHooks(
358
+ this.hookConfig,
359
+ "PreToolUse",
360
+ { tool: call.name, dangerLevel, args: call.arguments },
361
+ { configDir: this.hookConfigDir, onSummary: (message) => this.send({ type: "info", message: `[hook] ${message}` }) }
362
+ );
363
+ const preDeny = preToolDecisions.find((d) => d.action === "deny");
364
+ if (preDeny) {
365
+ const result = {
366
+ callId: call.id,
367
+ content: `[Hook denied] ${preDeny.reason ?? `PreToolUse blocked ${call.name}`}. Do not retry without asking.`,
368
+ isError: true
369
+ };
370
+ this.sendToolCallResult(call, result.content, true);
371
+ return result;
372
+ }
373
+ for (const d of preToolDecisions) {
374
+ const warnings = [d.warning, ...d.warnings ?? []].filter(Boolean);
375
+ for (const warning of warnings) this.send({ type: "info", message: `[hook] ${warning}` });
376
+ }
352
377
  const permission = checkPermissionWithProfile(
353
378
  call.name,
354
379
  call.arguments,
@@ -364,6 +389,14 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
364
389
  }
365
390
  );
366
391
  const networkPermission = checkNetworkPolicy(call.name, call.arguments, this.networkPolicy);
392
+ if (networkPermission?.action === "confirm" || permission.action === "confirm") {
393
+ runLifecycleHooks(
394
+ this.hookConfig,
395
+ "PermissionRequest",
396
+ { tool: call.name, dangerLevel, args: call.arguments, reason: networkPermission?.reason ?? permission.reason },
397
+ { configDir: this.hookConfigDir, onSummary: (message) => this.send({ type: "info", message: "[hook] " + message }) }
398
+ );
399
+ }
367
400
  if (networkPermission?.action === "deny") {
368
401
  const reason = networkPermission.reason ? ` (${networkPermission.reason})` : "";
369
402
  return {
@@ -387,11 +420,13 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
387
420
  const content = truncateOutput(rawContent, call.name);
388
421
  this.sendToolCallResult(call, rawContent, false);
389
422
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
423
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
390
424
  return { callId: call.id, content, isError: false };
391
425
  } catch (err) {
392
426
  const message = err instanceof Error ? err.message : String(err);
393
427
  this.sendToolCallResult(call, message, true);
394
428
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
429
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
395
430
  return { callId: call.id, content: message, isError: true };
396
431
  }
397
432
  }
@@ -412,11 +447,13 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
412
447
  const content = truncateOutput(rawContent, call.name);
413
448
  this.sendToolCallResult(call, rawContent, false);
414
449
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
450
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
415
451
  return { callId: call.id, content, isError: false };
416
452
  } catch (err) {
417
453
  const message = err instanceof Error ? err.message : String(err);
418
454
  this.sendToolCallResult(call, message, true);
419
455
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
456
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
420
457
  return { callId: call.id, content: message, isError: true };
421
458
  }
422
459
  }
@@ -434,11 +471,13 @@ var ToolExecutorWeb = class _ToolExecutorWeb {
434
471
  const content = truncateOutput(rawContent, call.name);
435
472
  this.sendToolCallResult(call, rawContent, false);
436
473
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "ok" });
474
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "ok" }, { configDir: this.hookConfigDir });
437
475
  return { callId: call.id, content, isError: false };
438
476
  } catch (err) {
439
477
  const message = err instanceof Error ? err.message : String(err);
440
478
  this.sendToolCallResult(call, message, true);
441
479
  runHook(this.hookConfig?.postToolExecution, { tool: call.name, status: "error" });
480
+ runLifecycleHooks(this.hookConfig, "PostToolUse", { tool: call.name, status: "error", error: message }, { configDir: this.hookConfigDir });
442
481
  return { callId: call.id, content: message, isError: true };
443
482
  }
444
483
  }
@@ -681,6 +720,7 @@ var SessionHandler = class {
681
720
  const permissionProfiles = this.config.get("permissionProfiles") ?? {};
682
721
  this.toolExecutor.setConfig({
683
722
  hookConfig: hooks,
723
+ hookConfigDir: this.config.getConfigDir(),
684
724
  permissionRules,
685
725
  defaultPermission,
686
726
  permissionProfileName,
@@ -693,6 +733,11 @@ var SessionHandler = class {
693
733
  if (permissionProfileWarning) {
694
734
  this.send({ type: "info", message: permissionProfileWarning });
695
735
  }
736
+ const pendingHooks = getPendingHookTrust(hooks, this.config.getConfigDir());
737
+ if (pendingHooks.length > 0) {
738
+ this.send({ type: "info", message: `${pendingHooks.length} project hook(s) require trust. Use /hooks list and /hooks trust <id>.` });
739
+ }
740
+ runLifecycleHooks(hooks, "SessionStart", { source: "web" }, { configDir: this.config.getConfigDir() });
696
741
  this.sendStatus();
697
742
  askUserContext.rl = null;
698
743
  askUserContext.prompting = false;
@@ -801,6 +846,7 @@ var SessionHandler = class {
801
846
  }
802
847
  }
803
848
  onDisconnect() {
849
+ runLifecycleHooks(this.config.get("hooks") ?? void 0, "Stop", { sessionId: this.sessions.current?.id }, { configDir: this.config.getConfigDir() });
804
850
  this.toolExecutor.cancelAll();
805
851
  if (this.abortController) this.abortController.abort();
806
852
  for (const resolve3 of this.pendingAskUser.values()) resolve3(null);
@@ -978,6 +1024,26 @@ var SessionHandler = class {
978
1024
  try {
979
1025
  this.ensureSession();
980
1026
  const session = this.sessions.current;
1027
+ const promptDecisions = runLifecycleHooks(
1028
+ this.config.get("hooks") ?? void 0,
1029
+ "UserPromptSubmit",
1030
+ { sessionId: session.id, prompt: content },
1031
+ { configDir: this.config.getConfigDir(), onSummary: (message) => this.send({ type: "info", message: "[hook] " + message }) }
1032
+ );
1033
+ const promptDeny = promptDecisions.find((d) => d.action === "deny");
1034
+ if (promptDeny) {
1035
+ this.send({ type: "error", message: "Prompt blocked by hook: " + (promptDeny.reason ?? "UserPromptSubmit denied") });
1036
+ return;
1037
+ }
1038
+ for (const d of promptDecisions) {
1039
+ const warnings = [d.warning, ...d.warnings ?? []].filter(Boolean);
1040
+ for (const warning of warnings) this.send({ type: "info", message: "[hook] " + warning });
1041
+ }
1042
+ const secretHits = scanString(content, { customRegexes: [] });
1043
+ if (secretHits.length > 0) {
1044
+ const kinds = [...new Set(secretHits.map((h) => h.kind))].join(", ");
1045
+ this.send({ type: "info", message: "Possible secret in prompt (" + kinds + ")." });
1046
+ }
981
1047
  const ALLOWED_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"]);
982
1048
  const MAX_IMAGE_SIZE = 10 * 1024 * 1024;
983
1049
  let msgContent;
@@ -2533,7 +2599,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
2533
2599
  case "test": {
2534
2600
  this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
2535
2601
  try {
2536
- const { executeTests } = await import("./run-tests-3MHWUF43.js");
2602
+ const { executeTests } = await import("./run-tests-H4Q2PRX6.js");
2537
2603
  const argStr = args.join(" ").trim();
2538
2604
  let testArgs = {};
2539
2605
  if (argStr) {
@@ -2883,6 +2949,21 @@ Add .md files to create commands.` });
2883
2949
  this.send({ type: "info", message: "Not enough messages to compact." });
2884
2950
  return;
2885
2951
  }
2952
+ const preDecisions = runLifecycleHooks(
2953
+ this.config.get("hooks") ?? void 0,
2954
+ "PreCompact",
2955
+ { sessionId: session.id, messageCount: session.messages.length, instruction },
2956
+ { configDir: this.config.getConfigDir(), onSummary: (message) => this.send({ type: "info", message: "[hook] " + message }) }
2957
+ );
2958
+ const preDeny = preDecisions.find((d) => d.action === "deny");
2959
+ if (preDeny) {
2960
+ this.send({ type: "error", message: "Compact blocked by hook: " + (preDeny.reason ?? "PreCompact denied") });
2961
+ return;
2962
+ }
2963
+ for (const d of preDecisions) {
2964
+ const warnings = [d.warning, ...d.warnings ?? []].filter(Boolean);
2965
+ for (const warning of warnings) this.send({ type: "info", message: "[hook] " + warning });
2966
+ }
2886
2967
  const provider = this.providers.get(this.currentProvider);
2887
2968
  if (!provider) return;
2888
2969
  this.send({ type: "info", message: "Compacting conversation..." });
@@ -2902,7 +2983,15 @@ Add .md files to create commands.` });
2902
2983
  });
2903
2984
  const summaryMsg = { role: "user", content: "[Conversation summary]", timestamp: /* @__PURE__ */ new Date() };
2904
2985
  const ackMsg = { role: "assistant", content: response.content, timestamp: /* @__PURE__ */ new Date() };
2905
- session.compact(summaryMsg, ackMsg, keepLast);
2986
+ const beforeCount = session.messages.length;
2987
+ const removedCount = session.compact(summaryMsg, ackMsg, keepLast);
2988
+ const afterCount = session.messages.length;
2989
+ runLifecycleHooks(
2990
+ this.config.get("hooks") ?? void 0,
2991
+ "PostCompact",
2992
+ { sessionId: session.id, beforeCount, afterCount, removedCount, keepLast, instruction },
2993
+ { configDir: this.config.getConfigDir(), onSummary: (message) => this.send({ type: "info", message: "[hook] " + message }) }
2994
+ );
2906
2995
  this.send({ type: "info", message: `Compacted: removed ${toSummarize.length} messages, kept last ${keepLast}.` });
2907
2996
  this.sendStatus();
2908
2997
  } catch (err) {