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.
- package/README.md +368 -1
- package/package.json +2 -2
- package/src/assets/web-panel/assets/{AIOps-DCjoAX_u.js → AIOps-Ut7EevnG.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-XHoOmsbP.js → ActionButton-Dv6BlfJg.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics--xaFkDnL.js → Analytics-TQVQuJ7u.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-CSa3FBn8.js → AppLayout-MSqLm2WK.js} +5 -5
- package/src/assets/web-panel/assets/{Audit-ONWXiAwG.js → Audit-mw81HwVy.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-CKOPNdgy.js → Backup-BQcPWDb1.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-PNj4uVqg.js → BaseInput-BYo_pwBH.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CZCulyXV.js → Chat-zi3YUKx2.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CjuJpfpV.js → ChatBubbleRenderer-DWSm1XJJ.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-jvy668lD.js → Checkbox-BvC8Erjt.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-DhUebOQD.js → Codegen-C32vx0OP.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BiBvHfdT.js → Col-DMBwmqyZ.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CmEdEti-.js → Community-nDWncmKV.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CtxpF4R5.js → Compact-lIc1HFn8.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CvPTrTAJ.js → Compliance-D14I_gd2.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BMafGHjy.js → Cowork-BiNI-_ZL.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-mdg_4TR1.js → Cron-N13sFzHb.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain--dGxsUvn.js → Crosschain-Dlnl0-v6.js} +1 -1
- package/src/assets/web-panel/assets/{DID-C9oKaCml.js → DID-CxtYS31I.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CoGxKMvy.js → Dashboard-G4UnHlTR.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CDDu3ZZ3.js → Dropdown-BazlxFGY.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Dy7_r9Ag.js → EmailListRenderer-BrpNdihm.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-CNg6vImJ.js → FamilyGuardDashboard-HD7jbOOR.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CT61bf3u.js → Federation-Bz8lzAGI.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-CSLRnXhg.js → FormItemContext-CcyzGS00.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-CZ4NE5N3.js → GenericCardRenderer-DRo9cwmp.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DBuOma3L.js → Git-B7bn333J.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BTU_SEef.js → Governance-DZX9CWAM.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-47SAmLC_.js → Inference-B3XhsL6W.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DCrK5vP4.js → KnowledgeGraph-CxFRTlQe.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BqiDxdav.js → Logs-xuys6mKH.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-CReUjsDt.js → Marketplace-CXyxv4WU.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-agZBV3p8.js → McpTools-BzZLQVI3.js} +6 -6
- package/src/assets/web-panel/assets/{Memory-C_YvUtyS.js → Memory-BANtaBa7.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-41fP1Tui.js → MobileBridge-BJIwjmxr.js} +3 -3
- package/src/assets/web-panel/assets/{MobileProjects-BkqLvGfL.js → MobileProjects-B857uSAZ.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-JFJCXUnk.js → Mtc-Cn7ceFEz.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-BHNpPZC9.js → MtcAudit-B0zE978G.js} +6 -6
- package/src/assets/web-panel/assets/{Multisig-DuCRumiz.js → Multisig-CQFT0wXW.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-DK-g0fKY.js → NLProgramming-DSxKdVY-.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BSMcjsPf.js → Notes-DtlTfam8.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-9ouC118H.js → NotificationSettings-CHQwayAg.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-Brpmzh9n.js +1 -0
- package/src/assets/web-panel/assets/{Organization-DSV7oRnR.js → Organization-nF_tzZDT.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-DVkkORc3.js → Overflow-CgCSf_PH.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BXXjkkQD.js → P2P-Bvn46bLY.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-O5hNnLTP.js → PdhVaultBrowser-Bzl9k7Gj.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-D_s0H5Av.js → Permissions-Dmezbuo8.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-CzMDrwUi.js → PersonalDataHub-lCKRxwZr.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-i9krLVTL.js → Pipeline-DDCGm9PA.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-cMQcj9I8.js → Privacy-Cgu18Kjl.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-Ca_l7avo.js → ProjectInit-CkF1AeRY.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BkaIhd6b.js → ProjectSettings-D0Q-orz1.js} +2 -2
- package/src/assets/web-panel/assets/Projects-KfGELrSY.js +1 -0
- package/src/assets/web-panel/assets/{Providers-D0nzYiqz.js → Providers-BACLV0z8.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-Bzzr9d0f.js → QuickAsk-CPsZUqDl.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-C-UFbQnX.js → Recommend-5jX0OI1-.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-BKMIKO5F.js → Reputation-5JKv54z0.js} +1 -1
- package/src/assets/web-panel/assets/{Row-Bs7htK1T.js → Row-DLiTF5LY.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-v6MdULUh.js → RssFeed-CFdGmCKW.js} +3 -3
- package/src/assets/web-panel/assets/{Search-DlRWYzvz.js → Search-BjIOnmA7.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DXWO37xX.js → Security-BujPqQSo.js} +4 -4
- package/src/assets/web-panel/assets/{Services-C2tWA-O0.js → Services-ChciPnMu.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-Q8pIYY4a.js → Skeleton-Cwswp1Jv.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-D7XBlErj.js → Skills-CtwR4vJV.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-CiyMVPJ1.js → Sla-pRIevich.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-CadCeeiR.js → SpeechSettings-BRqB28Ai.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-DzNAUhQq.js → SyncSettings-BYyj58_h.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-DTLpT48U.js +1 -0
- package/src/assets/web-panel/assets/{Templates-DfgEpUa4.js → Templates-Bbz_h7oW.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-C8ajkuYi.js → Tenant-D-H4E3cu.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-B9rHwQQx.js → Terminal-CLLi0-lV.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-D1ZVNezX.js → TimelineRenderer-BKI6eG0k.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CAkED4mx.js → Tokens-rsE_yDjM.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-CJSrm6X0.js → Trigger-8TpwuTGk.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-B-TeorSk.js → Trust-sMtZkHPs.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-Di7Ymofy.js → UkeySign-BAy2bAdG.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DM1eYNZe.js → VideoEditing-CBeR_DYK.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DvRWkbmR.js → Wallet-BymDnBcq.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-CeZ3Y622.js → WebAuthn-DQIjmqNz.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-Cq8c4h5j.js → WorkflowEditor-Cj7PB73f.js} +1 -1
- package/src/assets/web-panel/assets/{chat-7-WfML6Q.js → chat-DYnGj4vi.js} +1 -1
- package/src/assets/web-panel/assets/{colors-D6FgCmB-.js → colors-qOLKZNvN.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-ClYV25qi.js → compact-item-BpjCLPcW.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CDhtjdkV.js → createContext-CfakUZVQ.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DgtRXlrj.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DZSH5LQd.js → hasIn-C9RW1s7t.js} +1 -1
- package/src/assets/web-panel/assets/{index-B4PMzmOx.js → index-8Ia91vNV.js} +1 -1
- package/src/assets/web-panel/assets/{index-B78X5S22.js → index-B4kS312z.js} +1 -1
- package/src/assets/web-panel/assets/{index-CDX4QU3k.js → index-BE67I0SW.js} +1 -1
- package/src/assets/web-panel/assets/{index-DPEYvNvq.js → index-BFOSDeeo.js} +1 -1
- package/src/assets/web-panel/assets/{index-CKgS8E_X.js → index-BIz-pX0k.js} +1 -1
- package/src/assets/web-panel/assets/{index-Di9pFrHV.js → index-BJoWi1aR.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHeK8I5A.js → index-B_K0YtG2.js} +1 -1
- package/src/assets/web-panel/assets/{index-BpzOUiSb.js → index-BdR8XRyF.js} +1 -1
- package/src/assets/web-panel/assets/{index-DWRoh3_3.js → index-BfyRXPyV.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7pQa2is.js → index-Bl5LBZJM.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZ4zuoCP.js → index-BlxRICmz.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_mMFQ4S.js → index-BxiHBsfU.js} +1 -1
- package/src/assets/web-panel/assets/{index---azBCXl.js → index-C2S1hUWG.js} +1 -1
- package/src/assets/web-panel/assets/{index-BJ7mrOaB.js → index-CEHyZ77C.js} +1 -1
- package/src/assets/web-panel/assets/{index-CxwfFZ1u.js → index-CJZ2noI2.js} +1 -1
- package/src/assets/web-panel/assets/{index-DGj1orXm.js → index-COYEuArt.js} +1 -1
- package/src/assets/web-panel/assets/{index-DL6GFJAd.js → index-CVZTLSL1.js} +1 -1
- package/src/assets/web-panel/assets/{index-z-R0KaJS.js → index-CbnJ6FsO.js} +1 -1
- package/src/assets/web-panel/assets/{index-tU6pZ1TP.js → index-CvWFTG56.js} +1 -1
- package/src/assets/web-panel/assets/{index-rCs9VJJp.js → index-D-RzTqlR.js} +1 -1
- package/src/assets/web-panel/assets/{index-B6VWGnwq.js → index-DA80prWe.js} +1 -1
- package/src/assets/web-panel/assets/{index-D0YzTJJO.js → index-DAjszh8P.js} +1 -1
- package/src/assets/web-panel/assets/index-DIGTMmnW.js +1 -0
- package/src/assets/web-panel/assets/{index-DjG82V0v.js → index-DQvVYNoJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLizxxId.js → index-DSWdpR3c.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7sC56w8.js → index-DadPmrxI.js} +1 -1
- package/src/assets/web-panel/assets/{index-BlBF_l8m.js → index-DgMJagCq.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bj8hZiyL.js → index-DkmLJFE_.js} +1 -1
- package/src/assets/web-panel/assets/{index-CrTmxbL8.js → index-DzXYG5YJ.js} +1 -1
- package/src/assets/web-panel/assets/index-Ef5jERRW.js +1 -0
- package/src/assets/web-panel/assets/{index-BUOPjAUM.js → index-JkOMWGMX.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmU631Je.js → index-T3bIqK_p.js} +3 -3
- package/src/assets/web-panel/assets/{index-BqOIoEo6.js → index-UiiqS5k2.js} +1 -1
- package/src/assets/web-panel/assets/{index-CSjoWPxB.js → index-VYIJmPvJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-B13QnrnE.js → index-ZCtDWP2C.js} +1 -1
- package/src/assets/web-panel/assets/{index-DgaF1F0W.js → index-f9yoj84i.js} +1 -1
- package/src/assets/web-panel/assets/{index-Or_McYjX.js → index-lPc7EzUi.js} +1 -1
- package/src/assets/web-panel/assets/{index-DGJK8D0l.js → index-m9JeDv6B.js} +1 -1
- package/src/assets/web-panel/assets/{index-CWOkL-8O.js → index-qf0fAus7.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-CSdsIGy3.js → initDefaultProps-DgsgQr1H.js} +1 -1
- package/src/assets/web-panel/assets/{motion-Do-AcZV4.js → motion-TeUH7wzx.js} +1 -1
- package/src/assets/web-panel/assets/{move-BmgOoMsi.js → move-DdkIeWQx.js} +1 -1
- package/src/assets/web-panel/assets/{omit-D4Tm7-s9.js → omit-BH_PH6HT.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-CuWA8-lj.js → pickAttrs-CllCh-Nl.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BSbEF5op.js → placementArrow-BCjE2AzM.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-GIMJwB_9.js → responsiveObserve-BAVGAvRQ.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DlZxpIBe.js → slide-D4ZW-Inn.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BZ26LPlh.js → statusUtils-j4pxhmKV.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-Yn_3FZ0l.js → styleChecker-DH2SLtPg.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-O_LOE1AB.js → useFlexGapSupport-CYMMs-_Q.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-VFMyQqtl.js → useFs-BOX2ddKh.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-B_hyrGB-.js → usePersonalDataHub-BwcnN5z_.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-D4LttGy7.js → vnode-Cwalh7Hj.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-KnTK1fjj.js → zoom-B2_q_nbu.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +38 -4
- package/src/commands/init.js +115 -2
- package/src/commands/mcp.js +57 -0
- package/src/commands/memory.js +62 -0
- package/src/commands/session.js +106 -12
- package/src/index.js +10 -0
- package/src/lib/agent-core.js +1 -0
- package/src/lib/agent-session-export.js +124 -0
- package/src/lib/ide-context.js +62 -0
- package/src/lib/init-ai-refine.js +66 -0
- package/src/lib/json-schema-output.js +181 -0
- package/src/lib/mcp-serve.js +259 -0
- package/src/lib/project-instructions.js +364 -0
- package/src/lib/project-inventory.js +355 -0
- package/src/lib/repl-bang-memorize.js +142 -0
- package/src/lib/repl-completer.js +25 -4
- package/src/lib/repl-rewind.js +107 -0
- package/src/lib/update-notice-refresh.mjs +10 -0
- package/src/lib/update-notice.js +154 -0
- package/src/repl/agent-repl.js +263 -1
- package/src/runtime/agent-core.js +162 -0
- package/src/runtime/system-prompt.js +21 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-LG2nUO5y.js +0 -1
- package/src/assets/web-panel/assets/Projects-Dy9yNmDg.js +0 -1
- package/src/assets/web-panel/assets/Tasks-BjdHjZeb.js +0 -1
- package/src/assets/web-panel/assets/devWarning-O0FVFeZg.js +0 -1
- package/src/assets/web-panel/assets/index--ANIKvhL.js +0 -1
- 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
|
+
}
|
package/src/repl/agent-repl.js
CHANGED
|
@@ -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
|
-
|
|
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 },
|