chainlesschain 0.162.39 → 0.162.41

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 (172) 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-Ut7EevnG.js} +1 -1
  4. package/src/assets/web-panel/assets/{ActionButton-XHoOmsbP.js → ActionButton-Dv6BlfJg.js} +1 -1
  5. package/src/assets/web-panel/assets/{Analytics--xaFkDnL.js → Analytics-TQVQuJ7u.js} +3 -3
  6. package/src/assets/web-panel/assets/{AppLayout-CSa3FBn8.js → AppLayout-MSqLm2WK.js} +5 -5
  7. package/src/assets/web-panel/assets/{Audit-ONWXiAwG.js → Audit-mw81HwVy.js} +1 -1
  8. package/src/assets/web-panel/assets/{Backup-CKOPNdgy.js → Backup-BQcPWDb1.js} +1 -1
  9. package/src/assets/web-panel/assets/{BaseInput-PNj4uVqg.js → BaseInput-BYo_pwBH.js} +1 -1
  10. package/src/assets/web-panel/assets/{Chat-CZCulyXV.js → Chat-zi3YUKx2.js} +5 -5
  11. package/src/assets/web-panel/assets/{ChatBubbleRenderer-CjuJpfpV.js → ChatBubbleRenderer-DWSm1XJJ.js} +1 -1
  12. package/src/assets/web-panel/assets/{Checkbox-jvy668lD.js → Checkbox-BvC8Erjt.js} +1 -1
  13. package/src/assets/web-panel/assets/{Codegen-DhUebOQD.js → Codegen-C32vx0OP.js} +1 -1
  14. package/src/assets/web-panel/assets/{Col-BiBvHfdT.js → Col-DMBwmqyZ.js} +1 -1
  15. package/src/assets/web-panel/assets/{Community-CmEdEti-.js → Community-nDWncmKV.js} +1 -1
  16. package/src/assets/web-panel/assets/{Compact-CtxpF4R5.js → Compact-lIc1HFn8.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compliance-CvPTrTAJ.js → Compliance-D14I_gd2.js} +1 -1
  18. package/src/assets/web-panel/assets/{Cowork-BMafGHjy.js → Cowork-BiNI-_ZL.js} +3 -3
  19. package/src/assets/web-panel/assets/{Cron-mdg_4TR1.js → Cron-N13sFzHb.js} +2 -2
  20. package/src/assets/web-panel/assets/{Crosschain--dGxsUvn.js → Crosschain-Dlnl0-v6.js} +1 -1
  21. package/src/assets/web-panel/assets/{DID-C9oKaCml.js → DID-CxtYS31I.js} +2 -2
  22. package/src/assets/web-panel/assets/{Dashboard-CoGxKMvy.js → Dashboard-G4UnHlTR.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dropdown-CDDu3ZZ3.js → Dropdown-BazlxFGY.js} +1 -1
  24. package/src/assets/web-panel/assets/{EmailListRenderer-Dy7_r9Ag.js → EmailListRenderer-BrpNdihm.js} +1 -1
  25. package/src/assets/web-panel/assets/{FamilyGuardDashboard-CNg6vImJ.js → FamilyGuardDashboard-HD7jbOOR.js} +1 -1
  26. package/src/assets/web-panel/assets/{Federation-CT61bf3u.js → Federation-Bz8lzAGI.js} +1 -1
  27. package/src/assets/web-panel/assets/{FormItemContext-CSLRnXhg.js → FormItemContext-CcyzGS00.js} +1 -1
  28. package/src/assets/web-panel/assets/{GenericCardRenderer-CZ4NE5N3.js → GenericCardRenderer-DRo9cwmp.js} +1 -1
  29. package/src/assets/web-panel/assets/{Git-DBuOma3L.js → Git-B7bn333J.js} +2 -2
  30. package/src/assets/web-panel/assets/{Governance-BTU_SEef.js → Governance-DZX9CWAM.js} +1 -1
  31. package/src/assets/web-panel/assets/{Inference-47SAmLC_.js → Inference-B3XhsL6W.js} +1 -1
  32. package/src/assets/web-panel/assets/{KnowledgeGraph-DCrK5vP4.js → KnowledgeGraph-CxFRTlQe.js} +1 -1
  33. package/src/assets/web-panel/assets/{Logs-BqiDxdav.js → Logs-xuys6mKH.js} +2 -2
  34. package/src/assets/web-panel/assets/{Marketplace-CReUjsDt.js → Marketplace-CXyxv4WU.js} +1 -1
  35. package/src/assets/web-panel/assets/{McpTools-agZBV3p8.js → McpTools-BzZLQVI3.js} +6 -6
  36. package/src/assets/web-panel/assets/{Memory-C_YvUtyS.js → Memory-BANtaBa7.js} +2 -2
  37. package/src/assets/web-panel/assets/{MobileBridge-41fP1Tui.js → MobileBridge-BJIwjmxr.js} +3 -3
  38. package/src/assets/web-panel/assets/{MobileProjects-BkqLvGfL.js → MobileProjects-B857uSAZ.js} +1 -1
  39. package/src/assets/web-panel/assets/{Mtc-JFJCXUnk.js → Mtc-Cn7ceFEz.js} +5 -5
  40. package/src/assets/web-panel/assets/{MtcAudit-BHNpPZC9.js → MtcAudit-B0zE978G.js} +6 -6
  41. package/src/assets/web-panel/assets/{Multisig-DuCRumiz.js → Multisig-CQFT0wXW.js} +3 -3
  42. package/src/assets/web-panel/assets/{NLProgramming-DK-g0fKY.js → NLProgramming-DSxKdVY-.js} +1 -1
  43. package/src/assets/web-panel/assets/{Notes-BSMcjsPf.js → Notes-DtlTfam8.js} +3 -3
  44. package/src/assets/web-panel/assets/{NotificationSettings-9ouC118H.js → NotificationSettings-CHQwayAg.js} +1 -1
  45. package/src/assets/web-panel/assets/OrderTableRenderer-Brpmzh9n.js +1 -0
  46. package/src/assets/web-panel/assets/{Organization-DSV7oRnR.js → Organization-nF_tzZDT.js} +4 -4
  47. package/src/assets/web-panel/assets/{Overflow-DVkkORc3.js → Overflow-CgCSf_PH.js} +1 -1
  48. package/src/assets/web-panel/assets/{P2P-BXXjkkQD.js → P2P-Bvn46bLY.js} +2 -2
  49. package/src/assets/web-panel/assets/{PdhVaultBrowser-O5hNnLTP.js → PdhVaultBrowser-Bzl9k7Gj.js} +5 -5
  50. package/src/assets/web-panel/assets/{Permissions-D_s0H5Av.js → Permissions-Dmezbuo8.js} +4 -4
  51. package/src/assets/web-panel/assets/{PersonalDataHub-CzMDrwUi.js → PersonalDataHub-lCKRxwZr.js} +3 -3
  52. package/src/assets/web-panel/assets/{Pipeline-i9krLVTL.js → Pipeline-DDCGm9PA.js} +1 -1
  53. package/src/assets/web-panel/assets/{Privacy-cMQcj9I8.js → Privacy-Cgu18Kjl.js} +1 -1
  54. package/src/assets/web-panel/assets/{ProjectInit-Ca_l7avo.js → ProjectInit-CkF1AeRY.js} +2 -2
  55. package/src/assets/web-panel/assets/{ProjectSettings-BkaIhd6b.js → ProjectSettings-D0Q-orz1.js} +2 -2
  56. package/src/assets/web-panel/assets/Projects-KfGELrSY.js +1 -0
  57. package/src/assets/web-panel/assets/{Providers-D0nzYiqz.js → Providers-BACLV0z8.js} +1 -1
  58. package/src/assets/web-panel/assets/{QuickAsk-Bzzr9d0f.js → QuickAsk-CPsZUqDl.js} +1 -1
  59. package/src/assets/web-panel/assets/{Recommend-C-UFbQnX.js → Recommend-5jX0OI1-.js} +1 -1
  60. package/src/assets/web-panel/assets/{Reputation-BKMIKO5F.js → Reputation-5JKv54z0.js} +1 -1
  61. package/src/assets/web-panel/assets/{Row-Bs7htK1T.js → Row-DLiTF5LY.js} +1 -1
  62. package/src/assets/web-panel/assets/{RssFeed-v6MdULUh.js → RssFeed-CFdGmCKW.js} +3 -3
  63. package/src/assets/web-panel/assets/{Search-DlRWYzvz.js → Search-BjIOnmA7.js} +1 -1
  64. package/src/assets/web-panel/assets/{Security-DXWO37xX.js → Security-BujPqQSo.js} +4 -4
  65. package/src/assets/web-panel/assets/{Services-C2tWA-O0.js → Services-ChciPnMu.js} +2 -2
  66. package/src/assets/web-panel/assets/{Skeleton-Q8pIYY4a.js → Skeleton-Cwswp1Jv.js} +1 -1
  67. package/src/assets/web-panel/assets/{Skills-D7XBlErj.js → Skills-CtwR4vJV.js} +1 -1
  68. package/src/assets/web-panel/assets/{Sla-CiyMVPJ1.js → Sla-pRIevich.js} +1 -1
  69. package/src/assets/web-panel/assets/{SpeechSettings-CadCeeiR.js → SpeechSettings-BRqB28Ai.js} +1 -1
  70. package/src/assets/web-panel/assets/{SyncSettings-DzNAUhQq.js → SyncSettings-BYyj58_h.js} +2 -2
  71. package/src/assets/web-panel/assets/Tasks-DTLpT48U.js +1 -0
  72. package/src/assets/web-panel/assets/{Templates-DfgEpUa4.js → Templates-Bbz_h7oW.js} +1 -1
  73. package/src/assets/web-panel/assets/{Tenant-C8ajkuYi.js → Tenant-D-H4E3cu.js} +1 -1
  74. package/src/assets/web-panel/assets/{Terminal-B9rHwQQx.js → Terminal-CLLi0-lV.js} +2 -2
  75. package/src/assets/web-panel/assets/{TimelineRenderer-D1ZVNezX.js → TimelineRenderer-BKI6eG0k.js} +1 -1
  76. package/src/assets/web-panel/assets/{Tokens-CAkED4mx.js → Tokens-rsE_yDjM.js} +1 -1
  77. package/src/assets/web-panel/assets/{Trigger-CJSrm6X0.js → Trigger-8TpwuTGk.js} +1 -1
  78. package/src/assets/web-panel/assets/{Trust-B-TeorSk.js → Trust-sMtZkHPs.js} +1 -1
  79. package/src/assets/web-panel/assets/{UkeySign-Di7Ymofy.js → UkeySign-BAy2bAdG.js} +1 -1
  80. package/src/assets/web-panel/assets/{VideoEditing-DM1eYNZe.js → VideoEditing-CBeR_DYK.js} +1 -1
  81. package/src/assets/web-panel/assets/{Wallet-DvRWkbmR.js → Wallet-BymDnBcq.js} +4 -4
  82. package/src/assets/web-panel/assets/{WebAuthn-CeZ3Y622.js → WebAuthn-DQIjmqNz.js} +5 -5
  83. package/src/assets/web-panel/assets/{WorkflowEditor-Cq8c4h5j.js → WorkflowEditor-Cj7PB73f.js} +1 -1
  84. package/src/assets/web-panel/assets/{chat-7-WfML6Q.js → chat-DYnGj4vi.js} +1 -1
  85. package/src/assets/web-panel/assets/{colors-D6FgCmB-.js → colors-qOLKZNvN.js} +1 -1
  86. package/src/assets/web-panel/assets/{compact-item-ClYV25qi.js → compact-item-BpjCLPcW.js} +1 -1
  87. package/src/assets/web-panel/assets/{createContext-CDhtjdkV.js → createContext-CfakUZVQ.js} +1 -1
  88. package/src/assets/web-panel/assets/devWarning-DgtRXlrj.js +1 -0
  89. package/src/assets/web-panel/assets/{hasIn-DZSH5LQd.js → hasIn-C9RW1s7t.js} +1 -1
  90. package/src/assets/web-panel/assets/{index-B4PMzmOx.js → index-8Ia91vNV.js} +1 -1
  91. package/src/assets/web-panel/assets/{index-B78X5S22.js → index-B4kS312z.js} +1 -1
  92. package/src/assets/web-panel/assets/{index-CDX4QU3k.js → index-BE67I0SW.js} +1 -1
  93. package/src/assets/web-panel/assets/{index-DPEYvNvq.js → index-BFOSDeeo.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-CKgS8E_X.js → index-BIz-pX0k.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-Di9pFrHV.js → index-BJoWi1aR.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BHeK8I5A.js → index-B_K0YtG2.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-BpzOUiSb.js → index-BdR8XRyF.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-DWRoh3_3.js → index-BfyRXPyV.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-C7pQa2is.js → index-Bl5LBZJM.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-DZ4zuoCP.js → index-BlxRICmz.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-B_mMFQ4S.js → index-BxiHBsfU.js} +1 -1
  102. package/src/assets/web-panel/assets/{index---azBCXl.js → index-C2S1hUWG.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-BJ7mrOaB.js → index-CEHyZ77C.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CxwfFZ1u.js → index-CJZ2noI2.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DGj1orXm.js → index-COYEuArt.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-DL6GFJAd.js → index-CVZTLSL1.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-z-R0KaJS.js → index-CbnJ6FsO.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-tU6pZ1TP.js → index-CvWFTG56.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-rCs9VJJp.js → index-D-RzTqlR.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-B6VWGnwq.js → index-DA80prWe.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-D0YzTJJO.js → index-DAjszh8P.js} +1 -1
  112. package/src/assets/web-panel/assets/index-DIGTMmnW.js +1 -0
  113. package/src/assets/web-panel/assets/{index-DjG82V0v.js → index-DQvVYNoJ.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-DLizxxId.js → index-DSWdpR3c.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-C7sC56w8.js → index-DadPmrxI.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-BlBF_l8m.js → index-DgMJagCq.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-Bj8hZiyL.js → index-DkmLJFE_.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CrTmxbL8.js → index-DzXYG5YJ.js} +1 -1
  119. package/src/assets/web-panel/assets/index-Ef5jERRW.js +1 -0
  120. package/src/assets/web-panel/assets/{index-BUOPjAUM.js → index-JkOMWGMX.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-CmU631Je.js → index-T3bIqK_p.js} +3 -3
  122. package/src/assets/web-panel/assets/{index-BqOIoEo6.js → index-UiiqS5k2.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CSjoWPxB.js → index-VYIJmPvJ.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-B13QnrnE.js → index-ZCtDWP2C.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DgaF1F0W.js → index-f9yoj84i.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-Or_McYjX.js → index-lPc7EzUi.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-DGJK8D0l.js → index-m9JeDv6B.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-CWOkL-8O.js → index-qf0fAus7.js} +1 -1
  129. package/src/assets/web-panel/assets/{initDefaultProps-CSdsIGy3.js → initDefaultProps-DgsgQr1H.js} +1 -1
  130. package/src/assets/web-panel/assets/{motion-Do-AcZV4.js → motion-TeUH7wzx.js} +1 -1
  131. package/src/assets/web-panel/assets/{move-BmgOoMsi.js → move-DdkIeWQx.js} +1 -1
  132. package/src/assets/web-panel/assets/{omit-D4Tm7-s9.js → omit-BH_PH6HT.js} +1 -1
  133. package/src/assets/web-panel/assets/{pickAttrs-CuWA8-lj.js → pickAttrs-CllCh-Nl.js} +1 -1
  134. package/src/assets/web-panel/assets/{placementArrow-BSbEF5op.js → placementArrow-BCjE2AzM.js} +1 -1
  135. package/src/assets/web-panel/assets/{responsiveObserve-GIMJwB_9.js → responsiveObserve-BAVGAvRQ.js} +1 -1
  136. package/src/assets/web-panel/assets/{slide-DlZxpIBe.js → slide-D4ZW-Inn.js} +1 -1
  137. package/src/assets/web-panel/assets/{statusUtils-BZ26LPlh.js → statusUtils-j4pxhmKV.js} +1 -1
  138. package/src/assets/web-panel/assets/{styleChecker-Yn_3FZ0l.js → styleChecker-DH2SLtPg.js} +1 -1
  139. package/src/assets/web-panel/assets/{useFlexGapSupport-O_LOE1AB.js → useFlexGapSupport-CYMMs-_Q.js} +1 -1
  140. package/src/assets/web-panel/assets/{useFs-VFMyQqtl.js → useFs-BOX2ddKh.js} +1 -1
  141. package/src/assets/web-panel/assets/{usePersonalDataHub-B_hyrGB-.js → usePersonalDataHub-BwcnN5z_.js} +1 -1
  142. package/src/assets/web-panel/assets/{vnode-D4LttGy7.js → vnode-Cwalh7Hj.js} +1 -1
  143. package/src/assets/web-panel/assets/{zoom-KnTK1fjj.js → zoom-B2_q_nbu.js} +1 -1
  144. package/src/assets/web-panel/index.html +1 -1
  145. package/src/commands/agent.js +38 -4
  146. package/src/commands/init.js +115 -2
  147. package/src/commands/mcp.js +57 -0
  148. package/src/commands/memory.js +62 -0
  149. package/src/commands/session.js +106 -12
  150. package/src/index.js +10 -0
  151. package/src/lib/agent-core.js +1 -0
  152. package/src/lib/agent-session-export.js +124 -0
  153. package/src/lib/ide-context.js +62 -0
  154. package/src/lib/init-ai-refine.js +66 -0
  155. package/src/lib/json-schema-output.js +181 -0
  156. package/src/lib/mcp-serve.js +259 -0
  157. package/src/lib/project-instructions.js +364 -0
  158. package/src/lib/project-inventory.js +355 -0
  159. package/src/lib/repl-bang-memorize.js +142 -0
  160. package/src/lib/repl-completer.js +25 -4
  161. package/src/lib/repl-rewind.js +107 -0
  162. package/src/lib/update-notice-refresh.mjs +10 -0
  163. package/src/lib/update-notice.js +154 -0
  164. package/src/repl/agent-repl.js +263 -1
  165. package/src/runtime/agent-core.js +162 -0
  166. package/src/runtime/system-prompt.js +21 -1
  167. package/src/assets/web-panel/assets/OrderTableRenderer-LG2nUO5y.js +0 -1
  168. package/src/assets/web-panel/assets/Projects-Dy9yNmDg.js +0 -1
  169. package/src/assets/web-panel/assets/Tasks-BjdHjZeb.js +0 -1
  170. package/src/assets/web-panel/assets/devWarning-O0FVFeZg.js +0 -1
  171. package/src/assets/web-panel/assets/index--ANIKvhL.js +0 -1
  172. package/src/assets/web-panel/assets/index-DUfp4rnQ.js +0 -1
@@ -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
+ }
@@ -718,6 +718,18 @@ export async function startAgentRepl(options = {}) {
718
718
  } catch (_err) {
719
719
  // Non-critical
720
720
  }
721
+ // Resume recap (offline, extractive — no LLM): a quick "where were we"
722
+ // so the user doesn't have to scroll the old transcript.
723
+ try {
724
+ const { buildResumeRecap } = await import("../lib/repl-rewind.js");
725
+ const recap = buildResumeRecap(messages);
726
+ if (recap) {
727
+ logger.log(chalk.bold("Recap:"));
728
+ for (const line of recap) logger.log(chalk.gray(` ${line}`));
729
+ }
730
+ } catch (_err) {
731
+ /* non-critical */
732
+ }
721
733
  }
722
734
 
723
735
  const getPrompt = () => {
@@ -737,6 +749,31 @@ export async function startAgentRepl(options = {}) {
737
749
  const { makeAtCompleter } = await import("../lib/repl-completer.js");
738
750
  const atCompleter = makeAtCompleter({
739
751
  cwd: process.cwd(),
752
+ // Keep in sync with the rl.on("line") handlers + /help below.
753
+ slashCommands: [
754
+ "/auto",
755
+ "/clear",
756
+ "/compact",
757
+ "/context",
758
+ "/cowork",
759
+ "/exit",
760
+ "/help",
761
+ "/mcp",
762
+ "/model",
763
+ "/output-style",
764
+ "/plan",
765
+ "/profile",
766
+ "/provider",
767
+ "/quit",
768
+ "/reindex",
769
+ "/rewind",
770
+ "/search",
771
+ "/session",
772
+ "/stats",
773
+ "/statusline",
774
+ "/sub-agents",
775
+ "/task",
776
+ ],
740
777
  getIdeOpenFiles: async () => {
741
778
  const exec = _adhocMcp?.externalToolExecutors?.mcp__ide__getOpenEditors;
742
779
  if (!exec || exec.kind !== "mcp" || !_adhocMcp?.mcpClient?.callTool) {
@@ -763,6 +800,53 @@ export async function startAgentRepl(options = {}) {
763
800
  completer: atCompleter,
764
801
  });
765
802
 
803
+ // Esc interrupt (Claude-Code parity): pressing Esc while a turn is in
804
+ // flight aborts the in-flight agentLoop through its existing AbortSignal
805
+ // seam (throwIfAborted at each iteration); partial conversation is kept.
806
+ // Idle Esc presses (no active turn) are ignored, and escape-prefixed key
807
+ // sequences (arrows etc.) never reach here as bare "escape".
808
+ let _turnAbort = null;
809
+ let _lastIdleEscAt = 0;
810
+ if (process.stdin.isTTY) {
811
+ process.stdin.on("keypress", (_str, key) => {
812
+ if (!key || key.name !== "escape" || key.meta) return;
813
+ if (_turnAbort) {
814
+ process.stdout.write(chalk.yellow("\n⎋ interrupting…\n"));
815
+ try {
816
+ _turnAbort.abort();
817
+ } catch {
818
+ /* already aborted */
819
+ }
820
+ _turnAbort = null;
821
+ return;
822
+ }
823
+ // Double-Esc while idle → rewind picker shortcut (Claude-Code parity);
824
+ // the actual rewind is `/rewind <n>` so stdin stays readline-owned.
825
+ const nowTs = Date.now();
826
+ if (nowTs - _lastIdleEscAt < 600) {
827
+ _lastIdleEscAt = 0;
828
+ import("../lib/repl-rewind.js")
829
+ .then(({ listUserTurns, renderTurnList }) => {
830
+ process.stdout.write(
831
+ chalk.bold("\nRewind — pick a user turn (newest first):\n"),
832
+ );
833
+ process.stdout.write(
834
+ `${renderTurnList(listUserTurns(messages))}\n`,
835
+ );
836
+ process.stdout.write(
837
+ chalk.gray(
838
+ "Run /rewind <n> to rewind the conversation (files: cc checkpoint restore).\n",
839
+ ),
840
+ );
841
+ prompt();
842
+ })
843
+ .catch(() => {});
844
+ } else {
845
+ _lastIdleEscAt = nowTs;
846
+ }
847
+ });
848
+ }
849
+
766
850
  logger.log(chalk.bold("\nChainlessChain Agent"));
767
851
  logger.log(
768
852
  chalk.gray(`Model: ${model} Provider: ${provider} CWD: ${process.cwd()}`),
@@ -847,13 +931,57 @@ export async function startAgentRepl(options = {}) {
847
931
 
848
932
  prompt();
849
933
 
850
- rl.on("line", async (input) => {
934
+ // Steering (Claude-Code parity): typing while a turn is running QUEUES the
935
+ // line instead of racing a second concurrent turn; the queue drains FIFO
936
+ // when the current turn finishes.
937
+ let _processingLine = false;
938
+ const _pendingLines = [];
939
+ const handleLine = async (input) => {
851
940
  const trimmed = input.trim();
852
941
  if (!trimmed) {
853
942
  prompt();
854
943
  return;
855
944
  }
856
945
 
946
+ // `!` bash passthrough (Claude-Code parity): run the command right here —
947
+ // no LLM round-trip — and fold the output into the conversation context.
948
+ if (trimmed.startsWith("!") && trimmed.slice(1).trim()) {
949
+ try {
950
+ const { runBangCommand } = await import("../lib/repl-bang-memorize.js");
951
+ const res = runBangCommand(trimmed, { cwd: process.cwd() });
952
+ logger.log(chalk.gray(`$ ${res.cmd}`));
953
+ if (res.stdout) process.stdout.write(res.stdout.endsWith("\n") ? res.stdout : `${res.stdout}\n`);
954
+ if (res.stderr) process.stderr.write(chalk.red(res.stderr.endsWith("\n") ? res.stderr : `${res.stderr}\n`));
955
+ if (res.error) logger.error(`shell error: ${res.error.message}`);
956
+ logger.log(chalk.gray(`(exit ${res.exitCode})`));
957
+ messages.push(res.contextMessage);
958
+ } catch (err) {
959
+ logger.error(`! command failed: ${err.message}`);
960
+ }
961
+ prompt();
962
+ return;
963
+ }
964
+
965
+ // `#` quick-memorize (Claude-Code parity): append a note to the project
966
+ // cc.md (auto-loaded next session) and keep it active in this one.
967
+ if (trimmed.startsWith("#") && trimmed.slice(1).trim()) {
968
+ try {
969
+ const { appendMemoryNote } = await import("../lib/repl-bang-memorize.js");
970
+ const res = appendMemoryNote(trimmed, { cwd: process.cwd() });
971
+ messages.push({
972
+ role: "system",
973
+ content: `<memory-note source="${res.target}">${res.note}</memory-note>`,
974
+ });
975
+ logger.log(
976
+ chalk.green(`✔ remembered in ${res.target}${res.created ? " (created)" : ""}`),
977
+ );
978
+ } catch (err) {
979
+ logger.error(`# memorize failed: ${err.message}`);
980
+ }
981
+ prompt();
982
+ return;
983
+ }
984
+
857
985
  // Slash commands
858
986
  if (trimmed === "/exit" || trimmed === "/quit") {
859
987
  logger.log(chalk.gray("\nGoodbye!"));
@@ -863,6 +991,12 @@ export async function startAgentRepl(options = {}) {
863
991
 
864
992
  if (trimmed === "/help") {
865
993
  logger.log(chalk.bold("\nCommands:"));
994
+ logger.log(
995
+ ` ${chalk.cyan("! <cmd>")} Run a shell command directly (output joins context)`,
996
+ );
997
+ logger.log(
998
+ ` ${chalk.cyan("# <note>")} Remember a note in the project cc.md`,
999
+ );
866
1000
  logger.log(` ${chalk.cyan("/exit")} Exit the agent`);
867
1001
  logger.log(
868
1002
  ` ${chalk.cyan("/model")} Show/change model (/model <name>)`,
@@ -872,6 +1006,12 @@ export async function startAgentRepl(options = {}) {
872
1006
  logger.log(
873
1007
  ` ${chalk.cyan("/statusline")} Context-usage line on/off (/statusline [on|off])`,
874
1008
  );
1009
+ logger.log(
1010
+ ` ${chalk.cyan("/context")} Live context-window usage by role`,
1011
+ );
1012
+ logger.log(
1013
+ ` ${chalk.cyan("/rewind")} Rewind conversation to an earlier turn (double-Esc lists)`,
1014
+ );
875
1015
  logger.log(
876
1016
  ` ${chalk.cyan("/compact")} Smart compact (importance-based)`,
877
1017
  );
@@ -1110,6 +1250,90 @@ export async function startAgentRepl(options = {}) {
1110
1250
  return;
1111
1251
  }
1112
1252
 
1253
+ if (trimmed === "/rewind" || trimmed.startsWith("/rewind ")) {
1254
+ try {
1255
+ const { listUserTurns, rewindToTurn, renderTurnList } = await import(
1256
+ "../lib/repl-rewind.js"
1257
+ );
1258
+ const arg = trimmed.slice("/rewind".length).trim();
1259
+ if (!arg) {
1260
+ logger.log(
1261
+ chalk.bold("\nRewind — pick a user turn (newest first):"),
1262
+ );
1263
+ logger.log(renderTurnList(listUserTurns(messages)));
1264
+ logger.log(
1265
+ chalk.gray(
1266
+ "Usage: /rewind <n> (conversation only — restore files with `cc checkpoint restore`)",
1267
+ ),
1268
+ );
1269
+ } else {
1270
+ const res = rewindToTurn(messages, arg);
1271
+ if (!res) {
1272
+ logger.error(`No such turn: ${arg} — run /rewind to list.`);
1273
+ } else {
1274
+ logger.log(
1275
+ chalk.yellow(
1276
+ `⎌ rewound — dropped ${res.removed} message(s); edit and resend below`,
1277
+ ),
1278
+ );
1279
+ prompt();
1280
+ if (res.text) rl.write(res.text);
1281
+ return;
1282
+ }
1283
+ }
1284
+ } catch (err) {
1285
+ logger.error(`/rewind failed: ${err.message}`);
1286
+ }
1287
+ prompt();
1288
+ return;
1289
+ }
1290
+
1291
+ if (trimmed === "/context") {
1292
+ // Live-session twin of `cc context` (Claude-Code /context parity):
1293
+ // bucket the CURRENT in-memory conversation by role against the model
1294
+ // window. Reuses the same categorizer + estimator as the archived view.
1295
+ try {
1296
+ const { categorizeContext } = await import("../commands/context.js");
1297
+ const { estimateTokens } = await import(
1298
+ "../harness/prompt-compressor.js"
1299
+ );
1300
+ const { buckets, counts, total } = categorizeContext(
1301
+ messages,
1302
+ estimateTokens,
1303
+ );
1304
+ const window = getContextWindow(model, provider) || 0;
1305
+ logger.log(chalk.bold("\nContext usage (live session):"));
1306
+ const rows = [
1307
+ ["system", buckets.system, counts.system],
1308
+ ["user", buckets.user, counts.user],
1309
+ ["assistant", buckets.assistant, counts.assistant],
1310
+ ["tool", buckets.tool, counts.tool],
1311
+ ["tool_calls", buckets.toolCalls, null],
1312
+ ];
1313
+ for (const [label, tok, n] of rows) {
1314
+ if (!tok) continue;
1315
+ const share = total ? Math.round((tok / total) * 100) : 0;
1316
+ logger.log(
1317
+ ` ${label.padEnd(11)}${String(tok).padStart(9)} tok ${String(share).padStart(3)}%${
1318
+ n != null ? chalk.gray(` (${n} msgs)`) : ""
1319
+ }`,
1320
+ );
1321
+ }
1322
+ const pct = window ? Math.round((total / window) * 100) : null;
1323
+ logger.log(
1324
+ ` ${"total".padEnd(11)}${String(total).padStart(9)} tok${
1325
+ window
1326
+ ? ` ${pct}% of ${window} (${Math.max(0, window - total)} left)`
1327
+ : ""
1328
+ }`,
1329
+ );
1330
+ } catch (err) {
1331
+ logger.error(`/context failed: ${err.message}`);
1332
+ }
1333
+ prompt();
1334
+ return;
1335
+ }
1336
+
1113
1337
  if (trimmed === "/compact") {
1114
1338
  if (_compressor && messages.length > 3) {
1115
1339
  const { messages: compacted, stats } = await _compressor.compress(
@@ -2024,7 +2248,9 @@ export async function startAgentRepl(options = {}) {
2024
2248
  } catch (_e) {
2025
2249
  /* goal binding is best-effort — fall back to defaultPrepareCall */
2026
2250
  }
2251
+ _turnAbort = new AbortController();
2027
2252
  const { content: response, usageEvents } = await agentLoop(messages, {
2253
+ signal: _turnAbort.signal,
2028
2254
  provider,
2029
2255
  model: activeModel,
2030
2256
  thinking,
@@ -2051,6 +2277,7 @@ export async function startAgentRepl(options = {}) {
2051
2277
  externalToolDescriptors: _adhocMcp?.externalToolDescriptors,
2052
2278
  chatFn: _fallbackChatFn,
2053
2279
  });
2280
+ _turnAbort = null;
2054
2281
 
2055
2282
  if (sessionId && usageEvents?.length) {
2056
2283
  for (const ue of usageEvents) {
@@ -2176,6 +2403,16 @@ export async function startAgentRepl(options = {}) {
2176
2403
  }
2177
2404
  }
2178
2405
  } catch (err) {
2406
+ _turnAbort = null;
2407
+ // Esc interrupt: an aborted turn is normal flow, not an error — the
2408
+ // partial conversation stays usable and queued lines still drain.
2409
+ if (err?.name === "AbortError" || /abort/i.test(err?.message || "")) {
2410
+ logger.log(
2411
+ chalk.yellow("⎋ turn interrupted — partial progress kept"),
2412
+ );
2413
+ prompt();
2414
+ return;
2415
+ }
2179
2416
  logger.error(`Error: ${err.message}`);
2180
2417
 
2181
2418
  // Record error for context injection
@@ -2199,6 +2436,31 @@ export async function startAgentRepl(options = {}) {
2199
2436
  }
2200
2437
 
2201
2438
  prompt();
2439
+ };
2440
+
2441
+ rl.on("line", async (input) => {
2442
+ if (_processingLine) {
2443
+ if (input.trim()) {
2444
+ _pendingLines.push(input);
2445
+ logger.log(
2446
+ chalk.gray(
2447
+ `⏸ queued (${_pendingLines.length}) — runs after the current turn`,
2448
+ ),
2449
+ );
2450
+ }
2451
+ return;
2452
+ }
2453
+ _processingLine = true;
2454
+ try {
2455
+ await handleLine(input);
2456
+ while (_pendingLines.length) {
2457
+ const next = _pendingLines.shift();
2458
+ logger.log(chalk.cyan(`▶ running queued input: ${next}`));
2459
+ await handleLine(next);
2460
+ }
2461
+ } finally {
2462
+ _processingLine = false;
2463
+ }
2202
2464
  });
2203
2465
 
2204
2466
  rl.on("close", async () => {
@@ -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 },