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,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-session Markdown export — Claude-Code `/export` parity for the JSONL
|
|
3
|
+
* headless/agent session store (`~/.../sessions/<id>.jsonl`, the `--resume`
|
|
4
|
+
* sessions). `cc session export` keeps serving chat-DB sessions and falls
|
|
5
|
+
* back to this renderer when the id only exists in the JSONL store.
|
|
6
|
+
*
|
|
7
|
+
* Pure function over the event list (see jsonl-session-store.js appendEvent):
|
|
8
|
+
* { type, timestamp, data }
|
|
9
|
+
* types: session_start{title,provider,model} · user_message · assistant_message
|
|
10
|
+
* · system · tool_call{tool,args} · tool_result{tool,result}
|
|
11
|
+
* · compact(stats) · token_usage{...}
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const TOOL_BLOCK_CAP = 4000;
|
|
15
|
+
|
|
16
|
+
function asText(content) {
|
|
17
|
+
if (content == null) return "";
|
|
18
|
+
if (typeof content === "string") return content;
|
|
19
|
+
try {
|
|
20
|
+
return JSON.stringify(content, null, 2);
|
|
21
|
+
} catch {
|
|
22
|
+
return String(content);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function fence(body, lang = "") {
|
|
27
|
+
const text = asText(body);
|
|
28
|
+
const capped =
|
|
29
|
+
text.length > TOOL_BLOCK_CAP
|
|
30
|
+
? `${text.slice(0, TOOL_BLOCK_CAP)}\n… [truncated]`
|
|
31
|
+
: text;
|
|
32
|
+
// avoid breaking out of the fence when the payload contains ```
|
|
33
|
+
const guard = capped.includes("```") ? "````" : "```";
|
|
34
|
+
return `${guard}${lang}\n${capped}\n${guard}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Render a JSONL agent session as a readable Markdown transcript. */
|
|
38
|
+
export function renderAgentSessionMarkdown(sessionId, events, opts = {}) {
|
|
39
|
+
const L = [];
|
|
40
|
+
const start = events.find((e) => e?.type === "session_start");
|
|
41
|
+
const firstTs = events.find((e) => Number.isFinite(e?.timestamp))?.timestamp;
|
|
42
|
+
|
|
43
|
+
L.push(`# Agent Session ${sessionId}`);
|
|
44
|
+
L.push("");
|
|
45
|
+
const metaBits = [];
|
|
46
|
+
if (start?.data?.title && start.data.title !== "Untitled")
|
|
47
|
+
metaBits.push(`title: ${start.data.title}`);
|
|
48
|
+
if (start?.data?.provider) metaBits.push(`provider: ${start.data.provider}`);
|
|
49
|
+
if (start?.data?.model) metaBits.push(`model: ${start.data.model}`);
|
|
50
|
+
if (firstTs) metaBits.push(`started: ${new Date(firstTs).toISOString()}`);
|
|
51
|
+
if (opts.exportedAt) metaBits.push(`exported: ${opts.exportedAt}`);
|
|
52
|
+
if (metaBits.length) {
|
|
53
|
+
L.push(`> ${metaBits.join(" · ")}`);
|
|
54
|
+
L.push("");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let users = 0;
|
|
58
|
+
let assistants = 0;
|
|
59
|
+
let tokensIn = 0;
|
|
60
|
+
let tokensOut = 0;
|
|
61
|
+
|
|
62
|
+
for (const ev of events) {
|
|
63
|
+
if (!ev || typeof ev !== "object") continue;
|
|
64
|
+
switch (ev.type) {
|
|
65
|
+
case "user_message":
|
|
66
|
+
users += 1;
|
|
67
|
+
L.push("## 👤 User");
|
|
68
|
+
L.push("");
|
|
69
|
+
L.push(asText(ev.data?.content));
|
|
70
|
+
L.push("");
|
|
71
|
+
break;
|
|
72
|
+
case "assistant_message":
|
|
73
|
+
assistants += 1;
|
|
74
|
+
L.push("## 🤖 Assistant");
|
|
75
|
+
L.push("");
|
|
76
|
+
L.push(asText(ev.data?.content));
|
|
77
|
+
L.push("");
|
|
78
|
+
break;
|
|
79
|
+
case "system":
|
|
80
|
+
L.push("## ⚙ System");
|
|
81
|
+
L.push("");
|
|
82
|
+
L.push(asText(ev.data?.content));
|
|
83
|
+
L.push("");
|
|
84
|
+
break;
|
|
85
|
+
case "tool_call":
|
|
86
|
+
L.push(`**🔧 tool_call — \`${ev.data?.tool || "?"}\`**`);
|
|
87
|
+
L.push("");
|
|
88
|
+
L.push(fence(ev.data?.args, "json"));
|
|
89
|
+
L.push("");
|
|
90
|
+
break;
|
|
91
|
+
case "tool_result":
|
|
92
|
+
L.push(`**↩ tool_result — \`${ev.data?.tool || "?"}\`**`);
|
|
93
|
+
L.push("");
|
|
94
|
+
L.push(fence(ev.data?.result));
|
|
95
|
+
L.push("");
|
|
96
|
+
break;
|
|
97
|
+
case "compact": {
|
|
98
|
+
const s = ev.data || {};
|
|
99
|
+
const saved = s.savedTokens ?? s.saved ?? null;
|
|
100
|
+
L.push(
|
|
101
|
+
`> ⊟ context compacted${saved != null ? ` — saved ~${saved} tokens` : ""}`,
|
|
102
|
+
);
|
|
103
|
+
L.push("");
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case "token_usage": {
|
|
107
|
+
const d = ev.data || {};
|
|
108
|
+
tokensIn += Number(d.inputTokens ?? d.input_tokens ?? 0) || 0;
|
|
109
|
+
tokensOut += Number(d.outputTokens ?? d.output_tokens ?? 0) || 0;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
default:
|
|
113
|
+
break; // session_start handled above; unknown types skipped
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
L.push("---");
|
|
118
|
+
const totals = [`${users} user / ${assistants} assistant turns`];
|
|
119
|
+
if (tokensIn || tokensOut)
|
|
120
|
+
totals.push(`tokens in/out: ${tokensIn}/${tokensOut}`);
|
|
121
|
+
L.push(`_${totals.join(" · ")}_`);
|
|
122
|
+
L.push("");
|
|
123
|
+
return L.join("\n");
|
|
124
|
+
}
|
package/src/lib/ide-context.js
CHANGED
|
@@ -251,6 +251,68 @@ export async function collectIdeDiagnostics(mcp, filePath, opts = {}) {
|
|
|
251
251
|
return relevant.length > 0 ? relevant : null;
|
|
252
252
|
}
|
|
253
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
|
+
|
|
254
316
|
/**
|
|
255
317
|
* Render pulled diagnostics as a compact feedback string for the tool result.
|
|
256
318
|
* Returns null when there is nothing to report.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cc init --ai` — agent-enhanced inventory (claude /init parity, module 99
|
|
3
|
+
* §5.2). Runs AFTER the offline census wrote the starter cc.md: a bounded
|
|
4
|
+
* headless agent reads the repo (README, entry files) and rewrites cc.md so
|
|
5
|
+
* the Conventions section holds real, observed conventions instead of the
|
|
6
|
+
* "(add project rules here)" placeholder.
|
|
7
|
+
*
|
|
8
|
+
* Self-reference guard: the child run executes with CC_PROJECT_MEMORY=0 so
|
|
9
|
+
* the half-baked cc.md being refined is NOT injected into the very agent
|
|
10
|
+
* refining it. Restored in finally.
|
|
11
|
+
*
|
|
12
|
+
* Offline inventory stays the `cc init` default — this is strictly opt-in
|
|
13
|
+
* (needs a reachable LLM).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const AI_REFINE_MAX_TURNS = 12;
|
|
17
|
+
export const AI_REFINE_TOOLS = [
|
|
18
|
+
"read_file",
|
|
19
|
+
"list_dir",
|
|
20
|
+
"search_files",
|
|
21
|
+
"write_file",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export function buildRefinePrompt() {
|
|
25
|
+
return [
|
|
26
|
+
"You are refining this project's memory file `cc.md` (it was just generated from an offline folder census).",
|
|
27
|
+
"Steps:",
|
|
28
|
+
"1. read_file cc.md to see the current draft.",
|
|
29
|
+
"2. Read the README and one or two key entry/config files to learn the project's real conventions (code style, commit format, how to run tests, architecture notes worth remembering).",
|
|
30
|
+
"3. write_file cc.md with the refined version: KEEP the existing sections and data, but replace the placeholder under `## Conventions` with concrete, observed conventions (5-10 short bullets max). Do not invent facts you did not see.",
|
|
31
|
+
"Keep the file concise — it is loaded into every agent run.",
|
|
32
|
+
].join("\n");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Run the refine pass.
|
|
37
|
+
* @param {object} opts { cwd, provider?, model?, baseUrl?, apiKey?,
|
|
38
|
+
* maxTurns?, runHeadless? (test seam) }
|
|
39
|
+
* @returns {Promise<{exitCode:number, result:string, isError:boolean}>}
|
|
40
|
+
*/
|
|
41
|
+
export async function aiRefineMemoryFile(opts = {}) {
|
|
42
|
+
const run =
|
|
43
|
+
opts.runHeadless ||
|
|
44
|
+
(await import("../runtime/headless-runner.js")).runAgentHeadless;
|
|
45
|
+
|
|
46
|
+
const prev = process.env.CC_PROJECT_MEMORY;
|
|
47
|
+
process.env.CC_PROJECT_MEMORY = "0"; // self-reference guard
|
|
48
|
+
try {
|
|
49
|
+
return await run({
|
|
50
|
+
prompt: buildRefinePrompt(),
|
|
51
|
+
cwd: opts.cwd || process.cwd(),
|
|
52
|
+
provider: opts.provider || undefined,
|
|
53
|
+
model: opts.model || undefined,
|
|
54
|
+
baseUrl: opts.baseUrl || undefined,
|
|
55
|
+
apiKey: opts.apiKey || undefined,
|
|
56
|
+
permissionMode: "acceptEdits", // write_file must work headless
|
|
57
|
+
allowedTools: AI_REFINE_TOOLS,
|
|
58
|
+
maxTurns: opts.maxTurns || AI_REFINE_MAX_TURNS,
|
|
59
|
+
expandFileRefs: false,
|
|
60
|
+
outputFormat: "text",
|
|
61
|
+
});
|
|
62
|
+
} finally {
|
|
63
|
+
if (prev === undefined) delete process.env.CC_PROJECT_MEMORY;
|
|
64
|
+
else process.env.CC_PROJECT_MEMORY = prev;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cc agent -p --json-schema <file>` — structured output for headless runs.
|
|
3
|
+
*
|
|
4
|
+
* The final answer must be JSON that validates against a (subset) JSON
|
|
5
|
+
* Schema; invalid replies are retried with a corrective prompt (up to
|
|
6
|
+
* MAX_ATTEMPTS total). Implemented entirely AROUND runAgentHeadless using its
|
|
7
|
+
* `deps.writeOut` capture seam — the runner itself is untouched: each attempt
|
|
8
|
+
* runs with output captured, the validated JSON is the only thing printed.
|
|
9
|
+
*
|
|
10
|
+
* Validator subset (enough for tool/script contracts, not full draft-2020):
|
|
11
|
+
* type (object/array/string/number/integer/boolean/null), properties,
|
|
12
|
+
* required, items, enum, const, additionalProperties:false. Zero deps.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fsDefault from "fs";
|
|
16
|
+
|
|
17
|
+
export const MAX_ATTEMPTS = 3;
|
|
18
|
+
export const _deps = { fs: fsDefault };
|
|
19
|
+
|
|
20
|
+
/** Validate `value` against the schema subset. Returns error strings ([] = valid). */
|
|
21
|
+
export function validateAgainstSchema(value, schema, path = "$") {
|
|
22
|
+
const errors = [];
|
|
23
|
+
if (!schema || typeof schema !== "object") return errors;
|
|
24
|
+
|
|
25
|
+
const typeOf = (v) =>
|
|
26
|
+
v === null
|
|
27
|
+
? "null"
|
|
28
|
+
: Array.isArray(v)
|
|
29
|
+
? "array"
|
|
30
|
+
: typeof v === "number" && Number.isInteger(v)
|
|
31
|
+
? "integer"
|
|
32
|
+
: typeof v;
|
|
33
|
+
|
|
34
|
+
if (schema.type) {
|
|
35
|
+
const want = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
36
|
+
const got = typeOf(value);
|
|
37
|
+
const ok = want.some((t) => t === got || (t === "number" && got === "integer"));
|
|
38
|
+
if (!ok) {
|
|
39
|
+
errors.push(`${path}: expected type ${want.join("|")}, got ${got}`);
|
|
40
|
+
return errors; // type mismatch — deeper checks are noise
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (schema.enum && !schema.enum.some((e) => JSON.stringify(e) === JSON.stringify(value))) {
|
|
44
|
+
errors.push(`${path}: value not in enum [${schema.enum.map((e) => JSON.stringify(e)).join(", ")}]`);
|
|
45
|
+
}
|
|
46
|
+
if (schema.const !== undefined && JSON.stringify(schema.const) !== JSON.stringify(value)) {
|
|
47
|
+
errors.push(`${path}: must equal const ${JSON.stringify(schema.const)}`);
|
|
48
|
+
}
|
|
49
|
+
if (typeOf(value) === "object" && !Array.isArray(value)) {
|
|
50
|
+
for (const req of schema.required || []) {
|
|
51
|
+
if (!(req in value)) errors.push(`${path}: missing required property "${req}"`);
|
|
52
|
+
}
|
|
53
|
+
const props = schema.properties || {};
|
|
54
|
+
for (const [k, v] of Object.entries(value)) {
|
|
55
|
+
if (props[k]) {
|
|
56
|
+
errors.push(...validateAgainstSchema(v, props[k], `${path}.${k}`));
|
|
57
|
+
} else if (schema.additionalProperties === false) {
|
|
58
|
+
errors.push(`${path}: unexpected property "${k}"`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(value) && schema.items) {
|
|
63
|
+
value.forEach((item, i) => {
|
|
64
|
+
errors.push(...validateAgainstSchema(item, schema.items, `${path}[${i}]`));
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return errors;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Pull a JSON payload out of an LLM reply (bare, fenced, or embedded). */
|
|
71
|
+
export function extractJsonPayload(text) {
|
|
72
|
+
const raw = String(text || "").trim();
|
|
73
|
+
const tries = [];
|
|
74
|
+
tries.push(raw);
|
|
75
|
+
const fence = /```(?:json)?\s*([\s\S]*?)```/i.exec(raw);
|
|
76
|
+
if (fence) tries.push(fence[1].trim());
|
|
77
|
+
const firstObj = raw.indexOf("{");
|
|
78
|
+
const lastObj = raw.lastIndexOf("}");
|
|
79
|
+
if (firstObj !== -1 && lastObj > firstObj) tries.push(raw.slice(firstObj, lastObj + 1));
|
|
80
|
+
const firstArr = raw.indexOf("[");
|
|
81
|
+
const lastArr = raw.lastIndexOf("]");
|
|
82
|
+
if (firstArr !== -1 && lastArr > firstArr) tries.push(raw.slice(firstArr, lastArr + 1));
|
|
83
|
+
for (const candidate of tries) {
|
|
84
|
+
if (!candidate) continue;
|
|
85
|
+
try {
|
|
86
|
+
return { ok: true, value: JSON.parse(candidate) };
|
|
87
|
+
} catch {
|
|
88
|
+
/* next candidate */
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { ok: false, error: "reply contains no parseable JSON" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function buildSchemaInstruction(schema) {
|
|
95
|
+
return [
|
|
96
|
+
"OUTPUT CONTRACT: your FINAL reply must be ONLY a JSON value (no prose, no markdown fences) that validates against this JSON Schema:",
|
|
97
|
+
JSON.stringify(schema),
|
|
98
|
+
].join("\n");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function buildRetryPrompt(originalPrompt, raw, errors) {
|
|
102
|
+
return [
|
|
103
|
+
originalPrompt,
|
|
104
|
+
"",
|
|
105
|
+
"Your previous reply failed JSON Schema validation:",
|
|
106
|
+
...errors.slice(0, 10).map((e) => `- ${e}`),
|
|
107
|
+
"",
|
|
108
|
+
`Previous reply (for reference): ${String(raw).slice(0, 2000)}`,
|
|
109
|
+
"",
|
|
110
|
+
"Reply again with ONLY the corrected JSON.",
|
|
111
|
+
].join("\n");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Run a headless turn constrained to a schema, retrying on validation
|
|
116
|
+
* failure. Prints the validated JSON to writeOut; returns the exit code.
|
|
117
|
+
*
|
|
118
|
+
* @param {object} cfg { schemaFile|schema, baseOptions, runHeadless,
|
|
119
|
+
* maxAttempts?, writeOut?, writeErr?, deps? }
|
|
120
|
+
*/
|
|
121
|
+
export async function runJsonSchemaConstrained(cfg = {}) {
|
|
122
|
+
const fs = cfg.deps?.fs || _deps.fs;
|
|
123
|
+
const writeOut = cfg.writeOut || ((s) => process.stdout.write(s));
|
|
124
|
+
const writeErr = cfg.writeErr || ((s) => process.stderr.write(s));
|
|
125
|
+
const maxAttempts = cfg.maxAttempts || MAX_ATTEMPTS;
|
|
126
|
+
|
|
127
|
+
const schema =
|
|
128
|
+
cfg.schema || JSON.parse(fs.readFileSync(cfg.schemaFile, "utf-8"));
|
|
129
|
+
const instruction = buildSchemaInstruction(schema);
|
|
130
|
+
const base = cfg.baseOptions || {};
|
|
131
|
+
|
|
132
|
+
let prompt = base.prompt;
|
|
133
|
+
let lastRaw = "";
|
|
134
|
+
let lastErrors = ["no attempts ran"];
|
|
135
|
+
|
|
136
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
137
|
+
let captured = "";
|
|
138
|
+
const outcome = await cfg.runHeadless(
|
|
139
|
+
{
|
|
140
|
+
...base,
|
|
141
|
+
prompt,
|
|
142
|
+
outputFormat: "text",
|
|
143
|
+
appendSystemPrompt: [base.appendSystemPrompt, instruction]
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
.join("\n\n"),
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
writeOut: (s) => {
|
|
149
|
+
captured += s;
|
|
150
|
+
},
|
|
151
|
+
writeErr,
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
const raw = String(outcome?.result ?? captured ?? "").trim() || captured.trim();
|
|
155
|
+
lastRaw = raw;
|
|
156
|
+
const parsed = extractJsonPayload(raw);
|
|
157
|
+
if (parsed.ok) {
|
|
158
|
+
const errors = validateAgainstSchema(parsed.value, schema);
|
|
159
|
+
if (errors.length === 0) {
|
|
160
|
+
writeOut(`${JSON.stringify(parsed.value, null, 2)}\n`);
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
lastErrors = errors;
|
|
164
|
+
} else {
|
|
165
|
+
lastErrors = [parsed.error];
|
|
166
|
+
}
|
|
167
|
+
if (attempt < maxAttempts) {
|
|
168
|
+
writeErr(
|
|
169
|
+
`--json-schema: attempt ${attempt} failed validation (${lastErrors.length} error(s)) — retrying…\n`,
|
|
170
|
+
);
|
|
171
|
+
prompt = buildRetryPrompt(base.prompt, raw, lastErrors);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
writeErr(
|
|
176
|
+
`--json-schema: reply failed validation after ${maxAttempts} attempts:\n${lastErrors
|
|
177
|
+
.map((e) => ` - ${e}`)
|
|
178
|
+
.join("\n")}\nLast reply:\n${lastRaw.slice(0, 1000)}\n`,
|
|
179
|
+
);
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cc mcp serve` — expose cc's local file tools as an MCP server so OTHER
|
|
3
|
+
* MCP clients (Claude Desktop, another cc, any Streamable-HTTP client) can
|
|
4
|
+
* use this machine's workspace. Claude-Code `claude mcp serve` parity.
|
|
5
|
+
*
|
|
6
|
+
* Protocol: Streamable-HTTP MCP, same shape the IDE-bridge work verified
|
|
7
|
+
* against the real CLI MCPClient — every request is POST JSON-RPC answered
|
|
8
|
+
* with application/json (no persistent SSE GET needed): `initialize`,
|
|
9
|
+
* `notifications/initialized`, `tools/list`, `tools/call`; tool failures are
|
|
10
|
+
* `isError` results, transport failures JSON-RPC errors.
|
|
11
|
+
*
|
|
12
|
+
* Security: tools are CONFINED to the serve root (path resolves inside root
|
|
13
|
+
* or the call fails), Bearer-token auth is on by default (random token,
|
|
14
|
+
* printed once), `--read-only` drops write_file. Zero npm deps (node http).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import http from "http";
|
|
18
|
+
import fsDefault from "fs";
|
|
19
|
+
import pathDefault from "path";
|
|
20
|
+
import { randomBytes } from "crypto";
|
|
21
|
+
|
|
22
|
+
export const MAX_READ_BYTES = 200 * 1024;
|
|
23
|
+
export const MAX_LIST_ENTRIES = 500;
|
|
24
|
+
export const MAX_SEARCH_RESULTS = 200;
|
|
25
|
+
export const MAX_SEARCH_ENTRIES = 50_000;
|
|
26
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build"]);
|
|
27
|
+
|
|
28
|
+
export const _deps = { fs: fsDefault, path: pathDefault };
|
|
29
|
+
|
|
30
|
+
/** Resolve `rel` inside `root`; throws on escape (.. traversal, abs paths out). */
|
|
31
|
+
export function confine(root, rel, deps = _deps) {
|
|
32
|
+
const abs = deps.path.resolve(root, rel || ".");
|
|
33
|
+
const normRoot = deps.path.resolve(root);
|
|
34
|
+
if (abs !== normRoot && !abs.startsWith(normRoot + deps.path.sep)) {
|
|
35
|
+
throw new Error(`path escapes serve root: ${rel}`);
|
|
36
|
+
}
|
|
37
|
+
return abs;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ok(text) {
|
|
41
|
+
return { content: [{ type: "text", text: String(text) }] };
|
|
42
|
+
}
|
|
43
|
+
function fail(message) {
|
|
44
|
+
return { content: [{ type: "text", text: String(message) }], isError: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Build the tool table (name → {description, inputSchema, handler}). */
|
|
48
|
+
export function buildTools({ root, readOnly = false, deps = _deps }) {
|
|
49
|
+
const fs = deps.fs;
|
|
50
|
+
const tools = {
|
|
51
|
+
read_file: {
|
|
52
|
+
description: `Read a UTF-8 file under the serve root (${MAX_READ_BYTES} byte cap)`,
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: "object",
|
|
55
|
+
properties: { path: { type: "string" } },
|
|
56
|
+
required: ["path"],
|
|
57
|
+
},
|
|
58
|
+
handler: ({ path: rel }) => {
|
|
59
|
+
const abs = confine(root, rel, deps);
|
|
60
|
+
const buf = fs.readFileSync(abs);
|
|
61
|
+
const truncated = buf.length > MAX_READ_BYTES;
|
|
62
|
+
const text = (truncated ? buf.slice(0, MAX_READ_BYTES) : buf).toString(
|
|
63
|
+
"utf-8",
|
|
64
|
+
);
|
|
65
|
+
return ok(truncated ? `${text}\n… [truncated ${buf.length} bytes]` : text);
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
list_dir: {
|
|
69
|
+
description: "List a directory under the serve root (dirs get trailing /)",
|
|
70
|
+
inputSchema: {
|
|
71
|
+
type: "object",
|
|
72
|
+
properties: { path: { type: "string" } },
|
|
73
|
+
},
|
|
74
|
+
handler: ({ path: rel } = {}) => {
|
|
75
|
+
const abs = confine(root, rel || ".", deps);
|
|
76
|
+
const entries = fs
|
|
77
|
+
.readdirSync(abs, { withFileTypes: true })
|
|
78
|
+
.slice(0, MAX_LIST_ENTRIES)
|
|
79
|
+
.map((e) => (e.isDirectory() ? `${e.name}/` : e.name));
|
|
80
|
+
return ok(entries.join("\n"));
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
search_files: {
|
|
84
|
+
description:
|
|
85
|
+
"Find files under the serve root whose RELATIVE PATH contains the query (case-insensitive, bounded walk)",
|
|
86
|
+
inputSchema: {
|
|
87
|
+
type: "object",
|
|
88
|
+
properties: {
|
|
89
|
+
query: { type: "string" },
|
|
90
|
+
dir: { type: "string" },
|
|
91
|
+
},
|
|
92
|
+
required: ["query"],
|
|
93
|
+
},
|
|
94
|
+
handler: ({ query, dir } = {}) => {
|
|
95
|
+
const base = confine(root, dir || ".", deps);
|
|
96
|
+
const q = String(query).toLowerCase();
|
|
97
|
+
const hits = [];
|
|
98
|
+
let seen = 0;
|
|
99
|
+
const walk = (d) => {
|
|
100
|
+
if (hits.length >= MAX_SEARCH_RESULTS || seen >= MAX_SEARCH_ENTRIES)
|
|
101
|
+
return;
|
|
102
|
+
let list;
|
|
103
|
+
try {
|
|
104
|
+
list = fs.readdirSync(d, { withFileTypes: true });
|
|
105
|
+
} catch {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
for (const e of list) {
|
|
109
|
+
if (hits.length >= MAX_SEARCH_RESULTS || ++seen >= MAX_SEARCH_ENTRIES)
|
|
110
|
+
return;
|
|
111
|
+
const abs = deps.path.join(d, e.name);
|
|
112
|
+
if (e.isDirectory()) {
|
|
113
|
+
if (!SKIP_DIRS.has(e.name) && !e.name.startsWith("."))
|
|
114
|
+
walk(abs);
|
|
115
|
+
} else {
|
|
116
|
+
const rel = deps.path.relative(root, abs).replace(/\\/g, "/");
|
|
117
|
+
if (rel.toLowerCase().includes(q)) hits.push(rel);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
walk(base);
|
|
122
|
+
return ok(hits.join("\n") || "(no matches)");
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
if (!readOnly) {
|
|
127
|
+
tools.write_file = {
|
|
128
|
+
description: "Write a UTF-8 file under the serve root (creates parent dirs)",
|
|
129
|
+
inputSchema: {
|
|
130
|
+
type: "object",
|
|
131
|
+
properties: {
|
|
132
|
+
path: { type: "string" },
|
|
133
|
+
content: { type: "string" },
|
|
134
|
+
},
|
|
135
|
+
required: ["path", "content"],
|
|
136
|
+
},
|
|
137
|
+
handler: ({ path: rel, content }) => {
|
|
138
|
+
const abs = confine(root, rel, deps);
|
|
139
|
+
fs.mkdirSync(deps.path.dirname(abs), { recursive: true });
|
|
140
|
+
fs.writeFileSync(abs, String(content), "utf-8");
|
|
141
|
+
return ok(`wrote ${Buffer.byteLength(String(content))} bytes to ${rel}`);
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return tools;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function rpcResult(id, result) {
|
|
149
|
+
return JSON.stringify({ jsonrpc: "2.0", id, result });
|
|
150
|
+
}
|
|
151
|
+
function rpcError(id, code, message) {
|
|
152
|
+
return JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Start the server. Returns { server, port, token, url, close() }.
|
|
157
|
+
*
|
|
158
|
+
* @param {object} opts { root, port=0, token (null → random, false → no auth),
|
|
159
|
+
* readOnly, deps }
|
|
160
|
+
*/
|
|
161
|
+
export function startMcpServe(opts = {}) {
|
|
162
|
+
const deps = { ..._deps, ...(opts.deps || {}) };
|
|
163
|
+
const root = deps.path.resolve(opts.root || process.cwd());
|
|
164
|
+
const readOnly = Boolean(opts.readOnly);
|
|
165
|
+
const token =
|
|
166
|
+
opts.token === false
|
|
167
|
+
? null
|
|
168
|
+
: opts.token || randomBytes(16).toString("hex");
|
|
169
|
+
const tools = buildTools({ root, readOnly, deps });
|
|
170
|
+
|
|
171
|
+
const server = http.createServer((req, res) => {
|
|
172
|
+
const send = (status, body) => {
|
|
173
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
174
|
+
res.end(body);
|
|
175
|
+
};
|
|
176
|
+
if (req.method !== "POST") {
|
|
177
|
+
return send(405, rpcError(null, -32600, "POST only"));
|
|
178
|
+
}
|
|
179
|
+
if (token) {
|
|
180
|
+
const auth = req.headers.authorization || "";
|
|
181
|
+
if (auth !== `Bearer ${token}`) {
|
|
182
|
+
return send(401, rpcError(null, -32001, "unauthorized"));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
let raw = "";
|
|
186
|
+
req.on("data", (c) => {
|
|
187
|
+
raw += c;
|
|
188
|
+
});
|
|
189
|
+
req.on("end", () => {
|
|
190
|
+
let msg;
|
|
191
|
+
try {
|
|
192
|
+
msg = JSON.parse(raw);
|
|
193
|
+
} catch {
|
|
194
|
+
return send(400, rpcError(null, -32700, "parse error"));
|
|
195
|
+
}
|
|
196
|
+
const { id, method, params } = msg || {};
|
|
197
|
+
try {
|
|
198
|
+
if (method === "initialize") {
|
|
199
|
+
return send(
|
|
200
|
+
200,
|
|
201
|
+
rpcResult(id, {
|
|
202
|
+
protocolVersion: params?.protocolVersion || "2025-03-26",
|
|
203
|
+
capabilities: { tools: {} },
|
|
204
|
+
serverInfo: { name: "cc-mcp-serve", version: "1.0.0" },
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
if (method === "notifications/initialized") {
|
|
209
|
+
res.writeHead(202);
|
|
210
|
+
return res.end();
|
|
211
|
+
}
|
|
212
|
+
if (method === "tools/list") {
|
|
213
|
+
return send(
|
|
214
|
+
200,
|
|
215
|
+
rpcResult(id, {
|
|
216
|
+
tools: Object.entries(tools).map(([name, t]) => ({
|
|
217
|
+
name,
|
|
218
|
+
description: t.description,
|
|
219
|
+
inputSchema: t.inputSchema,
|
|
220
|
+
})),
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
if (method === "tools/call") {
|
|
225
|
+
const tool = tools[params?.name];
|
|
226
|
+
if (!tool) {
|
|
227
|
+
return send(200, rpcResult(id, fail(`unknown tool: ${params?.name}`)));
|
|
228
|
+
}
|
|
229
|
+
let result;
|
|
230
|
+
try {
|
|
231
|
+
result = tool.handler(params?.arguments || {});
|
|
232
|
+
} catch (err) {
|
|
233
|
+
result = fail(err.message);
|
|
234
|
+
}
|
|
235
|
+
return send(200, rpcResult(id, result));
|
|
236
|
+
}
|
|
237
|
+
return send(200, rpcError(id, -32601, `method not found: ${method}`));
|
|
238
|
+
} catch (err) {
|
|
239
|
+
return send(500, rpcError(id, -32603, err.message));
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return new Promise((resolve, reject) => {
|
|
245
|
+
server.on("error", reject);
|
|
246
|
+
server.listen(opts.port || 0, "127.0.0.1", () => {
|
|
247
|
+
const port = server.address().port;
|
|
248
|
+
resolve({
|
|
249
|
+
server,
|
|
250
|
+
port,
|
|
251
|
+
token,
|
|
252
|
+
root,
|
|
253
|
+
readOnly,
|
|
254
|
+
url: `http://127.0.0.1:${port}/mcp`,
|
|
255
|
+
close: () => new Promise((r) => server.close(r)),
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
}
|