chainlesschain 0.162.38 → 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-DV0Q9zKL.js → AIOps-CPmKv82o.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-C6vH8rhL.js → ActionButton-BNDYY7Qd.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-BvPDc2ui.js → Analytics-BgCMCOsk.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-CWnyqTqY.js → AppLayout-Dv4oJcqS.js} +5 -5
- package/src/assets/web-panel/assets/{Audit-BzenidV4.js → Audit-5iV3yrGa.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-CSl7bNwK.js → Backup-CHDhnbzF.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DAY3iHIq.js → BaseInput-B6reFkra.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-Jyhm9fgk.js → Chat-DwS5YyE2.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-CwlAnVjy.js → ChatBubbleRenderer-CqXa87Hw.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-D4rwURAi.js → Checkbox-yiW0M4RE.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-DYdjTEfC.js → Codegen-DoiVuD_g.js} +1 -1
- package/src/assets/web-panel/assets/{Col-DsVyZ_fS.js → Col-BVASLexk.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CjCpl27Q.js → Community-D6KQ7JoU.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-kt18dsjm.js → Compact-Bl9Uhb6v.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-BV5urquU.js → Compliance-MM31-dba.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-C4SovPWC.js → Cowork-PjU_1ieD.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-uuNs_xzA.js → Cron-DorNtPZL.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DR5a65tR.js → Crosschain-Bm5ts2Kw.js} +1 -1
- package/src/assets/web-panel/assets/{DID-B1KTf2-5.js → DID-7Y3jlFdY.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-Dkj7XgED.js → Dashboard-1oE532bG.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BhXCuJ19.js → Dropdown-hJlOPs0s.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-DG8365Iv.js → EmailListRenderer-BEqJxKaO.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-BdHGPu39.js → FamilyGuardDashboard-BvCGwB6X.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-Dwvxl0zR.js → Federation-CsXI72e5.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-BVmhCVWU.js → FormItemContext-Dh9SMul-.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DDPjvF2s.js → GenericCardRenderer-9edWzrtG.js} +1 -1
- package/src/assets/web-panel/assets/{Git-foK6WTSr.js → Git-ZYhNL8Xk.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CfqMdu6Y.js → Governance-BwAdp8QA.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-BKrLO4GO.js → Inference-5C-M1XsH.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-6o6Q-mmF.js → KnowledgeGraph-zFAi-zCi.js} +1 -1
- package/src/assets/web-panel/assets/Logs-BZsEdbgE.js +2 -0
- package/src/assets/web-panel/assets/{Marketplace-BWkfEocP.js → Marketplace-BP6gErRK.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-BPebQbWU.js → McpTools-CXVzoLrd.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-C0Dq-X3C.js → Memory-BIpChb4-.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-DRBoutTY.js → MobileBridge-B4O7wDT8.js} +2 -2
- package/src/assets/web-panel/assets/MobileProjects-7VPMoHus.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-Cj3QPM9p.js → Mtc-BTmEyTM5.js} +6 -6
- package/src/assets/web-panel/assets/{MtcAudit-rBQYbfQR.js → MtcAudit-CsbG9LlV.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-Dbuy4OY4.js → Multisig-CL8yoGon.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CMnt1se-.js → NLProgramming-C2cIlIp_.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-BX9tSCiF.js → Notes-7aBk_n_M.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-BFeirVRq.js → NotificationSettings-BuhQk4rJ.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-ybiMlKQW.js → OrderTableRenderer-mqMFZu0x.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-kTfRxKqk.js → Organization-CAdq-170.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-CtuCAzwV.js → Overflow--Xn0E787.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-KfbciaP3.js → P2P-DYt3YAXI.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-bqEUFhgC.js → PdhVaultBrowser-Bgb_v8WN.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-BgMypz-z.js → Permissions-DoFlmoaW.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-C3zUE-1z.js → PersonalDataHub-C-FJB3a0.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-iX-pYHpC.js → Pipeline-3bL2RzzL.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-B01uzeFM.js → Privacy-c4igYUCF.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-TsfbzJp7.js → ProjectInit-C0QS1UPR.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-iGvMp8sM.js → ProjectSettings-CkYC0xkE.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-Be9k29iQ.js → Projects-Di17SYft.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-C9Pc8dqo.js → Providers-41NySsLt.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-DN_yFiVO.js → QuickAsk-DHq9pD7z.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CvSNgl7H.js → Recommend-CLjgFPLv.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-S6BCz8xH.js → Reputation-EIrgErm3.js} +1 -1
- package/src/assets/web-panel/assets/{Row-CTRYCaqP.js → Row-GAvKzKH7.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-Cu8_P5ll.js → RssFeed-CYCNsVmD.js} +3 -3
- package/src/assets/web-panel/assets/{Search-rZ1Xza_U.js → Search-DWOE32k8.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CF43IJHX.js → Security-Dgh8Jevn.js} +4 -4
- package/src/assets/web-panel/assets/{Services-BobNHzne.js → Services-BxdgP67N.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-DWJ2kfuI.js → Skeleton-D-xT4ZkA.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-AmEZgHYr.js → Skills-BKN4lfSa.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DTS-fBiY.js → Sla--N1TudpS.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-DEr6MHRU.js → SpeechSettings-B0vfJpEh.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CVs9alv_.js → SyncSettings-BuBAbPAh.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-4XugjJ87.js +1 -0
- package/src/assets/web-panel/assets/{Templates-CTNjZRKA.js → Templates-DI2giLgc.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-DPbXg0Pg.js → Tenant-BiTWvm0g.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-DhKXcPw2.js → Terminal-vV6AWGDi.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-B0DMZOpk.js → TimelineRenderer-BmgzKdAp.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-RvWuBXgg.js → Tokens-Nvupdm6p.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-2O-BaTQG.js → Trigger-DRfR77WJ.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-6qY35L-C.js → Trust-De0Jal_6.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-DhV1wYtQ.js → UkeySign-Dzo4-VAM.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DgqA5UZm.js → VideoEditing-hg2ytiJB.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DJRYdUAK.js → Wallet--bU5-gRh.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-C2W-x0cg.js → WebAuthn-DZptt-PV.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-BP2tkDHe.js → WorkflowEditor-Dy9223bY.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CGVfeoTn.js → chat-DaxGeI9w.js} +1 -1
- package/src/assets/web-panel/assets/{colors-BmjRolM1.js → colors-Cu2VEci3.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BvJJkjZE.js → compact-item-CGolhyJq.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-DyhlvRYs.js → createContext-DY7EFhkD.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DV2BNd59.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BoBMR89s.js → hasIn-Bpc-NoFN.js} +1 -1
- package/src/assets/web-panel/assets/{index-CSgbOGaP.js → index-1D4sfByw.js} +1 -1
- package/src/assets/web-panel/assets/{index-C1t-r7yV.js → index-8h9y5S6X.js} +1 -1
- package/src/assets/web-panel/assets/{index-FKFT-QTk.js → index-BP9P6chP.js} +1 -1
- package/src/assets/web-panel/assets/{index-CIaGw7vl.js → index-BQ2z6Ky5.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQJVedQ3.js → index-BRAgl2J_.js} +1 -1
- package/src/assets/web-panel/assets/{index-BycpeGfj.js → index-BTvwiqJE.js} +1 -1
- package/src/assets/web-panel/assets/index-BZqtTmyG.js +1 -0
- package/src/assets/web-panel/assets/{index-D0YToIi_.js → index-BjfxHEmX.js} +1 -1
- package/src/assets/web-panel/assets/{index-BT1SQ9nj.js → index-BlHq81Ow.js} +1 -1
- package/src/assets/web-panel/assets/{index-DyS4I4L-.js → index-Bn5gM9Oy.js} +1 -1
- package/src/assets/web-panel/assets/{index-81tWFqfN.js → index-Bz83ngs0.js} +1 -1
- package/src/assets/web-panel/assets/{index-xZdOioVg.js → index-C-Hkl_2G.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cbh-lCxq.js → index-C0_zeYnx.js} +1 -1
- package/src/assets/web-panel/assets/{index-xPSzUoWT.js → index-C2RpsAiO.js} +1 -1
- package/src/assets/web-panel/assets/{index-n-N19np-.js → index-CBSk_VrT.js} +1 -1
- package/src/assets/web-panel/assets/{index-VXVukhBA.js → index-CFAnEzRW.js} +1 -1
- package/src/assets/web-panel/assets/{index-DtKdCXHW.js → index-CGqeHu_F.js} +1 -1
- package/src/assets/web-panel/assets/{index-Beh7jDbS.js → index-CJFYF8F9.js} +1 -1
- package/src/assets/web-panel/assets/{index-BvvNnWXe.js → index-CLNqZF55.js} +1 -1
- package/src/assets/web-panel/assets/{index-DeeLHcMY.js → index-CaKXhpEu.js} +1 -1
- package/src/assets/web-panel/assets/{index-BuQrONgf.js → index-Ciw5-X1B.js} +1 -1
- package/src/assets/web-panel/assets/{index-CDPMHKQi.js → index-D0GN5tdM.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bm_MmdwP.js → index-D63ObMdQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DIPZ6hbJ.js → index-DAov-rJR.js} +1 -1
- package/src/assets/web-panel/assets/{index-39VDXdn6.js → index-DElatOQ0.js} +1 -1
- package/src/assets/web-panel/assets/{index-DwTgvhOL.js → index-DNX81oSR.js} +1 -1
- package/src/assets/web-panel/assets/index-DUpwdJt9.js +1 -0
- package/src/assets/web-panel/assets/{index-DgbWSwr5.js → index-DZ4Vm8dQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-D-93XwJd.js → index-DexYD87j.js} +1 -1
- package/src/assets/web-panel/assets/{index-C0xn6hOr.js → index-DfKmAEtE.js} +1 -1
- package/src/assets/web-panel/assets/{index-ZNIms1nA.js → index-DldaToUA.js} +1 -1
- package/src/assets/web-panel/assets/{index-wLAjVpmJ.js → index-DpRSzAFl.js} +1 -1
- package/src/assets/web-panel/assets/{index-Te0ruvY_.js → index-DxXkr-NS.js} +1 -1
- package/src/assets/web-panel/assets/{index-Czsbrn75.js → index-RumxOD0S.js} +1 -1
- package/src/assets/web-panel/assets/{index-vF1pR00A.js → index-VBRPxZeE.js} +1 -1
- package/src/assets/web-panel/assets/{index-BqGNmoKy.js → index-eF9RV_4c.js} +1 -1
- package/src/assets/web-panel/assets/{index-CzDVBBcg.js → index-lfP8sdzB.js} +1 -1
- package/src/assets/web-panel/assets/{index-Y1b8i0NV.js → index-oJQgRCrR.js} +3 -3
- package/src/assets/web-panel/assets/{index-ByWpNjTj.js → index-rkm7dHwG.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BLKSE8he.js → initDefaultProps-CkJZfCo8.js} +1 -1
- package/src/assets/web-panel/assets/{motion-Bb59qqLK.js → motion-BerbusV1.js} +1 -1
- package/src/assets/web-panel/assets/{move-CB3pYCk6.js → move-DyRzKPD4.js} +1 -1
- package/src/assets/web-panel/assets/{omit-iImQWuU7.js → omit-CCdrTUAs.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DRP2Chqo.js → pickAttrs-mVDeZx2m.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BrlfD4tF.js → placementArrow-Bb_-Fs_o.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-Cqxkuh5H.js → responsiveObserve-C6TMj1R_.js} +1 -1
- package/src/assets/web-panel/assets/{slide-nxKEuLMj.js → slide-CdCNsy1J.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-30E47KSk.js → statusUtils-Ccxd1rFd.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-Dn2_-5bn.js → styleChecker-3IL-yw1V.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DkZ00X6F.js → useFlexGapSupport-CH8DjUHl.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-ByrwSCOr.js → useFs-Cn9nE2sp.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-BDY6jtUD.js → usePersonalDataHub-BPyT0HO7.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-BL2q5BLv.js → vnode-Mfm7vy07.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-BSkPKE42.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 +333 -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 +154 -0
- package/src/lib/update-notice-refresh.mjs +10 -0
- package/src/lib/update-notice.js +154 -0
- package/src/repl/agent-repl.js +154 -0
- package/src/runtime/agent-core.js +195 -0
- package/src/runtime/headless-runner.js +19 -0
- package/src/runtime/headless-stream.js +19 -9
- package/src/runtime/system-prompt.js +21 -1
- package/src/assets/web-panel/assets/Logs-L5ZIW0Dz.js +0 -2
- package/src/assets/web-panel/assets/MobileProjects-BMP6eLp1.js +0 -1
- package/src/assets/web-panel/assets/Tasks-BcVDAxdi.js +0 -1
- package/src/assets/web-panel/assets/devWarning-CetO0WH0.js +0 -1
- package/src/assets/web-panel/assets/index-BZVz-WfV.js +0 -1
- package/src/assets/web-panel/assets/index-D0-bvFy3.js +0 -1
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IDE live prompt context (Claude-Code parity) — when an IDE bridge is
|
|
3
|
+
* connected (lib/ide-bridge.js → mcp-config.js `loadIdeMcp`, server `ide`),
|
|
4
|
+
* automatically share the editor's state at the moment a prompt is submitted:
|
|
5
|
+
* the active file, the open editor tabs, and the current selection. The agent
|
|
6
|
+
* no longer has to *choose* to call mcp__ide__getSelection — the context rides
|
|
7
|
+
* along with every user turn, exactly like Claude Code's at-submit selection
|
|
8
|
+
* sharing.
|
|
9
|
+
*
|
|
10
|
+
* The context is EPHEMERAL by design: entry points append it to the in-flight
|
|
11
|
+
* user content only, after session persistence, so a resumed session replays
|
|
12
|
+
* the user's words, not a stale editor snapshot.
|
|
13
|
+
*
|
|
14
|
+
* Everything here is best-effort and bounded: a missing/slow IDE server can
|
|
15
|
+
* never block or fail a turn (short timeout, all errors → null), and the
|
|
16
|
+
* injected block is capped. `CC_IDE_CONTEXT=0` disables the feature without
|
|
17
|
+
* disconnecting the IDE tools themselves.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Hard cap on the selected text we inline into the prompt. */
|
|
21
|
+
const SELECTION_TEXT_CAP = 2000;
|
|
22
|
+
/** At most this many open-editor entries are listed. */
|
|
23
|
+
const OPEN_EDITORS_CAP = 10;
|
|
24
|
+
/** Per-tool-call budget; the IDE answers from memory, so this is generous. */
|
|
25
|
+
const DEFAULT_TIMEOUT_MS = 1500;
|
|
26
|
+
|
|
27
|
+
/** Env kill-switch: CC_IDE_CONTEXT=0|false|off disables injection. */
|
|
28
|
+
export function ideContextEnabled(env = process.env) {
|
|
29
|
+
const v = String(env?.CC_IDE_CONTEXT ?? "").toLowerCase();
|
|
30
|
+
return !(v === "0" || v === "false" || v === "off");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Does this resolved MCP bundle expose the IDE bridge's selection tool?
|
|
35
|
+
* (`resolveAgentMcp` connects the bridge as server `ide`, so its tools land in
|
|
36
|
+
* `externalToolExecutors` as mcp__ide__*.)
|
|
37
|
+
*/
|
|
38
|
+
export function hasIdeContextTools(mcp) {
|
|
39
|
+
return !!(
|
|
40
|
+
mcp?.mcpClient?.callTool &&
|
|
41
|
+
mcp.externalToolExecutors?.mcp__ide__getSelection?.kind === "mcp"
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Read an MCP tools/call result's first text block as JSON. The IDE bridge
|
|
47
|
+
* servers always wrap handler data as
|
|
48
|
+
* `{content:[{type:"text",text:JSON.stringify(data)}]}`. Returns null for
|
|
49
|
+
* isError results, non-text content, or unparsable text.
|
|
50
|
+
*/
|
|
51
|
+
export function parseToolResultJson(result) {
|
|
52
|
+
if (!result || result.isError) return null;
|
|
53
|
+
const block = Array.isArray(result.content)
|
|
54
|
+
? result.content.find((b) => b && b.type === "text")
|
|
55
|
+
: null;
|
|
56
|
+
if (!block || typeof block.text !== "string") return null;
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(block.text);
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Resolve to the promise's value, or null after `ms` / on rejection. */
|
|
65
|
+
function withTimeout(promise, ms) {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const timer = setTimeout(() => resolve(null), ms);
|
|
68
|
+
promise.then(
|
|
69
|
+
(v) => {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
resolve(v);
|
|
72
|
+
},
|
|
73
|
+
() => {
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
resolve(null);
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Query the connected IDE for its live state. Returns
|
|
83
|
+
* `{ selection, openEditors }` (either field may be null) or null when the
|
|
84
|
+
* feature is disabled, no IDE tools are connected, or nothing useful came
|
|
85
|
+
* back. Never throws.
|
|
86
|
+
*
|
|
87
|
+
* @param {object} mcp resolved bundle from resolveAgentMcp
|
|
88
|
+
* @param {object} opts { env?, timeoutMs? }
|
|
89
|
+
*/
|
|
90
|
+
export async function collectIdeContext(mcp, opts = {}) {
|
|
91
|
+
if (!ideContextEnabled(opts.env || process.env)) return null;
|
|
92
|
+
if (!hasIdeContextTools(mcp)) return null;
|
|
93
|
+
const timeoutMs = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
94
|
+
const executors = mcp.externalToolExecutors;
|
|
95
|
+
const call = (name) => {
|
|
96
|
+
const exec = executors[name];
|
|
97
|
+
if (!exec || exec.kind !== "mcp") return Promise.resolve(null);
|
|
98
|
+
let p;
|
|
99
|
+
try {
|
|
100
|
+
p = mcp.mcpClient.callTool(exec.serverName, exec.toolName, {});
|
|
101
|
+
} catch {
|
|
102
|
+
return Promise.resolve(null);
|
|
103
|
+
}
|
|
104
|
+
return withTimeout(p.then(parseToolResultJson), timeoutMs);
|
|
105
|
+
};
|
|
106
|
+
const [selection, editors] = await Promise.all([
|
|
107
|
+
call("mcp__ide__getSelection"),
|
|
108
|
+
call("mcp__ide__getOpenEditors"),
|
|
109
|
+
]);
|
|
110
|
+
const openEditors = Array.isArray(editors?.editors) ? editors.editors : null;
|
|
111
|
+
if (!selection && !(openEditors && openEditors.length > 0)) return null;
|
|
112
|
+
return { selection: selection || null, openEditors };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Render collected IDE state as a compact tagged block for the user turn.
|
|
117
|
+
* Returns null when there is nothing worth saying.
|
|
118
|
+
*/
|
|
119
|
+
export function formatIdeContext(ctx) {
|
|
120
|
+
if (!ctx) return null;
|
|
121
|
+
const lines = [];
|
|
122
|
+
const editors = Array.isArray(ctx.openEditors) ? ctx.openEditors : [];
|
|
123
|
+
const active = editors.find((e) => e && e.active);
|
|
124
|
+
if (active?.file) lines.push(`Active file: ${active.file}`);
|
|
125
|
+
if (editors.length > 0) {
|
|
126
|
+
const names = editors
|
|
127
|
+
.filter((e) => e && e.file)
|
|
128
|
+
.slice(0, OPEN_EDITORS_CAP)
|
|
129
|
+
.map((e) => (e.active ? `${e.file} (active)` : e.file));
|
|
130
|
+
const more = editors.length - names.length;
|
|
131
|
+
lines.push(
|
|
132
|
+
`Open editors: ${names.join(", ")}${more > 0 ? ` (+${more} more)` : ""}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
const sel = ctx.selection;
|
|
136
|
+
if (sel && typeof sel.text === "string" && sel.text.length > 0) {
|
|
137
|
+
const start = sel.selection?.start?.line;
|
|
138
|
+
const end = sel.selection?.end?.line;
|
|
139
|
+
const range =
|
|
140
|
+
Number.isInteger(start) && Number.isInteger(end)
|
|
141
|
+
? `:${start + 1}-${end + 1}` // editor lines are 0-based
|
|
142
|
+
: "";
|
|
143
|
+
const text =
|
|
144
|
+
sel.text.length > SELECTION_TEXT_CAP
|
|
145
|
+
? sel.text.slice(0, SELECTION_TEXT_CAP) + "\n...(selection truncated)"
|
|
146
|
+
: sel.text;
|
|
147
|
+
lines.push(`Selected text in ${sel.file || "the active editor"}${range}:`);
|
|
148
|
+
lines.push(text);
|
|
149
|
+
} else if (sel?.file && !active) {
|
|
150
|
+
lines.push(`Active file: ${sel.file}`);
|
|
151
|
+
}
|
|
152
|
+
if (lines.length === 0) return null;
|
|
153
|
+
return (
|
|
154
|
+
"<ide-context>\n" +
|
|
155
|
+
"Live editor state, shared automatically at prompt time (an IDE is " +
|
|
156
|
+
"connected). This reflects what the user is looking at NOW:\n" +
|
|
157
|
+
lines.join("\n") +
|
|
158
|
+
"\n</ide-context>"
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Append extra text to user-turn content, preserving multimodal arrays
|
|
164
|
+
* (OpenAI-style content parts from --image runs).
|
|
165
|
+
*/
|
|
166
|
+
export function appendTextToContent(content, extra) {
|
|
167
|
+
if (!extra) return content;
|
|
168
|
+
if (typeof content === "string") {
|
|
169
|
+
return content.length > 0 ? `${content}\n\n${extra}` : extra;
|
|
170
|
+
}
|
|
171
|
+
if (Array.isArray(content)) {
|
|
172
|
+
return [...content, { type: "text", text: extra }];
|
|
173
|
+
}
|
|
174
|
+
return content;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* One-call convenience for entry points: collect + format. Returns the tagged
|
|
179
|
+
* block string or null. Never throws.
|
|
180
|
+
*/
|
|
181
|
+
export async function buildIdePromptContext(mcp, opts = {}) {
|
|
182
|
+
try {
|
|
183
|
+
const ctx = await collectIdeContext(mcp, opts);
|
|
184
|
+
return ctx ? formatIdeContext(ctx) : null;
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ─── Post-edit diagnostics feedback (Claude-Code parity) ────────────────────
|
|
191
|
+
//
|
|
192
|
+
// After the agent mutates a file, the connected IDE's language servers see the
|
|
193
|
+
// change and update their diagnostics. Pulling them right back into the tool
|
|
194
|
+
// result lets the model fix what it just broke in the SAME loop instead of
|
|
195
|
+
// discovering it turns later. The IDE needs a beat to re-lint, hence the
|
|
196
|
+
// settle delay (CC_IDE_DIAG_SETTLE_MS overrides; 0 skips the wait).
|
|
197
|
+
|
|
198
|
+
/** Give language servers this long to notice the disk change before asking. */
|
|
199
|
+
const DIAG_SETTLE_MS = 600;
|
|
200
|
+
/** At most this many diagnostics are surfaced per edit. */
|
|
201
|
+
const DIAG_CAP = 10;
|
|
202
|
+
/** Only these severities are worth interrupting the model for. */
|
|
203
|
+
const DIAG_SEVERITIES = new Set(["error", "warning"]);
|
|
204
|
+
|
|
205
|
+
/** Does this MCP surface expose the IDE bridge's diagnostics tool? */
|
|
206
|
+
export function hasIdeDiagnosticsTool(mcp) {
|
|
207
|
+
return !!(
|
|
208
|
+
mcp?.mcpClient?.callTool &&
|
|
209
|
+
mcp.externalToolExecutors?.mcp__ide__getDiagnostics?.kind === "mcp"
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Pull the IDE's current error/warning diagnostics for one file. `mcp` accepts
|
|
215
|
+
* either a resolveAgentMcp bundle or agent-core's tool context (both carry
|
|
216
|
+
* `mcpClient` + `externalToolExecutors`). Returns a non-empty array or null.
|
|
217
|
+
* Never throws.
|
|
218
|
+
*
|
|
219
|
+
* @param {object} mcp { mcpClient, externalToolExecutors }
|
|
220
|
+
* @param {string} filePath absolute path of the just-edited file
|
|
221
|
+
* @param {object} opts { env?, settleMs?, timeoutMs? }
|
|
222
|
+
*/
|
|
223
|
+
export async function collectIdeDiagnostics(mcp, filePath, opts = {}) {
|
|
224
|
+
const env = opts.env || process.env;
|
|
225
|
+
if (!ideContextEnabled(env)) return null;
|
|
226
|
+
if (!filePath || !hasIdeDiagnosticsTool(mcp)) return null;
|
|
227
|
+
const settle =
|
|
228
|
+
opts.settleMs ??
|
|
229
|
+
(Number.isFinite(Number(env.CC_IDE_DIAG_SETTLE_MS))
|
|
230
|
+
? Number(env.CC_IDE_DIAG_SETTLE_MS)
|
|
231
|
+
: DIAG_SETTLE_MS);
|
|
232
|
+
if (settle > 0) await new Promise((r) => setTimeout(r, settle));
|
|
233
|
+
const exec = mcp.externalToolExecutors.mcp__ide__getDiagnostics;
|
|
234
|
+
let p;
|
|
235
|
+
try {
|
|
236
|
+
p = mcp.mcpClient.callTool(exec.serverName, exec.toolName, {
|
|
237
|
+
path: filePath,
|
|
238
|
+
});
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const data = await withTimeout(
|
|
243
|
+
p.then(parseToolResultJson),
|
|
244
|
+
opts.timeoutMs || DEFAULT_TIMEOUT_MS,
|
|
245
|
+
);
|
|
246
|
+
const all = Array.isArray(data?.diagnostics) ? data.diagnostics : null;
|
|
247
|
+
if (!all) return null;
|
|
248
|
+
const relevant = all.filter(
|
|
249
|
+
(d) => d && DIAG_SEVERITIES.has(String(d.severity).toLowerCase()),
|
|
250
|
+
);
|
|
251
|
+
return relevant.length > 0 ? relevant : null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ─── IDE-native diff approval (Claude-Code parity) ──────────────────────────
|
|
255
|
+
//
|
|
256
|
+
// When a permission `ask` fires for a file edit and an IDE bridge is up, the
|
|
257
|
+
// confirmation can be the editor's own openDiff review instead of a terminal
|
|
258
|
+
// y/N. The openDiff contract (extension P2): it BLOCKS until the user decides;
|
|
259
|
+
// on Accept the IDE itself writes the (possibly user-edited) right-hand text
|
|
260
|
+
// to the file — so an accepted review REPLACES the tool's own write, it does
|
|
261
|
+
// not precede it. The caller must skip normal execution on "accepted".
|
|
262
|
+
|
|
263
|
+
/** Env kill-switch for diff-approval routing: CC_IDE_DIFF_APPROVAL=0 disables. */
|
|
264
|
+
export function ideDiffApprovalEnabled(env = process.env) {
|
|
265
|
+
const v = String(env?.CC_IDE_DIFF_APPROVAL ?? "").toLowerCase();
|
|
266
|
+
return !(v === "0" || v === "false" || v === "off");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Does this MCP surface expose the IDE bridge's openDiff tool? */
|
|
270
|
+
export function hasIdeOpenDiff(mcp) {
|
|
271
|
+
return !!(
|
|
272
|
+
mcp?.mcpClient?.callTool &&
|
|
273
|
+
mcp.externalToolExecutors?.mcp__ide__openDiff?.kind === "mcp"
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Run one blocking openDiff review in the connected IDE. Returns
|
|
279
|
+
* { outcome:"accepted", finalText|null } — the IDE wrote the file itself
|
|
280
|
+
* { outcome:"rejected" } — nothing was written
|
|
281
|
+
* null — IDE unavailable / transport
|
|
282
|
+
* error / malformed reply → the
|
|
283
|
+
* caller falls back to its normal
|
|
284
|
+
* confirmation path.
|
|
285
|
+
* Deliberately NO timeout: a review takes as long as the user takes (the MCP
|
|
286
|
+
* HTTP client has no request timeout; the extension holds the response open).
|
|
287
|
+
*/
|
|
288
|
+
export async function requestIdeDiffApproval(mcp, req = {}) {
|
|
289
|
+
if (!hasIdeOpenDiff(mcp)) return null;
|
|
290
|
+
if (!req.path || typeof req.modifiedText !== "string") return null;
|
|
291
|
+
const exec = mcp.externalToolExecutors.mcp__ide__openDiff;
|
|
292
|
+
let result;
|
|
293
|
+
try {
|
|
294
|
+
result = await mcp.mcpClient.callTool(exec.serverName, exec.toolName, {
|
|
295
|
+
path: req.path,
|
|
296
|
+
modifiedText: req.modifiedText,
|
|
297
|
+
...(typeof req.originalText === "string"
|
|
298
|
+
? { originalText: req.originalText }
|
|
299
|
+
: {}),
|
|
300
|
+
...(req.title ? { title: req.title } : {}),
|
|
301
|
+
});
|
|
302
|
+
} catch {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
const data = parseToolResultJson(result);
|
|
306
|
+
if (data?.outcome === "accepted") {
|
|
307
|
+
return {
|
|
308
|
+
outcome: "accepted",
|
|
309
|
+
finalText: typeof data.finalText === "string" ? data.finalText : null,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
if (data?.outcome === "rejected") return { outcome: "rejected" };
|
|
313
|
+
return null; // anything else is not a verdict — fail safe to fallback
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Render pulled diagnostics as a compact feedback string for the tool result.
|
|
318
|
+
* Returns null when there is nothing to report.
|
|
319
|
+
*/
|
|
320
|
+
export function formatIdeDiagnostics(diags, { cap = DIAG_CAP } = {}) {
|
|
321
|
+
if (!Array.isArray(diags) || diags.length === 0) return null;
|
|
322
|
+
const shown = diags.slice(0, cap).map((d) => {
|
|
323
|
+
const loc = Number.isInteger(d.line) ? `:${d.line + 1}` : "";
|
|
324
|
+
const src = d.source ? ` (${d.source})` : "";
|
|
325
|
+
return ` [${d.severity}] ${d.file || ""}${loc} ${d.message || ""}${src}`.trimEnd();
|
|
326
|
+
});
|
|
327
|
+
const more = diags.length - shown.length;
|
|
328
|
+
return (
|
|
329
|
+
`IDE diagnostics after this edit (${diags.length} problem${diags.length === 1 ? "" : "s"}):\n` +
|
|
330
|
+
shown.join("\n") +
|
|
331
|
+
(more > 0 ? `\n (+${more} more)` : "")
|
|
332
|
+
);
|
|
333
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-memory loader — file-based project instructions for `cc agent`
|
|
3
|
+
* (Claude-Code CLAUDE.md-hierarchy parity, with our own file name).
|
|
4
|
+
*
|
|
5
|
+
* The primary instruction file is **`cc.md`** (ChainlessChain branding);
|
|
6
|
+
* `CLAUDE.md` and `AGENTS.md` are accepted as compatibility fallbacks so any
|
|
7
|
+
* repo that already carries Claude-Code/agent memory works with zero setup.
|
|
8
|
+
*
|
|
9
|
+
* Discovery order (first existing name wins per location):
|
|
10
|
+
*
|
|
11
|
+
* 1. user scope : `~/.chainlesschain/cc.md`, else `~/.claude/CLAUDE.md`
|
|
12
|
+
* 2. project scope: per directory from <git-root> down to <cwd> —
|
|
13
|
+
* `cc.md` → `CLAUDE.md` → `AGENTS.md`
|
|
14
|
+
* (root-first, so deeper files refine shallower ones)
|
|
15
|
+
* 3. local scope : `cc.local.md` → `CLAUDE.local.md` next to each project
|
|
16
|
+
* file (gitignored personal notes)
|
|
17
|
+
*
|
|
18
|
+
* `@path` import lines inside an instruction file pull in the referenced file
|
|
19
|
+
* (resolved relative to the importing file; `~/` works too), recursively up to
|
|
20
|
+
* MAX_IMPORT_DEPTH with cycle protection. Tokens inside fenced code blocks and
|
|
21
|
+
* tokens that don't resolve to a real file (npm scopes like `@scope/pkg`,
|
|
22
|
+
* emails) are ignored silently.
|
|
23
|
+
*
|
|
24
|
+
* Loading is fail-open: any I/O error yields an empty block — composing the
|
|
25
|
+
* system prompt must never crash because of a bad memory file. All fs access
|
|
26
|
+
* goes through an injectable `deps` seam (project `_deps` philosophy) and all
|
|
27
|
+
* reads are explicit UTF-8 (encoding.md rule).
|
|
28
|
+
*
|
|
29
|
+
* Disable globally with `CC_PROJECT_MEMORY=0`, per-call with
|
|
30
|
+
* `projectMemory: false` on composeSystemPrompt.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import fsDefault from "fs";
|
|
34
|
+
import pathDefault from "path";
|
|
35
|
+
import osDefault from "os";
|
|
36
|
+
|
|
37
|
+
export const DEFAULT_MAX_FILE_BYTES = 48 * 1024; // per instruction/import file
|
|
38
|
+
export const DEFAULT_MAX_TOTAL_BYTES = 192 * 1024; // whole block budget
|
|
39
|
+
export const MAX_IMPORT_DEPTH = 5;
|
|
40
|
+
|
|
41
|
+
/** Per-directory project file names, first match wins. */
|
|
42
|
+
export const PROJECT_FILE_NAMES = ["cc.md", "CLAUDE.md", "AGENTS.md"];
|
|
43
|
+
/** Local (gitignored) companion names, first match wins. */
|
|
44
|
+
export const LOCAL_FILE_NAMES = ["cc.local.md", "CLAUDE.local.md"];
|
|
45
|
+
|
|
46
|
+
// Same boundary rule as file-ref-expander: `@` at start / after whitespace or
|
|
47
|
+
// an opening bracket-quote, so emails and decorative @ never match.
|
|
48
|
+
const IMPORT_TOKEN_RE = /(^|[\s("'`[{])@([^\s"'`)\]}]+)/g;
|
|
49
|
+
|
|
50
|
+
function resolveDeps(opts) {
|
|
51
|
+
return {
|
|
52
|
+
fs: opts.deps?.fs || fsDefault,
|
|
53
|
+
path: opts.deps?.path || pathDefault,
|
|
54
|
+
os: opts.deps?.os || osDefault,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isFile(fs, p) {
|
|
59
|
+
try {
|
|
60
|
+
return fs.statSync(p).isFile();
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** First existing candidate among `names` inside `dir`, or null. */
|
|
67
|
+
function firstExisting(fs, path, dir, names) {
|
|
68
|
+
for (const name of names) {
|
|
69
|
+
const p = path.join(dir, name);
|
|
70
|
+
if (isFile(fs, p)) return p;
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Walk up from cwd looking for a `.git` marker; null when not in a repo. */
|
|
76
|
+
export function findProjectRoot(cwd, opts = {}) {
|
|
77
|
+
const { fs, path } = resolveDeps(opts);
|
|
78
|
+
let dir = path.resolve(cwd);
|
|
79
|
+
for (;;) {
|
|
80
|
+
try {
|
|
81
|
+
if (fs.existsSync(path.join(dir, ".git"))) return dir;
|
|
82
|
+
} catch {
|
|
83
|
+
/* keep walking */
|
|
84
|
+
}
|
|
85
|
+
const parent = path.dirname(dir);
|
|
86
|
+
if (parent === dir) return null;
|
|
87
|
+
dir = parent;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Ordered instruction-file discovery (user → project root → … → cwd, with the
|
|
93
|
+
* local companion right after its project file). Only existing files are
|
|
94
|
+
* returned. Deduped by absolute path (covers cwd == home corner cases).
|
|
95
|
+
*
|
|
96
|
+
* @returns {Array<{path:string, scope:"user"|"project"|"local"|"rules"}>}
|
|
97
|
+
*/
|
|
98
|
+
export function findInstructionFiles(opts = {}) {
|
|
99
|
+
const { fs, path, os } = resolveDeps(opts);
|
|
100
|
+
const cwd = path.resolve(opts.cwd || process.cwd());
|
|
101
|
+
const home = opts.home || os.homedir() || "";
|
|
102
|
+
|
|
103
|
+
const seen = new Set();
|
|
104
|
+
const out = [];
|
|
105
|
+
const push = (p, scope) => {
|
|
106
|
+
if (!p) return;
|
|
107
|
+
const abs = path.resolve(p);
|
|
108
|
+
if (seen.has(abs) || !isFile(fs, abs)) return;
|
|
109
|
+
seen.add(abs);
|
|
110
|
+
out.push({ path: abs, scope });
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (home) {
|
|
114
|
+
push(
|
|
115
|
+
firstExisting(fs, path, home, [
|
|
116
|
+
path.join(".chainlesschain", "cc.md"),
|
|
117
|
+
path.join(".claude", "CLAUDE.md"),
|
|
118
|
+
]),
|
|
119
|
+
"user",
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const root = findProjectRoot(cwd, opts) || cwd;
|
|
124
|
+
const chain = [];
|
|
125
|
+
let dir = cwd;
|
|
126
|
+
for (;;) {
|
|
127
|
+
chain.unshift(dir);
|
|
128
|
+
if (dir === root) break;
|
|
129
|
+
const parent = path.dirname(dir);
|
|
130
|
+
if (parent === dir) break;
|
|
131
|
+
dir = parent;
|
|
132
|
+
}
|
|
133
|
+
for (const d of chain) {
|
|
134
|
+
push(firstExisting(fs, path, d, PROJECT_FILE_NAMES), "project");
|
|
135
|
+
push(firstExisting(fs, path, d, LOCAL_FILE_NAMES), "local");
|
|
136
|
+
// Template-scaffolded project rules (`cc init -t` writes these) join the
|
|
137
|
+
// chain too, so scaffold-flow and memory-flow projects both feed the agent.
|
|
138
|
+
push(path.join(d, ".chainlesschain", "rules.md"), "rules");
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Collect `@path` import tokens from instruction text, skipping fenced code
|
|
145
|
+
* blocks (``` / ~~~). Line-level scanning is good enough for memory files,
|
|
146
|
+
* which use imports on their own prose lines.
|
|
147
|
+
*/
|
|
148
|
+
export function collectImportTokens(text) {
|
|
149
|
+
const found = [];
|
|
150
|
+
let inFence = false;
|
|
151
|
+
for (const line of String(text).split(/\r?\n/)) {
|
|
152
|
+
if (/^\s*(```|~~~)/.test(line)) {
|
|
153
|
+
inFence = !inFence;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (inFence) continue;
|
|
157
|
+
IMPORT_TOKEN_RE.lastIndex = 0;
|
|
158
|
+
let m;
|
|
159
|
+
while ((m = IMPORT_TOKEN_RE.exec(line)) !== null) {
|
|
160
|
+
if (m[2]) found.push(m[2]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return found;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function readCapped(fs, abs, maxFileBytes) {
|
|
167
|
+
const buf = fs.readFileSync(abs);
|
|
168
|
+
const truncated = buf.length > maxFileBytes;
|
|
169
|
+
const content = (truncated ? buf.slice(0, maxFileBytes) : buf).toString(
|
|
170
|
+
"utf-8",
|
|
171
|
+
);
|
|
172
|
+
return { content, bytes: buf.length, truncated };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Load the full instruction set: hierarchy files + recursive imports.
|
|
177
|
+
*
|
|
178
|
+
* @param {object} [opts] { cwd, home, deps, maxFileBytes, maxTotalBytes }
|
|
179
|
+
* @returns {{ files: Array<{path,scope,bytes,truncated,content}>, warnings: string[] }}
|
|
180
|
+
*/
|
|
181
|
+
export function loadProjectInstructions(opts = {}) {
|
|
182
|
+
const { fs, path, os } = resolveDeps(opts);
|
|
183
|
+
const home = opts.home || os.homedir() || "";
|
|
184
|
+
const maxFileBytes = Number.isFinite(opts.maxFileBytes)
|
|
185
|
+
? opts.maxFileBytes
|
|
186
|
+
: DEFAULT_MAX_FILE_BYTES;
|
|
187
|
+
const maxTotalBytes = Number.isFinite(opts.maxTotalBytes)
|
|
188
|
+
? opts.maxTotalBytes
|
|
189
|
+
: DEFAULT_MAX_TOTAL_BYTES;
|
|
190
|
+
|
|
191
|
+
const roots = findInstructionFiles(opts);
|
|
192
|
+
const visited = new Set(roots.map((r) => r.path));
|
|
193
|
+
const out = [];
|
|
194
|
+
const warnings = [];
|
|
195
|
+
let total = 0;
|
|
196
|
+
|
|
197
|
+
// Queue of { abs, scope, depth } — imports inherit "import" scope.
|
|
198
|
+
const queue = roots.map((r) => ({ abs: r.path, scope: r.scope, depth: 0 }));
|
|
199
|
+
|
|
200
|
+
while (queue.length) {
|
|
201
|
+
const { abs, scope, depth } = queue.shift();
|
|
202
|
+
if (total >= maxTotalBytes) {
|
|
203
|
+
warnings.push(
|
|
204
|
+
`project-memory budget (${maxTotalBytes} bytes) exhausted — remaining files skipped`,
|
|
205
|
+
);
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
let entry;
|
|
209
|
+
try {
|
|
210
|
+
entry = readCapped(fs, abs, maxFileBytes);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
warnings.push(`${abs} — cannot read: ${err.message}`);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
total += Math.min(entry.bytes, maxFileBytes);
|
|
216
|
+
out.push({ path: abs, scope, ...entry });
|
|
217
|
+
|
|
218
|
+
if (depth >= MAX_IMPORT_DEPTH) continue;
|
|
219
|
+
const baseDir = path.dirname(abs);
|
|
220
|
+
for (const raw of collectImportTokens(entry.content)) {
|
|
221
|
+
let target = raw;
|
|
222
|
+
if (target.startsWith("~/") || target === "~") {
|
|
223
|
+
if (!home) continue;
|
|
224
|
+
target = path.join(home, target.slice(1));
|
|
225
|
+
}
|
|
226
|
+
const resolved = path.resolve(baseDir, target);
|
|
227
|
+
if (visited.has(resolved) || !isFile(fs, resolved)) continue; // silent:
|
|
228
|
+
// non-files are decorative @tokens (npm scopes, emails), not imports.
|
|
229
|
+
visited.add(resolved);
|
|
230
|
+
queue.push({ abs: resolved, scope: "import", depth: depth + 1 });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return { files: out, warnings };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function escapeAttr(s) {
|
|
237
|
+
return String(s).replace(/"/g, """);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Render loaded instructions as a single system-prompt block ("" if none). */
|
|
241
|
+
export function renderProjectInstructionsBlock(loaded) {
|
|
242
|
+
const files = loaded?.files || [];
|
|
243
|
+
if (!files.length) return "";
|
|
244
|
+
const parts = [
|
|
245
|
+
'<project-instructions note="project memory auto-loaded from cc.md / CLAUDE.md / AGENTS.md; follow these as authoritative project conventions">',
|
|
246
|
+
];
|
|
247
|
+
for (const f of files) {
|
|
248
|
+
const attrs =
|
|
249
|
+
`path="${escapeAttr(f.path)}" scope="${f.scope}"` +
|
|
250
|
+
(f.truncated ? ` truncated="true" total-bytes="${f.bytes}"` : "");
|
|
251
|
+
parts.push(`<file ${attrs}>`);
|
|
252
|
+
parts.push(f.content.trimEnd());
|
|
253
|
+
if (f.truncated) {
|
|
254
|
+
parts.push(`… [truncated — file is ${f.bytes} bytes]`);
|
|
255
|
+
}
|
|
256
|
+
parts.push(`</file>`);
|
|
257
|
+
}
|
|
258
|
+
parts.push("</project-instructions>");
|
|
259
|
+
return parts.join("\n");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* One-call convenience for composeSystemPrompt: returns the rendered block or
|
|
264
|
+
* "" — and never throws (fail-open by design).
|
|
265
|
+
*/
|
|
266
|
+
export function loadProjectInstructionsBlock(opts = {}) {
|
|
267
|
+
try {
|
|
268
|
+
const loaded = loadProjectInstructions(opts);
|
|
269
|
+
return renderProjectInstructionsBlock(loaded);
|
|
270
|
+
} catch {
|
|
271
|
+
return "";
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export const _deps = { fs: fsDefault, path: pathDefault, os: osDefault };
|