chainlesschain 0.162.39 → 0.162.40

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 (162) hide show
  1. package/README.md +368 -1
  2. package/package.json +2 -2
  3. package/src/assets/web-panel/assets/{AIOps-DCjoAX_u.js → AIOps-CPmKv82o.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-XHoOmsbP.js → ActionButton-BNDYY7Qd.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics--xaFkDnL.js → Analytics-BgCMCOsk.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-CSa3FBn8.js → AppLayout-Dv4oJcqS.js} +5 -5
  7. package/src/assets/web-panel/assets/{Audit-ONWXiAwG.js → Audit-5iV3yrGa.js} +1 -1
  8. package/src/assets/web-panel/assets/{Backup-CKOPNdgy.js → Backup-CHDhnbzF.js} +1 -1
  9. package/src/assets/web-panel/assets/{BaseInput-PNj4uVqg.js → BaseInput-B6reFkra.js} +1 -1
  10. package/src/assets/web-panel/assets/{Chat-CZCulyXV.js → Chat-DwS5YyE2.js} +6 -6
  11. package/src/assets/web-panel/assets/{ChatBubbleRenderer-CjuJpfpV.js → ChatBubbleRenderer-CqXa87Hw.js} +1 -1
  12. package/src/assets/web-panel/assets/{Checkbox-jvy668lD.js → Checkbox-yiW0M4RE.js} +1 -1
  13. package/src/assets/web-panel/assets/{Codegen-DhUebOQD.js → Codegen-DoiVuD_g.js} +1 -1
  14. package/src/assets/web-panel/assets/{Col-BiBvHfdT.js → Col-BVASLexk.js} +1 -1
  15. package/src/assets/web-panel/assets/{Community-CmEdEti-.js → Community-D6KQ7JoU.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compact-CtxpF4R5.js → Compact-Bl9Uhb6v.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compliance-CvPTrTAJ.js → Compliance-MM31-dba.js} +1 -1
  18. package/src/assets/web-panel/assets/{Cowork-BMafGHjy.js → Cowork-PjU_1ieD.js} +2 -2
  19. package/src/assets/web-panel/assets/{Cron-mdg_4TR1.js → Cron-DorNtPZL.js} +2 -2
  20. package/src/assets/web-panel/assets/{Crosschain--dGxsUvn.js → Crosschain-Bm5ts2Kw.js} +1 -1
  21. package/src/assets/web-panel/assets/{DID-C9oKaCml.js → DID-7Y3jlFdY.js} +2 -2
  22. package/src/assets/web-panel/assets/{Dashboard-CoGxKMvy.js → Dashboard-1oE532bG.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dropdown-CDDu3ZZ3.js → Dropdown-hJlOPs0s.js} +1 -1
  24. package/src/assets/web-panel/assets/{EmailListRenderer-Dy7_r9Ag.js → EmailListRenderer-BEqJxKaO.js} +1 -1
  25. package/src/assets/web-panel/assets/{FamilyGuardDashboard-CNg6vImJ.js → FamilyGuardDashboard-BvCGwB6X.js} +1 -1
  26. package/src/assets/web-panel/assets/{Federation-CT61bf3u.js → Federation-CsXI72e5.js} +1 -1
  27. package/src/assets/web-panel/assets/{FormItemContext-CSLRnXhg.js → FormItemContext-Dh9SMul-.js} +1 -1
  28. package/src/assets/web-panel/assets/{GenericCardRenderer-CZ4NE5N3.js → GenericCardRenderer-9edWzrtG.js} +1 -1
  29. package/src/assets/web-panel/assets/{Git-DBuOma3L.js → Git-ZYhNL8Xk.js} +2 -2
  30. package/src/assets/web-panel/assets/{Governance-BTU_SEef.js → Governance-BwAdp8QA.js} +1 -1
  31. package/src/assets/web-panel/assets/{Inference-47SAmLC_.js → Inference-5C-M1XsH.js} +1 -1
  32. package/src/assets/web-panel/assets/{KnowledgeGraph-DCrK5vP4.js → KnowledgeGraph-zFAi-zCi.js} +1 -1
  33. package/src/assets/web-panel/assets/{Logs-BqiDxdav.js → Logs-BZsEdbgE.js} +2 -2
  34. package/src/assets/web-panel/assets/{Marketplace-CReUjsDt.js → Marketplace-BP6gErRK.js} +1 -1
  35. package/src/assets/web-panel/assets/{McpTools-agZBV3p8.js → McpTools-CXVzoLrd.js} +6 -6
  36. package/src/assets/web-panel/assets/{Memory-C_YvUtyS.js → Memory-BIpChb4-.js} +2 -2
  37. package/src/assets/web-panel/assets/{MobileBridge-41fP1Tui.js → MobileBridge-B4O7wDT8.js} +3 -3
  38. package/src/assets/web-panel/assets/MobileProjects-7VPMoHus.js +1 -0
  39. package/src/assets/web-panel/assets/{Mtc-JFJCXUnk.js → Mtc-BTmEyTM5.js} +6 -6
  40. package/src/assets/web-panel/assets/{MtcAudit-BHNpPZC9.js → MtcAudit-CsbG9LlV.js} +6 -6
  41. package/src/assets/web-panel/assets/{Multisig-DuCRumiz.js → Multisig-CL8yoGon.js} +3 -3
  42. package/src/assets/web-panel/assets/{NLProgramming-DK-g0fKY.js → NLProgramming-C2cIlIp_.js} +1 -1
  43. package/src/assets/web-panel/assets/{Notes-BSMcjsPf.js → Notes-7aBk_n_M.js} +3 -3
  44. package/src/assets/web-panel/assets/{NotificationSettings-9ouC118H.js → NotificationSettings-BuhQk4rJ.js} +1 -1
  45. package/src/assets/web-panel/assets/{OrderTableRenderer-LG2nUO5y.js → OrderTableRenderer-mqMFZu0x.js} +1 -1
  46. package/src/assets/web-panel/assets/{Organization-DSV7oRnR.js → Organization-CAdq-170.js} +4 -4
  47. package/src/assets/web-panel/assets/{Overflow-DVkkORc3.js → Overflow--Xn0E787.js} +1 -1
  48. package/src/assets/web-panel/assets/{P2P-BXXjkkQD.js → P2P-DYt3YAXI.js} +2 -2
  49. package/src/assets/web-panel/assets/{PdhVaultBrowser-O5hNnLTP.js → PdhVaultBrowser-Bgb_v8WN.js} +3 -3
  50. package/src/assets/web-panel/assets/{Permissions-D_s0H5Av.js → Permissions-DoFlmoaW.js} +4 -4
  51. package/src/assets/web-panel/assets/{PersonalDataHub-CzMDrwUi.js → PersonalDataHub-C-FJB3a0.js} +3 -3
  52. package/src/assets/web-panel/assets/{Pipeline-i9krLVTL.js → Pipeline-3bL2RzzL.js} +1 -1
  53. package/src/assets/web-panel/assets/{Privacy-cMQcj9I8.js → Privacy-c4igYUCF.js} +1 -1
  54. package/src/assets/web-panel/assets/{ProjectInit-Ca_l7avo.js → ProjectInit-C0QS1UPR.js} +2 -2
  55. package/src/assets/web-panel/assets/{ProjectSettings-BkaIhd6b.js → ProjectSettings-CkYC0xkE.js} +2 -2
  56. package/src/assets/web-panel/assets/{Projects-Dy9yNmDg.js → Projects-Di17SYft.js} +1 -1
  57. package/src/assets/web-panel/assets/{Providers-D0nzYiqz.js → Providers-41NySsLt.js} +1 -1
  58. package/src/assets/web-panel/assets/{QuickAsk-Bzzr9d0f.js → QuickAsk-DHq9pD7z.js} +1 -1
  59. package/src/assets/web-panel/assets/{Recommend-C-UFbQnX.js → Recommend-CLjgFPLv.js} +1 -1
  60. package/src/assets/web-panel/assets/{Reputation-BKMIKO5F.js → Reputation-EIrgErm3.js} +1 -1
  61. package/src/assets/web-panel/assets/{Row-Bs7htK1T.js → Row-GAvKzKH7.js} +1 -1
  62. package/src/assets/web-panel/assets/{RssFeed-v6MdULUh.js → RssFeed-CYCNsVmD.js} +2 -2
  63. package/src/assets/web-panel/assets/{Search-DlRWYzvz.js → Search-DWOE32k8.js} +1 -1
  64. package/src/assets/web-panel/assets/{Security-DXWO37xX.js → Security-Dgh8Jevn.js} +4 -4
  65. package/src/assets/web-panel/assets/{Services-C2tWA-O0.js → Services-BxdgP67N.js} +2 -2
  66. package/src/assets/web-panel/assets/{Skeleton-Q8pIYY4a.js → Skeleton-D-xT4ZkA.js} +1 -1
  67. package/src/assets/web-panel/assets/{Skills-D7XBlErj.js → Skills-BKN4lfSa.js} +1 -1
  68. package/src/assets/web-panel/assets/{Sla-CiyMVPJ1.js → Sla--N1TudpS.js} +1 -1
  69. package/src/assets/web-panel/assets/{SpeechSettings-CadCeeiR.js → SpeechSettings-B0vfJpEh.js} +1 -1
  70. package/src/assets/web-panel/assets/{SyncSettings-DzNAUhQq.js → SyncSettings-BuBAbPAh.js} +2 -2
  71. package/src/assets/web-panel/assets/{Tasks-BjdHjZeb.js → Tasks-4XugjJ87.js} +1 -1
  72. package/src/assets/web-panel/assets/{Templates-DfgEpUa4.js → Templates-DI2giLgc.js} +1 -1
  73. package/src/assets/web-panel/assets/{Tenant-C8ajkuYi.js → Tenant-BiTWvm0g.js} +1 -1
  74. package/src/assets/web-panel/assets/{Terminal-B9rHwQQx.js → Terminal-vV6AWGDi.js} +2 -2
  75. package/src/assets/web-panel/assets/{TimelineRenderer-D1ZVNezX.js → TimelineRenderer-BmgzKdAp.js} +1 -1
  76. package/src/assets/web-panel/assets/{Tokens-CAkED4mx.js → Tokens-Nvupdm6p.js} +1 -1
  77. package/src/assets/web-panel/assets/{Trigger-CJSrm6X0.js → Trigger-DRfR77WJ.js} +1 -1
  78. package/src/assets/web-panel/assets/{Trust-B-TeorSk.js → Trust-De0Jal_6.js} +1 -1
  79. package/src/assets/web-panel/assets/{UkeySign-Di7Ymofy.js → UkeySign-Dzo4-VAM.js} +1 -1
  80. package/src/assets/web-panel/assets/{VideoEditing-DM1eYNZe.js → VideoEditing-hg2ytiJB.js} +1 -1
  81. package/src/assets/web-panel/assets/{Wallet-DvRWkbmR.js → Wallet--bU5-gRh.js} +4 -4
  82. package/src/assets/web-panel/assets/{WebAuthn-CeZ3Y622.js → WebAuthn-DZptt-PV.js} +4 -4
  83. package/src/assets/web-panel/assets/{WorkflowEditor-Cq8c4h5j.js → WorkflowEditor-Dy9223bY.js} +1 -1
  84. package/src/assets/web-panel/assets/{chat-7-WfML6Q.js → chat-DaxGeI9w.js} +1 -1
  85. package/src/assets/web-panel/assets/{colors-D6FgCmB-.js → colors-Cu2VEci3.js} +1 -1
  86. package/src/assets/web-panel/assets/{compact-item-ClYV25qi.js → compact-item-CGolhyJq.js} +1 -1
  87. package/src/assets/web-panel/assets/{createContext-CDhtjdkV.js → createContext-DY7EFhkD.js} +1 -1
  88. package/src/assets/web-panel/assets/devWarning-DV2BNd59.js +1 -0
  89. package/src/assets/web-panel/assets/{hasIn-DZSH5LQd.js → hasIn-Bpc-NoFN.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-CKgS8E_X.js → index-1D4sfByw.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-Or_McYjX.js → index-8h9y5S6X.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-BlBF_l8m.js → index-BP9P6chP.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-CSjoWPxB.js → index-BQ2z6Ky5.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-DjG82V0v.js → index-BRAgl2J_.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-B_mMFQ4S.js → index-BTvwiqJE.js} +1 -1
  96. package/src/assets/web-panel/assets/index-BZqtTmyG.js +1 -0
  97. package/src/assets/web-panel/assets/{index-D0YzTJJO.js → index-BjfxHEmX.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-DgaF1F0W.js → index-BlHq81Ow.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-Bj8hZiyL.js → index-Bn5gM9Oy.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DPEYvNvq.js → index-Bz83ngs0.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DL6GFJAd.js → index-C-Hkl_2G.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-BJ7mrOaB.js → index-C0_zeYnx.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-CrTmxbL8.js → index-C2RpsAiO.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CDX4QU3k.js → index-CBSk_VrT.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DZ4zuoCP.js → index-CFAnEzRW.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-B6VWGnwq.js → index-CGqeHu_F.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-C7pQa2is.js → index-CJFYF8F9.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-CWOkL-8O.js → index-CLNqZF55.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-tU6pZ1TP.js → index-CaKXhpEu.js} +1 -1
  110. package/src/assets/web-panel/assets/{index---azBCXl.js → index-Ciw5-X1B.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-DLizxxId.js → index-D0GN5tdM.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-BHeK8I5A.js → index-D63ObMdQ.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-z-R0KaJS.js → index-DAov-rJR.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-BUOPjAUM.js → index-DElatOQ0.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-Di9pFrHV.js → index-DNX81oSR.js} +1 -1
  116. package/src/assets/web-panel/assets/index-DUpwdJt9.js +1 -0
  117. package/src/assets/web-panel/assets/{index-B13QnrnE.js → index-DZ4Vm8dQ.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-C7sC56w8.js → index-DexYD87j.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-CxwfFZ1u.js → index-DfKmAEtE.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-B78X5S22.js → index-DldaToUA.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BpzOUiSb.js → index-DpRSzAFl.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-B4PMzmOx.js → index-DxXkr-NS.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-BqOIoEo6.js → index-RumxOD0S.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-DGJK8D0l.js → index-VBRPxZeE.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DWRoh3_3.js → index-eF9RV_4c.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-DGj1orXm.js → index-lfP8sdzB.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-CmU631Je.js → index-oJQgRCrR.js} +3 -3
  128. package/src/assets/web-panel/assets/{index-rCs9VJJp.js → index-rkm7dHwG.js} +1 -1
  129. package/src/assets/web-panel/assets/{initDefaultProps-CSdsIGy3.js → initDefaultProps-CkJZfCo8.js} +1 -1
  130. package/src/assets/web-panel/assets/{motion-Do-AcZV4.js → motion-BerbusV1.js} +1 -1
  131. package/src/assets/web-panel/assets/{move-BmgOoMsi.js → move-DyRzKPD4.js} +1 -1
  132. package/src/assets/web-panel/assets/{omit-D4Tm7-s9.js → omit-CCdrTUAs.js} +1 -1
  133. package/src/assets/web-panel/assets/{pickAttrs-CuWA8-lj.js → pickAttrs-mVDeZx2m.js} +1 -1
  134. package/src/assets/web-panel/assets/{placementArrow-BSbEF5op.js → placementArrow-Bb_-Fs_o.js} +1 -1
  135. package/src/assets/web-panel/assets/{responsiveObserve-GIMJwB_9.js → responsiveObserve-C6TMj1R_.js} +1 -1
  136. package/src/assets/web-panel/assets/{slide-DlZxpIBe.js → slide-CdCNsy1J.js} +1 -1
  137. package/src/assets/web-panel/assets/{statusUtils-BZ26LPlh.js → statusUtils-Ccxd1rFd.js} +1 -1
  138. package/src/assets/web-panel/assets/{styleChecker-Yn_3FZ0l.js → styleChecker-3IL-yw1V.js} +1 -1
  139. package/src/assets/web-panel/assets/{useFlexGapSupport-O_LOE1AB.js → useFlexGapSupport-CH8DjUHl.js} +1 -1
  140. package/src/assets/web-panel/assets/{useFs-VFMyQqtl.js → useFs-Cn9nE2sp.js} +1 -1
  141. package/src/assets/web-panel/assets/{usePersonalDataHub-B_hyrGB-.js → usePersonalDataHub-BPyT0HO7.js} +1 -1
  142. package/src/assets/web-panel/assets/{vnode-D4LttGy7.js → vnode-Mfm7vy07.js} +1 -1
  143. package/src/assets/web-panel/assets/{zoom-KnTK1fjj.js → zoom-CTpAiAE9.js} +1 -1
  144. package/src/assets/web-panel/index.html +1 -1
  145. package/src/commands/init.js +84 -2
  146. package/src/commands/session.js +36 -12
  147. package/src/index.js +10 -0
  148. package/src/lib/agent-session-export.js +124 -0
  149. package/src/lib/ide-context.js +62 -0
  150. package/src/lib/project-instructions.js +275 -0
  151. package/src/lib/project-inventory.js +355 -0
  152. package/src/lib/repl-bang-memorize.js +142 -0
  153. package/src/lib/repl-completer.js +17 -2
  154. package/src/lib/update-notice-refresh.mjs +10 -0
  155. package/src/lib/update-notice.js +154 -0
  156. package/src/repl/agent-repl.js +118 -0
  157. package/src/runtime/agent-core.js +162 -0
  158. package/src/runtime/system-prompt.js +21 -1
  159. package/src/assets/web-panel/assets/MobileProjects-BkqLvGfL.js +0 -1
  160. package/src/assets/web-panel/assets/devWarning-O0FVFeZg.js +0 -1
  161. package/src/assets/web-panel/assets/index--ANIKvhL.js +0 -1
  162. package/src/assets/web-panel/assets/index-DUfp4rnQ.js +0 -1
@@ -0,0 +1,142 @@
1
+ /**
2
+ * REPL `!` bash passthrough + `#` quick-memorize (Claude-Code parity).
3
+ *
4
+ * Pure logic for two agent-REPL input prefixes, extracted so it is unit
5
+ * testable without driving readline:
6
+ *
7
+ * - `! <cmd>` — run the shell command immediately (no LLM round-trip) and
8
+ * return a `<bash-input>/<bash-output>` context message so the model sees
9
+ * what happened on the next turn. Windows runs through `cmd.exe /d /s /c`
10
+ * with a `chcp 65001` prefix (encoding.md rule); POSIX through `/bin/sh -c`.
11
+ *
12
+ * - `# <note>` — append a one-line note to the project memory file (cc.md at
13
+ * the git root — the file project-instructions.js auto-loads). Creates the
14
+ * file/`## Notes` section when missing; next sessions pick it up
15
+ * automatically and the caller can also inject it into the live context.
16
+ *
17
+ * All process/fs access goes through `_deps` for tests.
18
+ */
19
+
20
+ import { spawnSync as spawnSyncDefault } from "child_process";
21
+ import fsDefault from "fs";
22
+ import pathDefault from "path";
23
+ import { findProjectRoot } from "./project-instructions.js";
24
+
25
+ export const BANG_TIMEOUT_MS = 120_000;
26
+ export const BANG_MAX_BUFFER = 10 * 1024 * 1024;
27
+ export const BANG_OUTPUT_CAP = 30_000;
28
+
29
+ export const _deps = {
30
+ spawnSync: spawnSyncDefault,
31
+ fs: fsDefault,
32
+ path: pathDefault,
33
+ };
34
+
35
+ function cap(s) {
36
+ const str = s || "";
37
+ return str.length > BANG_OUTPUT_CAP
38
+ ? `${str.slice(0, BANG_OUTPUT_CAP)}\n… [truncated]`
39
+ : str;
40
+ }
41
+
42
+ /** True when the REPL line is a `!` bash passthrough. */
43
+ export function isBangCommand(trimmed) {
44
+ return (
45
+ typeof trimmed === "string" &&
46
+ trimmed.startsWith("!") &&
47
+ trimmed.slice(1).trim().length > 0
48
+ );
49
+ }
50
+
51
+ /** True when the REPL line is a `#` quick-memorize. */
52
+ export function isMemorizeLine(trimmed) {
53
+ return (
54
+ typeof trimmed === "string" &&
55
+ trimmed.startsWith("#") &&
56
+ trimmed.slice(1).trim().length > 0
57
+ );
58
+ }
59
+
60
+ /**
61
+ * Run a `!` command synchronously.
62
+ *
63
+ * @returns {{ cmd, stdout, stderr, exitCode, error, contextMessage }}
64
+ * `contextMessage` is ready to push as a user-role message.
65
+ */
66
+ export function runBangCommand(line, opts = {}) {
67
+ const spawnSync = opts.deps?.spawnSync || _deps.spawnSync;
68
+ const cwd = opts.cwd || process.cwd();
69
+ const isWin =
70
+ opts.platform != null ? opts.platform === "win32" : process.platform === "win32";
71
+ const cmd = String(line).replace(/^!/, "").trim();
72
+
73
+ const res = isWin
74
+ ? spawnSync("cmd.exe", ["/d", "/s", "/c", `chcp 65001 >nul && ${cmd}`], {
75
+ encoding: "utf-8",
76
+ timeout: BANG_TIMEOUT_MS,
77
+ maxBuffer: BANG_MAX_BUFFER,
78
+ cwd,
79
+ })
80
+ : spawnSync("/bin/sh", ["-c", cmd], {
81
+ encoding: "utf-8",
82
+ timeout: BANG_TIMEOUT_MS,
83
+ maxBuffer: BANG_MAX_BUFFER,
84
+ cwd,
85
+ });
86
+
87
+ const exitCode = res.status == null ? (res.error ? -1 : 0) : res.status;
88
+ const stdout = cap(res.stdout);
89
+ const stderr = cap(res.stderr);
90
+ const body = [stdout, stderr].filter(Boolean).join("\n");
91
+ return {
92
+ cmd,
93
+ stdout,
94
+ stderr,
95
+ exitCode,
96
+ error: res.error || null,
97
+ contextMessage: {
98
+ role: "user",
99
+ content: `<bash-input>${cmd}</bash-input>\n<bash-output exit-code="${exitCode}">\n${body}\n</bash-output>`,
100
+ },
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Append a `#` note to the project cc.md (created at the git root — falls
106
+ * back to cwd outside a repo). Inserts under a `## Notes` heading, creating
107
+ * file/section as needed.
108
+ *
109
+ * @returns {{ target, line, created }}
110
+ */
111
+ export function appendMemoryNote(rawLine, opts = {}) {
112
+ const fs = opts.deps?.fs || _deps.fs;
113
+ const path = opts.deps?.path || _deps.path;
114
+ const cwd = opts.cwd || process.cwd();
115
+ const note = String(rawLine).replace(/^#/, "").trim();
116
+ const stamp = opts.date || new Date().toISOString().slice(0, 10);
117
+
118
+ const root =
119
+ findProjectRoot(cwd, { deps: { fs, path } }) || path.resolve(cwd);
120
+ const target = opts.target || path.join(root, "cc.md");
121
+ const line = `- ${note} _(noted ${stamp})_`;
122
+
123
+ let text = null;
124
+ try {
125
+ text = fs.readFileSync(target, "utf-8");
126
+ } catch {
127
+ /* file does not exist yet */
128
+ }
129
+
130
+ let created = false;
131
+ if (text == null) {
132
+ text = `# Project Memory\n\n## Notes\n\n${line}\n`;
133
+ created = true;
134
+ } else if (/^## Notes\s*$/m.test(text)) {
135
+ // insert right after the heading (keeps newest notes on top)
136
+ text = text.replace(/^## Notes\s*$/m, (m) => `${m}\n\n${line}`);
137
+ } else {
138
+ text = `${text.trimEnd()}\n\n## Notes\n\n${line}\n`;
139
+ }
140
+ fs.writeFileSync(target, text, "utf-8");
141
+ return { target, line, note, created };
142
+ }
@@ -78,9 +78,12 @@ export function fileCandidates(prefix, { cwd = process.cwd(), deps } = {}) {
78
78
 
79
79
  /**
80
80
  * Build a readline completer. Returns `completer(line)` → `[hits, replaced]`
81
- * per the readline contract; non-@ lines complete to nothing.
81
+ * per the readline contract. Completes `@path` tokens anywhere in the line
82
+ * and `/command` names at line start (while the command token is still being
83
+ * typed); everything else completes to nothing.
82
84
  *
83
- * @param {object} opts { cwd?, getIdeOpenFiles?: () => Promise<string[]>, deps? }
85
+ * @param {object} opts { cwd?, getIdeOpenFiles?: () => Promise<string[]>,
86
+ * slashCommands?: string[], deps? }
84
87
  */
85
88
  export function makeAtCompleter(opts = {}) {
86
89
  const cwd = opts.cwd || process.cwd();
@@ -118,7 +121,19 @@ export function makeAtCompleter(opts = {}) {
118
121
  });
119
122
  };
120
123
 
124
+ const slashCommands = Array.isArray(opts.slashCommands)
125
+ ? [...opts.slashCommands].sort()
126
+ : [];
127
+
121
128
  const completer = (line) => {
129
+ // `/command` completion (Claude-Code parity): only while typing the
130
+ // command token itself — once a space follows, args are the user's.
131
+ const slash = /^\/([A-Za-z_-]*)$/.exec(line);
132
+ if (slash && slashCommands.length) {
133
+ const pref = `/${slash[1].toLowerCase()}`;
134
+ const hits = slashCommands.filter((c) => c.toLowerCase().startsWith(pref));
135
+ return [hits, line];
136
+ }
122
137
  const at = extractAtPrefix(line);
123
138
  if (!at) return [[], line];
124
139
  refreshIde(); // async top-up for the NEXT tab; this one uses the cache
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Detached cache refresher for the startup update notice.
3
+ * Spawned unref'd by update-notice.js; argv[2] = cache file path.
4
+ * Exits quietly on any failure — the notice is strictly best-effort.
5
+ */
6
+ import { refreshCacheOnce } from "./update-notice.js";
7
+
8
+ refreshCacheOnce({ cacheFile: process.argv[2] })
9
+ .catch(() => {})
10
+ .finally(() => process.exit(0));
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Startup update notice — Claude-Code-style "new version available" line.
3
+ *
4
+ * Zero startup cost by design:
5
+ * - the CLI entry only does ONE sync cache read (~/.chainlesschain/
6
+ * update-check.json) and prints a single gray stderr line when the cached
7
+ * latest version is newer than the running one (TTY only, never pollutes
8
+ * piped/JSON output);
9
+ * - when the cache is stale (>24h) it spawns a DETACHED, unref'd child that
10
+ * refreshes the cache from the npm registry for the NEXT run — the current
11
+ * invocation never waits on the network. The cache's checkedAt is touched
12
+ * optimistically before spawning so concurrent invocations don't stampede.
13
+ *
14
+ * Disable with CC_UPDATE_NOTICE=0. `cc update` remains the full interactive
15
+ * checker (GitHub releases + assets); this is just the passive nudge.
16
+ */
17
+
18
+ import fsDefault from "fs";
19
+ import pathDefault from "path";
20
+ import osDefault from "os";
21
+ import { spawn as spawnDefault } from "child_process";
22
+ import { fileURLToPath } from "url";
23
+ import chalk from "chalk";
24
+ import semver from "semver";
25
+ import { VERSION } from "../constants.js";
26
+
27
+ export const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
28
+ export const NPM_LATEST_URL = "https://registry.npmjs.org/chainlesschain/latest";
29
+
30
+ export const _deps = {
31
+ fs: fsDefault,
32
+ path: pathDefault,
33
+ os: osDefault,
34
+ spawn: spawnDefault,
35
+ };
36
+
37
+ export function cachePath(deps = _deps) {
38
+ return deps.path.join(
39
+ deps.os.homedir() || "",
40
+ ".chainlesschain",
41
+ "update-check.json",
42
+ );
43
+ }
44
+
45
+ function readCache(deps) {
46
+ try {
47
+ return JSON.parse(deps.fs.readFileSync(cachePath(deps), "utf-8"));
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ function writeCache(deps, cache) {
54
+ try {
55
+ const p = cachePath(deps);
56
+ deps.fs.mkdirSync(deps.path.dirname(p), { recursive: true });
57
+ deps.fs.writeFileSync(p, JSON.stringify(cache), "utf-8");
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Entry-point hook. Cheap and fail-open — never throws, never blocks.
66
+ *
67
+ * @returns {{ printed: boolean, spawned: boolean }}
68
+ */
69
+ export function maybeNotifyUpdate(opts = {}) {
70
+ const deps = { ..._deps, ...(opts.deps || {}) };
71
+ const env = opts.env || process.env;
72
+ const now = opts.now ?? Date.now();
73
+ const isTTY = opts.isTTY ?? Boolean(process.stderr.isTTY);
74
+ const current = opts.currentVersion || VERSION;
75
+ const print =
76
+ opts.print || ((line) => process.stderr.write(chalk.gray(line) + "\n"));
77
+
78
+ const out = { printed: false, spawned: false };
79
+ try {
80
+ if (env.CC_UPDATE_NOTICE === "0") return out;
81
+
82
+ const cache = readCache(deps);
83
+
84
+ if (
85
+ isTTY &&
86
+ cache?.latest &&
87
+ semver.valid(cache.latest) &&
88
+ semver.valid(current) &&
89
+ semver.gt(cache.latest, current)
90
+ ) {
91
+ print(
92
+ `Update available: chainlesschain ${current} → ${cache.latest} (npm i -g chainlesschain · CC_UPDATE_NOTICE=0 to hide)`,
93
+ );
94
+ out.printed = true;
95
+ }
96
+
97
+ const stale = !cache?.checkedAt || now - cache.checkedAt > CACHE_TTL_MS;
98
+ if (stale) {
99
+ // Optimistic touch first: parallel `cc` invocations inside the stale
100
+ // window won't each spawn a refresher.
101
+ writeCache(deps, { ...(cache || {}), checkedAt: now });
102
+ const refresher = deps.path.join(
103
+ deps.path.dirname(fileURLToPath(import.meta.url)),
104
+ "update-notice-refresh.mjs",
105
+ );
106
+ const child = deps.spawn(
107
+ process.execPath,
108
+ [refresher, cachePath(deps)],
109
+ { detached: true, stdio: "ignore", windowsHide: true },
110
+ );
111
+ if (child && typeof child.unref === "function") child.unref();
112
+ out.spawned = true;
113
+ }
114
+ } catch {
115
+ /* fail-open: a broken cache or spawn must never affect the CLI */
116
+ }
117
+ return out;
118
+ }
119
+
120
+ /**
121
+ * One cache refresh (used by the detached child; exported for tests).
122
+ * npm registry only — light, unauthenticated, no GitHub rate-limit risk.
123
+ */
124
+ export async function refreshCacheOnce({
125
+ cacheFile,
126
+ fetchImpl = fetch,
127
+ deps = _deps,
128
+ now = Date.now(),
129
+ timeoutMs = 10_000,
130
+ } = {}) {
131
+ const ctrl = new AbortController();
132
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
133
+ try {
134
+ const res = await fetchImpl(NPM_LATEST_URL, {
135
+ headers: { Accept: "application/json" },
136
+ signal: ctrl.signal,
137
+ });
138
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
139
+ const data = await res.json();
140
+ if (!data?.version) throw new Error("no version field");
141
+ const file = cacheFile || cachePath(deps);
142
+ deps.fs.mkdirSync(deps.path.dirname(file), { recursive: true });
143
+ deps.fs.writeFileSync(
144
+ file,
145
+ JSON.stringify({ checkedAt: now, latest: data.version }),
146
+ "utf-8",
147
+ );
148
+ return { ok: true, latest: data.version };
149
+ } catch (err) {
150
+ return { ok: false, error: err.message };
151
+ } finally {
152
+ clearTimeout(timer);
153
+ }
154
+ }
@@ -737,6 +737,30 @@ export async function startAgentRepl(options = {}) {
737
737
  const { makeAtCompleter } = await import("../lib/repl-completer.js");
738
738
  const atCompleter = makeAtCompleter({
739
739
  cwd: process.cwd(),
740
+ // Keep in sync with the rl.on("line") handlers + /help below.
741
+ slashCommands: [
742
+ "/auto",
743
+ "/clear",
744
+ "/compact",
745
+ "/context",
746
+ "/cowork",
747
+ "/exit",
748
+ "/help",
749
+ "/mcp",
750
+ "/model",
751
+ "/output-style",
752
+ "/plan",
753
+ "/profile",
754
+ "/provider",
755
+ "/quit",
756
+ "/reindex",
757
+ "/search",
758
+ "/session",
759
+ "/stats",
760
+ "/statusline",
761
+ "/sub-agents",
762
+ "/task",
763
+ ],
740
764
  getIdeOpenFiles: async () => {
741
765
  const exec = _adhocMcp?.externalToolExecutors?.mcp__ide__getOpenEditors;
742
766
  if (!exec || exec.kind !== "mcp" || !_adhocMcp?.mcpClient?.callTool) {
@@ -854,6 +878,45 @@ export async function startAgentRepl(options = {}) {
854
878
  return;
855
879
  }
856
880
 
881
+ // `!` bash passthrough (Claude-Code parity): run the command right here —
882
+ // no LLM round-trip — and fold the output into the conversation context.
883
+ if (trimmed.startsWith("!") && trimmed.slice(1).trim()) {
884
+ try {
885
+ const { runBangCommand } = await import("../lib/repl-bang-memorize.js");
886
+ const res = runBangCommand(trimmed, { cwd: process.cwd() });
887
+ logger.log(chalk.gray(`$ ${res.cmd}`));
888
+ if (res.stdout) process.stdout.write(res.stdout.endsWith("\n") ? res.stdout : `${res.stdout}\n`);
889
+ if (res.stderr) process.stderr.write(chalk.red(res.stderr.endsWith("\n") ? res.stderr : `${res.stderr}\n`));
890
+ if (res.error) logger.error(`shell error: ${res.error.message}`);
891
+ logger.log(chalk.gray(`(exit ${res.exitCode})`));
892
+ messages.push(res.contextMessage);
893
+ } catch (err) {
894
+ logger.error(`! command failed: ${err.message}`);
895
+ }
896
+ prompt();
897
+ return;
898
+ }
899
+
900
+ // `#` quick-memorize (Claude-Code parity): append a note to the project
901
+ // cc.md (auto-loaded next session) and keep it active in this one.
902
+ if (trimmed.startsWith("#") && trimmed.slice(1).trim()) {
903
+ try {
904
+ const { appendMemoryNote } = await import("../lib/repl-bang-memorize.js");
905
+ const res = appendMemoryNote(trimmed, { cwd: process.cwd() });
906
+ messages.push({
907
+ role: "system",
908
+ content: `<memory-note source="${res.target}">${res.note}</memory-note>`,
909
+ });
910
+ logger.log(
911
+ chalk.green(`✔ remembered in ${res.target}${res.created ? " (created)" : ""}`),
912
+ );
913
+ } catch (err) {
914
+ logger.error(`# memorize failed: ${err.message}`);
915
+ }
916
+ prompt();
917
+ return;
918
+ }
919
+
857
920
  // Slash commands
858
921
  if (trimmed === "/exit" || trimmed === "/quit") {
859
922
  logger.log(chalk.gray("\nGoodbye!"));
@@ -863,6 +926,12 @@ export async function startAgentRepl(options = {}) {
863
926
 
864
927
  if (trimmed === "/help") {
865
928
  logger.log(chalk.bold("\nCommands:"));
929
+ logger.log(
930
+ ` ${chalk.cyan("! <cmd>")} Run a shell command directly (output joins context)`,
931
+ );
932
+ logger.log(
933
+ ` ${chalk.cyan("# <note>")} Remember a note in the project cc.md`,
934
+ );
866
935
  logger.log(` ${chalk.cyan("/exit")} Exit the agent`);
867
936
  logger.log(
868
937
  ` ${chalk.cyan("/model")} Show/change model (/model <name>)`,
@@ -872,6 +941,9 @@ export async function startAgentRepl(options = {}) {
872
941
  logger.log(
873
942
  ` ${chalk.cyan("/statusline")} Context-usage line on/off (/statusline [on|off])`,
874
943
  );
944
+ logger.log(
945
+ ` ${chalk.cyan("/context")} Live context-window usage by role`,
946
+ );
875
947
  logger.log(
876
948
  ` ${chalk.cyan("/compact")} Smart compact (importance-based)`,
877
949
  );
@@ -1110,6 +1182,52 @@ export async function startAgentRepl(options = {}) {
1110
1182
  return;
1111
1183
  }
1112
1184
 
1185
+ if (trimmed === "/context") {
1186
+ // Live-session twin of `cc context` (Claude-Code /context parity):
1187
+ // bucket the CURRENT in-memory conversation by role against the model
1188
+ // window. Reuses the same categorizer + estimator as the archived view.
1189
+ try {
1190
+ const { categorizeContext } = await import("../commands/context.js");
1191
+ const { estimateTokens } = await import(
1192
+ "../harness/prompt-compressor.js"
1193
+ );
1194
+ const { buckets, counts, total } = categorizeContext(
1195
+ messages,
1196
+ estimateTokens,
1197
+ );
1198
+ const window = getContextWindow(model, provider) || 0;
1199
+ logger.log(chalk.bold("\nContext usage (live session):"));
1200
+ const rows = [
1201
+ ["system", buckets.system, counts.system],
1202
+ ["user", buckets.user, counts.user],
1203
+ ["assistant", buckets.assistant, counts.assistant],
1204
+ ["tool", buckets.tool, counts.tool],
1205
+ ["tool_calls", buckets.toolCalls, null],
1206
+ ];
1207
+ for (const [label, tok, n] of rows) {
1208
+ if (!tok) continue;
1209
+ const share = total ? Math.round((tok / total) * 100) : 0;
1210
+ logger.log(
1211
+ ` ${label.padEnd(11)}${String(tok).padStart(9)} tok ${String(share).padStart(3)}%${
1212
+ n != null ? chalk.gray(` (${n} msgs)`) : ""
1213
+ }`,
1214
+ );
1215
+ }
1216
+ const pct = window ? Math.round((total / window) * 100) : null;
1217
+ logger.log(
1218
+ ` ${"total".padEnd(11)}${String(total).padStart(9)} tok${
1219
+ window
1220
+ ? ` ${pct}% of ${window} (${Math.max(0, window - total)} left)`
1221
+ : ""
1222
+ }`,
1223
+ );
1224
+ } catch (err) {
1225
+ logger.error(`/context failed: ${err.message}`);
1226
+ }
1227
+ prompt();
1228
+ return;
1229
+ }
1230
+
1113
1231
  if (trimmed === "/compact") {
1114
1232
  if (_compressor && messages.length > 3) {
1115
1233
  const { messages: compacted, stats } = await _compressor.compress(
@@ -230,6 +230,25 @@ async function runSettingsPreToolUseHooks(name, args, context, cwd) {
230
230
  return { blocked: true, reason: outcome.reason, hook: outcome.hook };
231
231
  }
232
232
  if (outcome.decision === "ask") {
233
+ // File edits in an interactive session with an IDE bridge: route the ask
234
+ // through the editor's openDiff review (same machinery as settings ask —
235
+ // accepted means the IDE wrote the file, so the caller must skip
236
+ // execution; see tryIdeDiffApprovalForEdit).
237
+ const ide = await tryIdeDiffApprovalForEdit(name, args, context, cwd, {
238
+ rule: `hook:${outcome.hook}`,
239
+ source: "PreToolUse hook",
240
+ });
241
+ if (ide?.outcome === "accepted") {
242
+ return { blocked: false, ideApplied: ide.result };
243
+ }
244
+ if (ide?.outcome === "rejected") {
245
+ return {
246
+ blocked: true,
247
+ reason: ide.result.error,
248
+ hook: outcome.hook,
249
+ ideResult: ide.result,
250
+ };
251
+ }
233
252
  const confirm = context.permissionConfirm || context.shellConfirm || null;
234
253
  const ok =
235
254
  typeof confirm === "function"
@@ -603,6 +622,133 @@ export function buildSystemPrompt(cwd, opts = {}) {
603
622
 
604
623
  // ─── Tool execution ──────────────────────────────────────────────────────
605
624
 
625
+ /** The file-mutating tools whose `ask` can be reviewed as an IDE diff. */
626
+ const IDE_DIFF_EDIT_TOOLS = new Set([
627
+ "write_file",
628
+ "edit_file",
629
+ "edit_file_hashed",
630
+ ]);
631
+
632
+ /**
633
+ * Compute the content an edit tool WOULD write, without writing it — the
634
+ * left/right sides for an IDE diff review. Mirrors the corresponding
635
+ * executeToolInner cases exactly (write_file / edit_file / edit_file_hashed,
636
+ * the latter via the same pure replaceByHash). Returns
637
+ * `{ filePath, newContent, originalText|null }` or null when the edit cannot
638
+ * be computed (missing file, anchor/old_string miss, bad args) — the caller
639
+ * then falls back to the normal confirmation path so the tool can produce its
640
+ * own diagnostics.
641
+ */
642
+ export function computeProposedEdit(name, args = {}, cwd = process.cwd()) {
643
+ try {
644
+ if (!args.path) return null;
645
+ const filePath = path.resolve(cwd, args.path);
646
+ if (name === "write_file") {
647
+ if (typeof args.content !== "string") return null;
648
+ const originalText = fs.existsSync(filePath)
649
+ ? fs.readFileSync(filePath, "utf8")
650
+ : "";
651
+ return { filePath, newContent: args.content, originalText };
652
+ }
653
+ if (name === "edit_file") {
654
+ if (!fs.existsSync(filePath)) return null;
655
+ const content = fs.readFileSync(filePath, "utf8");
656
+ if (
657
+ typeof args.old_string !== "string" ||
658
+ !content.includes(args.old_string)
659
+ ) {
660
+ return null;
661
+ }
662
+ return {
663
+ filePath,
664
+ newContent: content.replace(args.old_string, args.new_string),
665
+ originalText: content,
666
+ };
667
+ }
668
+ if (name === "edit_file_hashed") {
669
+ if (!fs.existsSync(filePath)) return null;
670
+ if (!args.anchor_hash || typeof args.new_line !== "string") return null;
671
+ const original = fs.readFileSync(filePath, "utf8");
672
+ const result = replaceByHash(original, {
673
+ anchorHash: args.anchor_hash,
674
+ expectedLine: args.expected_line,
675
+ newLine: args.new_line,
676
+ });
677
+ if (!result.success) return null;
678
+ return { filePath, newContent: result.content, originalText: original };
679
+ }
680
+ } catch {
681
+ // unreadable file etc. → no proposal, normal path handles it
682
+ }
683
+ return null;
684
+ }
685
+
686
+ /**
687
+ * Shared IDE-diff approval routing for an `ask` decision about a file edit
688
+ * (used by BOTH the settings-rules ask and the PreToolUse-hook ask). Returns
689
+ * { outcome:"accepted", result } — the IDE wrote the file; the caller MUST
690
+ * return `result` and skip execution
691
+ * { outcome:"rejected", result } — deny with `result`, file untouched
692
+ * null — not applicable (non-edit tool, headless,
693
+ * no IDE, disabled, no proposal, IDE died)
694
+ * → caller falls back to its own confirm.
695
+ */
696
+ async function tryIdeDiffApprovalForEdit(
697
+ name,
698
+ args,
699
+ context,
700
+ cwd,
701
+ { rule, source } = {},
702
+ ) {
703
+ if (!IDE_DIFF_EDIT_TOOLS.has(name)) return null;
704
+ if (typeof context.permissionConfirm !== "function") return null; // interactive only
705
+ if (!context.mcpClient || !context.externalToolExecutors) return null;
706
+ try {
707
+ const { ideDiffApprovalEnabled, hasIdeOpenDiff, requestIdeDiffApproval } =
708
+ await import("../lib/ide-context.js");
709
+ const mcpLike = {
710
+ mcpClient: context.mcpClient,
711
+ externalToolExecutors: context.externalToolExecutors,
712
+ };
713
+ if (!ideDiffApprovalEnabled() || !hasIdeOpenDiff(mcpLike)) return null;
714
+ const proposal = computeProposedEdit(name, args, cwd);
715
+ if (!proposal) return null;
716
+ const verdict = await requestIdeDiffApproval(mcpLike, {
717
+ path: proposal.filePath,
718
+ modifiedText: proposal.newContent,
719
+ originalText: proposal.originalText,
720
+ title: `cc agent: ${name} ${path.basename(proposal.filePath)}`,
721
+ });
722
+ if (verdict?.outcome === "accepted") {
723
+ return {
724
+ outcome: "accepted",
725
+ result: {
726
+ success: true,
727
+ path: proposal.filePath,
728
+ appliedVia: "ide-diff",
729
+ ...(verdict.finalText != null &&
730
+ verdict.finalText !== proposal.newContent
731
+ ? { userEdited: true }
732
+ : {}),
733
+ policy: { decision: "allow", rule, via: "ide-diff" },
734
+ },
735
+ };
736
+ }
737
+ if (verdict?.outcome === "rejected") {
738
+ return {
739
+ outcome: "rejected",
740
+ result: {
741
+ error: `[Permission] "${name}" was rejected in the IDE diff review (${source}: ${rule}).`,
742
+ policy: { decision: "deny", rule, via: "ide-diff" },
743
+ },
744
+ };
745
+ }
746
+ } catch (_err) {
747
+ // diff-approval routing is best-effort — fall back to the normal confirm
748
+ }
749
+ return null;
750
+ }
751
+
606
752
  /**
607
753
  * Execute a single tool call with plan-mode filtering and hook pipeline.
608
754
  *
@@ -719,6 +865,17 @@ export async function executeTool(name, args, context = {}) {
719
865
  // 3 + 4. settings ask / allow (only reached when neither layer denied)
720
866
  let ruleAllowed = false;
721
867
  if (settingsVerdict.decision === "ask") {
868
+ // IDE-native diff approval (Claude-Code parity): for file edits in an
869
+ // interactive session with an IDE bridge connected, review the edit in
870
+ // the editor instead of a terminal y/N. Accepted = the IDE wrote the
871
+ // file → return the synthetic result and SKIP execution; rejected =
872
+ // deny; null = fall through to the normal confirm below. Shared with the
873
+ // PreToolUse-hook ask path (tryIdeDiffApprovalForEdit).
874
+ const ide = await tryIdeDiffApprovalForEdit(name, args, context, cwd, {
875
+ rule: settingsVerdict.rule,
876
+ source: "settings rule",
877
+ });
878
+ if (ide) return ide.result;
722
879
  const confirm = context.permissionConfirm || context.shellConfirm || null;
723
880
  const ok =
724
881
  typeof confirm === "function"
@@ -792,7 +949,12 @@ export async function executeTool(name, args, context = {}) {
792
949
  }
793
950
  if (context.settingsHooks) {
794
951
  const pre = await runSettingsPreToolUseHooks(name, args, context, cwd);
952
+ // A hook `ask` resolved by the IDE diff review: accepted → the IDE
953
+ // already wrote the file, return the synthetic result and skip the tool;
954
+ // rejected → the ide-diff deny shape (via:"ide-diff", not via:"hook").
955
+ if (pre.ideApplied) return pre.ideApplied;
795
956
  if (pre.blocked) {
957
+ if (pre.ideResult) return pre.ideResult;
796
958
  return {
797
959
  error: `[Hook] PreToolUse blocked "${name}"${pre.reason ? ": " + pre.reason : ""}`,
798
960
  policy: { decision: "block", via: "hook", hook: pre.hook || null },