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.
- package/README.md +368 -1
- package/package.json +2 -2
- package/src/assets/web-panel/assets/{AIOps-DCjoAX_u.js → AIOps-CPmKv82o.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-XHoOmsbP.js → ActionButton-BNDYY7Qd.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics--xaFkDnL.js → Analytics-BgCMCOsk.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-CSa3FBn8.js → AppLayout-Dv4oJcqS.js} +5 -5
- package/src/assets/web-panel/assets/{Audit-ONWXiAwG.js → Audit-5iV3yrGa.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-CKOPNdgy.js → Backup-CHDhnbzF.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-PNj4uVqg.js → BaseInput-B6reFkra.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CZCulyXV.js → Chat-DwS5YyE2.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CjuJpfpV.js → ChatBubbleRenderer-CqXa87Hw.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-jvy668lD.js → Checkbox-yiW0M4RE.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-DhUebOQD.js → Codegen-DoiVuD_g.js} +1 -1
- package/src/assets/web-panel/assets/{Col-BiBvHfdT.js → Col-BVASLexk.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CmEdEti-.js → Community-D6KQ7JoU.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CtxpF4R5.js → Compact-Bl9Uhb6v.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CvPTrTAJ.js → Compliance-MM31-dba.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BMafGHjy.js → Cowork-PjU_1ieD.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-mdg_4TR1.js → Cron-DorNtPZL.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain--dGxsUvn.js → Crosschain-Bm5ts2Kw.js} +1 -1
- package/src/assets/web-panel/assets/{DID-C9oKaCml.js → DID-7Y3jlFdY.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-CoGxKMvy.js → Dashboard-1oE532bG.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CDDu3ZZ3.js → Dropdown-hJlOPs0s.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Dy7_r9Ag.js → EmailListRenderer-BEqJxKaO.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-CNg6vImJ.js → FamilyGuardDashboard-BvCGwB6X.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CT61bf3u.js → Federation-CsXI72e5.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-CSLRnXhg.js → FormItemContext-Dh9SMul-.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-CZ4NE5N3.js → GenericCardRenderer-9edWzrtG.js} +1 -1
- package/src/assets/web-panel/assets/{Git-DBuOma3L.js → Git-ZYhNL8Xk.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BTU_SEef.js → Governance-BwAdp8QA.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-47SAmLC_.js → Inference-5C-M1XsH.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-DCrK5vP4.js → KnowledgeGraph-zFAi-zCi.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BqiDxdav.js → Logs-BZsEdbgE.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-CReUjsDt.js → Marketplace-BP6gErRK.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-agZBV3p8.js → McpTools-CXVzoLrd.js} +6 -6
- package/src/assets/web-panel/assets/{Memory-C_YvUtyS.js → Memory-BIpChb4-.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-41fP1Tui.js → MobileBridge-B4O7wDT8.js} +3 -3
- package/src/assets/web-panel/assets/MobileProjects-7VPMoHus.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-JFJCXUnk.js → Mtc-BTmEyTM5.js} +6 -6
- package/src/assets/web-panel/assets/{MtcAudit-BHNpPZC9.js → MtcAudit-CsbG9LlV.js} +6 -6
- package/src/assets/web-panel/assets/{Multisig-DuCRumiz.js → Multisig-CL8yoGon.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-DK-g0fKY.js → NLProgramming-C2cIlIp_.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BSMcjsPf.js → Notes-7aBk_n_M.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-9ouC118H.js → NotificationSettings-BuhQk4rJ.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-LG2nUO5y.js → OrderTableRenderer-mqMFZu0x.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-DSV7oRnR.js → Organization-CAdq-170.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-DVkkORc3.js → Overflow--Xn0E787.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-BXXjkkQD.js → P2P-DYt3YAXI.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-O5hNnLTP.js → PdhVaultBrowser-Bgb_v8WN.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-D_s0H5Av.js → Permissions-DoFlmoaW.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-CzMDrwUi.js → PersonalDataHub-C-FJB3a0.js} +3 -3
- package/src/assets/web-panel/assets/{Pipeline-i9krLVTL.js → Pipeline-3bL2RzzL.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-cMQcj9I8.js → Privacy-c4igYUCF.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-Ca_l7avo.js → ProjectInit-C0QS1UPR.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-BkaIhd6b.js → ProjectSettings-CkYC0xkE.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-Dy9yNmDg.js → Projects-Di17SYft.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-D0nzYiqz.js → Providers-41NySsLt.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-Bzzr9d0f.js → QuickAsk-DHq9pD7z.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-C-UFbQnX.js → Recommend-CLjgFPLv.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-BKMIKO5F.js → Reputation-EIrgErm3.js} +1 -1
- package/src/assets/web-panel/assets/{Row-Bs7htK1T.js → Row-GAvKzKH7.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-v6MdULUh.js → RssFeed-CYCNsVmD.js} +2 -2
- package/src/assets/web-panel/assets/{Search-DlRWYzvz.js → Search-DWOE32k8.js} +1 -1
- package/src/assets/web-panel/assets/{Security-DXWO37xX.js → Security-Dgh8Jevn.js} +4 -4
- package/src/assets/web-panel/assets/{Services-C2tWA-O0.js → Services-BxdgP67N.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-Q8pIYY4a.js → Skeleton-D-xT4ZkA.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-D7XBlErj.js → Skills-BKN4lfSa.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-CiyMVPJ1.js → Sla--N1TudpS.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-CadCeeiR.js → SpeechSettings-B0vfJpEh.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-DzNAUhQq.js → SyncSettings-BuBAbPAh.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-BjdHjZeb.js → Tasks-4XugjJ87.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-DfgEpUa4.js → Templates-DI2giLgc.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-C8ajkuYi.js → Tenant-BiTWvm0g.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-B9rHwQQx.js → Terminal-vV6AWGDi.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-D1ZVNezX.js → TimelineRenderer-BmgzKdAp.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CAkED4mx.js → Tokens-Nvupdm6p.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-CJSrm6X0.js → Trigger-DRfR77WJ.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-B-TeorSk.js → Trust-De0Jal_6.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-Di7Ymofy.js → UkeySign-Dzo4-VAM.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DM1eYNZe.js → VideoEditing-hg2ytiJB.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DvRWkbmR.js → Wallet--bU5-gRh.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-CeZ3Y622.js → WebAuthn-DZptt-PV.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-Cq8c4h5j.js → WorkflowEditor-Dy9223bY.js} +1 -1
- package/src/assets/web-panel/assets/{chat-7-WfML6Q.js → chat-DaxGeI9w.js} +1 -1
- package/src/assets/web-panel/assets/{colors-D6FgCmB-.js → colors-Cu2VEci3.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-ClYV25qi.js → compact-item-CGolhyJq.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CDhtjdkV.js → createContext-DY7EFhkD.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DV2BNd59.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-DZSH5LQd.js → hasIn-Bpc-NoFN.js} +1 -1
- package/src/assets/web-panel/assets/{index-CKgS8E_X.js → index-1D4sfByw.js} +1 -1
- package/src/assets/web-panel/assets/{index-Or_McYjX.js → index-8h9y5S6X.js} +1 -1
- package/src/assets/web-panel/assets/{index-BlBF_l8m.js → index-BP9P6chP.js} +1 -1
- package/src/assets/web-panel/assets/{index-CSjoWPxB.js → index-BQ2z6Ky5.js} +1 -1
- package/src/assets/web-panel/assets/{index-DjG82V0v.js → index-BRAgl2J_.js} +1 -1
- package/src/assets/web-panel/assets/{index-B_mMFQ4S.js → index-BTvwiqJE.js} +1 -1
- package/src/assets/web-panel/assets/index-BZqtTmyG.js +1 -0
- package/src/assets/web-panel/assets/{index-D0YzTJJO.js → index-BjfxHEmX.js} +1 -1
- package/src/assets/web-panel/assets/{index-DgaF1F0W.js → index-BlHq81Ow.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bj8hZiyL.js → index-Bn5gM9Oy.js} +1 -1
- package/src/assets/web-panel/assets/{index-DPEYvNvq.js → index-Bz83ngs0.js} +1 -1
- package/src/assets/web-panel/assets/{index-DL6GFJAd.js → index-C-Hkl_2G.js} +1 -1
- package/src/assets/web-panel/assets/{index-BJ7mrOaB.js → index-C0_zeYnx.js} +1 -1
- package/src/assets/web-panel/assets/{index-CrTmxbL8.js → index-C2RpsAiO.js} +1 -1
- package/src/assets/web-panel/assets/{index-CDX4QU3k.js → index-CBSk_VrT.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZ4zuoCP.js → index-CFAnEzRW.js} +1 -1
- package/src/assets/web-panel/assets/{index-B6VWGnwq.js → index-CGqeHu_F.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7pQa2is.js → index-CJFYF8F9.js} +1 -1
- package/src/assets/web-panel/assets/{index-CWOkL-8O.js → index-CLNqZF55.js} +1 -1
- package/src/assets/web-panel/assets/{index-tU6pZ1TP.js → index-CaKXhpEu.js} +1 -1
- package/src/assets/web-panel/assets/{index---azBCXl.js → index-Ciw5-X1B.js} +1 -1
- package/src/assets/web-panel/assets/{index-DLizxxId.js → index-D0GN5tdM.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHeK8I5A.js → index-D63ObMdQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-z-R0KaJS.js → index-DAov-rJR.js} +1 -1
- package/src/assets/web-panel/assets/{index-BUOPjAUM.js → index-DElatOQ0.js} +1 -1
- package/src/assets/web-panel/assets/{index-Di9pFrHV.js → index-DNX81oSR.js} +1 -1
- package/src/assets/web-panel/assets/index-DUpwdJt9.js +1 -0
- package/src/assets/web-panel/assets/{index-B13QnrnE.js → index-DZ4Vm8dQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7sC56w8.js → index-DexYD87j.js} +1 -1
- package/src/assets/web-panel/assets/{index-CxwfFZ1u.js → index-DfKmAEtE.js} +1 -1
- package/src/assets/web-panel/assets/{index-B78X5S22.js → index-DldaToUA.js} +1 -1
- package/src/assets/web-panel/assets/{index-BpzOUiSb.js → index-DpRSzAFl.js} +1 -1
- package/src/assets/web-panel/assets/{index-B4PMzmOx.js → index-DxXkr-NS.js} +1 -1
- package/src/assets/web-panel/assets/{index-BqOIoEo6.js → index-RumxOD0S.js} +1 -1
- package/src/assets/web-panel/assets/{index-DGJK8D0l.js → index-VBRPxZeE.js} +1 -1
- package/src/assets/web-panel/assets/{index-DWRoh3_3.js → index-eF9RV_4c.js} +1 -1
- package/src/assets/web-panel/assets/{index-DGj1orXm.js → index-lfP8sdzB.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmU631Je.js → index-oJQgRCrR.js} +3 -3
- package/src/assets/web-panel/assets/{index-rCs9VJJp.js → index-rkm7dHwG.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-CSdsIGy3.js → initDefaultProps-CkJZfCo8.js} +1 -1
- package/src/assets/web-panel/assets/{motion-Do-AcZV4.js → motion-BerbusV1.js} +1 -1
- package/src/assets/web-panel/assets/{move-BmgOoMsi.js → move-DyRzKPD4.js} +1 -1
- package/src/assets/web-panel/assets/{omit-D4Tm7-s9.js → omit-CCdrTUAs.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-CuWA8-lj.js → pickAttrs-mVDeZx2m.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BSbEF5op.js → placementArrow-Bb_-Fs_o.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-GIMJwB_9.js → responsiveObserve-C6TMj1R_.js} +1 -1
- package/src/assets/web-panel/assets/{slide-DlZxpIBe.js → slide-CdCNsy1J.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BZ26LPlh.js → statusUtils-Ccxd1rFd.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-Yn_3FZ0l.js → styleChecker-3IL-yw1V.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-O_LOE1AB.js → useFlexGapSupport-CH8DjUHl.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-VFMyQqtl.js → useFs-Cn9nE2sp.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-B_hyrGB-.js → usePersonalDataHub-BPyT0HO7.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-D4LttGy7.js → vnode-Mfm7vy07.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-KnTK1fjj.js → zoom-CTpAiAE9.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/init.js +84 -2
- package/src/commands/session.js +36 -12
- package/src/index.js +10 -0
- package/src/lib/agent-session-export.js +124 -0
- package/src/lib/ide-context.js +62 -0
- package/src/lib/project-instructions.js +275 -0
- package/src/lib/project-inventory.js +355 -0
- package/src/lib/repl-bang-memorize.js +142 -0
- package/src/lib/repl-completer.js +17 -2
- package/src/lib/update-notice-refresh.mjs +10 -0
- package/src/lib/update-notice.js +154 -0
- package/src/repl/agent-repl.js +118 -0
- package/src/runtime/agent-core.js +162 -0
- package/src/runtime/system-prompt.js +21 -1
- package/src/assets/web-panel/assets/MobileProjects-BkqLvGfL.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,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
|
|
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[]>,
|
|
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
|
+
}
|
package/src/repl/agent-repl.js
CHANGED
|
@@ -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 },
|