chainlesschain 0.162.48 → 0.162.60
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/bin/chainlesschain.js +1 -1
- package/package.json +2 -2
- package/src/assets/web-panel/assets/{AIOps-BeMOUkMq.js → AIOps-a2cSbSEu.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DrRegmIt.js → ActionButton-DwvSB5Pp.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-BM7hvUn8.js → Analytics-BqaRaBDD.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-BfLLYSz_.js → AppLayout-Beck7v8t.js} +5 -5
- package/src/assets/web-panel/assets/{Audit-BpiPR-rs.js → Audit-UtJhPdXJ.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BLq4IRI9.js → Backup-HVZhcdll.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-Bv89IFrM.js → BaseInput-DRY_ZGmj.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-BUM9MdnH.js → Chat-D7Vuwfe2.js} +4 -4
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-rH_t53ZW.js → ChatBubbleRenderer-BS2q_hPX.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-82jih_zU.js → Checkbox-B406i7N1.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-ygmM1mNL.js → Codegen-BvTCqHi3.js} +1 -1
- package/src/assets/web-panel/assets/{Col-CqXc8_ft.js → Col-DRiyxTQP.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CqPPfyR3.js → Community-DWmhxHQa.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-LR8Z206-.js → Compact-DO1HBZEz.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-D4alm_Gx.js → Compliance-D4j-VHwS.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-D-DLVanT.js → Cowork-BGBkWtat.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-UDtmjvd4.js → Cron-Xa9PtMUQ.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-iqbVDPNE.js → Crosschain-wVWs4lqN.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DzdUfzc9.js → DID-DTkqiRuT.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-DvKp1skY.js → Dashboard-d9STUbrr.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-S6ORlfef.js → Dropdown-CrdxS-C8.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-mJViHjiq.js → EmailListRenderer-D78XHUEp.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-A4Lu5m_e.js → FamilyGuardDashboard-iAeSETIP.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-Bv-6737r.js → Federation-CTV1Sxqs.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DNcZoiWu.js → FormItemContext-BtwNuQKK.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-B_a0l9U4.js → GenericCardRenderer-CdEgHjkl.js} +1 -1
- package/src/assets/web-panel/assets/{Git-BbVAsh_N.js → Git-BTo-PJr_.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-ChxUMZEU.js → Governance-DquOG94r.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-Buu_bAQ-.js → Inference-DDtcBxRB.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-BMmMjxEu.js → KnowledgeGraph-KUOmNj5C.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BeGHcPEC.js → Logs-HKm7kRs7.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-CP2Qg_xJ.js → Marketplace-IxrOcbFB.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-C42B4JYb.js → McpTools-D6a1LM3S.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-D5VeitFY.js → Memory-lFkD2ZuM.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BouoJ2FQ.js → MobileBridge-xwuQTps5.js} +2 -2
- package/src/assets/web-panel/assets/MobileProjects-BYr1D3WO.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-Chn6ES5y.js → Mtc-BXpJGrjm.js} +6 -6
- package/src/assets/web-panel/assets/{MtcAudit-D0_Q6GEn.js → MtcAudit-CWttaim1.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-BWaCi_wo.js → Multisig-jKgTuVLS.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CoSEiroa.js → NLProgramming-xl4RDzQj.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-CiqCbDw3.js → Notes-DPBOvscE.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-BXmzKL9F.js → NotificationSettings-8TkIkRo3.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-BhlKyGrE.js → OrderTableRenderer-Dwa-XtoE.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-CAVgSxyI.js → Organization-CJ0xVwZM.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-BIVUo8YB.js → Overflow-V7VuUslt.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-DX-Ov-eJ.js → P2P-BxuccEGq.js} +2 -2
- package/src/assets/web-panel/assets/PdhVaultBrowser-DaP2Q5kU.js +7 -0
- package/src/assets/web-panel/assets/{Permissions-CANl-V55.js → Permissions-CPJFF0zU.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-BE90gjUO.js → PersonalDataHub-Cmn2uiuw.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-Ck8lV8Pn.js → Pipeline-0zX89_iz.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BBYUDK4T.js → Privacy-DROUg3XE.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-DcbOrnbt.js → ProjectInit-c5KESOK4.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-DZ6-whxP.js → ProjectSettings-BfiCcnb_.js} +2 -2
- package/src/assets/web-panel/assets/Projects-BtZH5-Eh.js +1 -0
- package/src/assets/web-panel/assets/{Providers-VWoO_Y9u.js → Providers-C9Rr_dOk.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-RJfSXWYg.js → QuickAsk-Du4p90W6.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-Y7MWWkXa.js → Recommend-B-DQenTl.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-nG8nut3l.js → Reputation-DvwlAVRZ.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DAio6Dx2.js → Row-rTnbvkP-.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-DwY72Lc7.js → RssFeed-DwvsqWbB.js} +3 -3
- package/src/assets/web-panel/assets/{Search-qGG6AUWY.js → Search-U_Xj5SvF.js} +1 -1
- package/src/assets/web-panel/assets/{Security-Djsmom8n.js → Security-CdjsVDQ8.js} +4 -4
- package/src/assets/web-panel/assets/{Services-DTEgHkUO.js → Services-DUd3mFXk.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-R5xY0J9Y.js → Skeleton-CA2gCJmY.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CFaRLO3o.js → Skills-BIw7Rb-m.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-B5bzoY1I.js → Sla-vySPesC0.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-B_al6SiQ.js → SpeechSettings-EniuTjBJ.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-DkUU63oJ.js → SyncSettings-DkrqbzYS.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-C2y4XdrQ.js → Tasks-oyPnWRbw.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-CcYJxTNB.js → Templates-C2Kvn60U.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-B0_sIpl2.js → Tenant-x6jVMx4D.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-B_waZb0O.js → Terminal-C2nZbPBs.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-DuMkErBx.js → TimelineRenderer-BF6HAETd.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BXjPA6rV.js → Tokens-B-KcVAin.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-BMAAx3Uu.js → Trigger-B-Caiptm.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BPeNXpsi.js → Trust-_8sq7pJp.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-A0qabV14.js → UkeySign-CLveUEgo.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BMLfYykd.js → VideoEditing-iVc9jxt9.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BfDI3zjs.js → Wallet-D1n5pidD.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-BCnZEScW.js → WebAuthn-CA8kubXb.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-DvtS5Asf.js → WorkflowEditor-BXWwd_fB.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CDJZtBM7.js → chat-DUNkQr1A.js} +1 -1
- package/src/assets/web-panel/assets/{colors-fbH1Saco.js → colors-Dz5ozTcp.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-CuZFa9l8.js → compact-item-CZ5-JSLh.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CQuPOqqD.js → createContext-CFxlcPug.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-BMRVR8Xp.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-CDZlDrhJ.js → hasIn-CqWIkHJm.js} +1 -1
- package/src/assets/web-panel/assets/{index-BivMeInw.js → index-3elHm6lI.js} +1 -1
- package/src/assets/web-panel/assets/{index-DvBgQoaw.js → index-8l5LLWxH.js} +1 -1
- package/src/assets/web-panel/assets/{index-BJCXJCUA.js → index-BBq1ySIt.js} +1 -1
- package/src/assets/web-panel/assets/{index-CNmJrCxV.js → index-BI2EU3hC.js} +1 -1
- package/src/assets/web-panel/assets/{index-BSy0noke.js → index-BJt6sNTF.js} +1 -1
- package/src/assets/web-panel/assets/{index-oe9ZPRtQ.js → index-BLLSLAC3.js} +1 -1
- package/src/assets/web-panel/assets/{index-C6epsHef.js → index-BLNgGXeg.js} +1 -1
- package/src/assets/web-panel/assets/{index-COSyGm80.js → index-BO-yo7Jv.js} +1 -1
- package/src/assets/web-panel/assets/index-BT2s_zxU.js +1 -0
- package/src/assets/web-panel/assets/{index-DGAK9Dj4.js → index-BiAcVeea.js} +1 -1
- package/src/assets/web-panel/assets/{index-DjgZRX0_.js → index-C2SoMbLc.js} +1 -1
- package/src/assets/web-panel/assets/{index-CQ5FVEji.js → index-C5XUilwu.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bt1j0mjJ.js → index-C5zhjact.js} +1 -1
- package/src/assets/web-panel/assets/{index-BKaue5Pv.js → index-CBeASfAD.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bz5_6E63.js → index-CD7UjlnE.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cr_shi_7.js → index-CDq8IVvv.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dkuecn17.js → index-CE-gpaY9.js} +1 -1
- package/src/assets/web-panel/assets/{index-CkhudJH0.js → index-CL6wt2JN.js} +1 -1
- package/src/assets/web-panel/assets/{index-kGzPbvry.js → index-CLu3Oyef.js} +1 -1
- package/src/assets/web-panel/assets/{index-D365qmj8.js → index-C_WWTpLE.js} +1 -1
- package/src/assets/web-panel/assets/{index-Deqod8La.js → index-CbcHBDYj.js} +1 -1
- package/src/assets/web-panel/assets/{index-DwAiu9LT.js → index-CguUaiiY.js} +1 -1
- package/src/assets/web-panel/assets/{index-WDQkyh-E.js → index-Ci1-8q-g.js} +1 -1
- package/src/assets/web-panel/assets/{index-BNEG9-EK.js → index-CvMZxZOn.js} +1 -1
- package/src/assets/web-panel/assets/{index-CnfR7qmj.js → index-D6Nkerss.js} +1 -1
- package/src/assets/web-panel/assets/{index-CdiNFWPp.js → index-DEzYXMgc.js} +1 -1
- package/src/assets/web-panel/assets/{index--SCX46Az.js → index-DF0hXW5L.js} +1 -1
- package/src/assets/web-panel/assets/{index-BhZkMMey.js → index-DGAjS_D9.js} +1 -1
- package/src/assets/web-panel/assets/{index-CD9ml9ZJ.js → index-DO6mf95c.js} +1 -1
- package/src/assets/web-panel/assets/index-DOTL6NDc.js +1 -0
- package/src/assets/web-panel/assets/{index-DNN-xBWV.js → index-DUyhvh0L.js} +1 -1
- package/src/assets/web-panel/assets/{index-ycBlRXAf.js → index-DW18L-o6.js} +1 -1
- package/src/assets/web-panel/assets/{index-CAzMdnkI.js → index-Dc-5Ulgt.js} +1 -1
- package/src/assets/web-panel/assets/{index-CzeRvhia.js → index-DvUlrMy-.js} +3 -3
- package/src/assets/web-panel/assets/{index-BdORz0iZ.js → index-FsYDYVUk.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmeO_DfK.js → index-GVbsyUQm.js} +1 -1
- package/src/assets/web-panel/assets/{index-BzIDfObk.js → index-noQc_RpT.js} +1 -1
- package/src/assets/web-panel/assets/{index-14gri6Vh.js → index-rR060KAF.js} +1 -1
- package/src/assets/web-panel/assets/{index-DhFyStIG.js → index-xaZX6ZDL.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-DHRWNV-y.js → initDefaultProps-PIetywTX.js} +1 -1
- package/src/assets/web-panel/assets/{motion-MJ2jhdVO.js → motion-gQJEK3wO.js} +1 -1
- package/src/assets/web-panel/assets/{move-Bly0QFE5.js → move-ML1nRxts.js} +1 -1
- package/src/assets/web-panel/assets/{omit-DkoMB0pZ.js → omit-wUWsw3YL.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-9M-gsXIc.js → pickAttrs-A0Wlomih.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-Bnht1xci.js → placementArrow-C5RKYdxT.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-B0Cn1i64.js → responsiveObserve-DIxNVSJl.js} +1 -1
- package/src/assets/web-panel/assets/{slide-BdI4DDyM.js → slide-B-tNesVu.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-DdBktcMD.js → statusUtils-CftzO200.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-aYEwS4Pw.js → styleChecker-CMgvWu90.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-y8cUyKiP.js → useFlexGapSupport-sxqoDNhZ.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-Bk1SrPFp.js → useFs-CowEXz4v.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-Dp04zAB3.js → usePersonalDataHub-6ISRG7V-.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-B52a38TC.js → vnode-C2mnXfmw.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DFwyL43U.js → zoom-DMsur0jx.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +66 -28
- package/src/harness/mcp-client.js +48 -2
- package/src/lib/agent-core.js +2 -0
- package/src/lib/llm-pricing.js +9 -0
- package/src/lib/mcp-client.js +1 -0
- package/src/lib/permission-rules.cjs +11 -1
- package/src/lib/personal-data-hub-wiring.js +24 -0
- package/src/lib/repl-multiline.js +64 -0
- package/src/lib/repl-rewind.js +65 -2
- package/src/lib/repl-vim.js +445 -0
- package/src/lib/safe-mode.js +17 -3
- package/src/lib/skill-loader.js +45 -1
- package/src/lib/slash-commands.js +13 -3
- package/src/lib/status-line.cjs +33 -3
- package/src/repl/agent-repl.js +427 -23
- package/src/repl/config-summary.js +59 -0
- package/src/repl/conversation-export.js +133 -0
- package/src/repl/doctor-status.js +114 -0
- package/src/repl/memory-status.js +45 -0
- package/src/repl/permissions-status.js +51 -0
- package/src/repl/recent-sessions.js +46 -0
- package/src/repl/session-cost.js +216 -0
- package/src/runtime/agent-core.js +23 -8
- package/src/runtime/fallback-model.js +125 -30
- package/src/runtime/headless-runner.js +2 -0
- package/src/runtime/headless-stream.js +2 -0
- package/src/runtime/mcp-config.js +14 -3
- package/src/assets/web-panel/assets/MobileProjects-mAJsyk7U.js +0 -1
- package/src/assets/web-panel/assets/PdhVaultBrowser-6clRu-J6.js +0 -7
- package/src/assets/web-panel/assets/Projects-DO25SEFT.js +0 -1
- package/src/assets/web-panel/assets/devWarning-mhGhHpNs.js +0 -1
- package/src/assets/web-panel/assets/index-DGBjZXvW.js +0 -1
- package/src/assets/web-panel/assets/index-DujMkFuc.js +0 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/config` REPL command (Claude-Code parity) — show the effective
|
|
3
|
+
* configuration in a readable, SECRET-SAFE form: the LLM provider/model/baseURL
|
|
4
|
+
* actually in effect, whether an API key is set (never the key itself), the
|
|
5
|
+
* web-search backend, and the config-file path. Also surfaces the session's
|
|
6
|
+
* active provider/model, which can differ from the file (a --provider flag or
|
|
7
|
+
* .claude/settings.json overrides it).
|
|
8
|
+
*
|
|
9
|
+
* Pure: takes the loaded config + context, returns plain text. The REPL does
|
|
10
|
+
* the I/O. NEVER prints a secret — only "set (…1234)" / "not set".
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Mask a secret: presence + last 4 chars only, or "not set". */
|
|
14
|
+
export function maskSecret(v) {
|
|
15
|
+
if (v == null || v === "") return "not set";
|
|
16
|
+
const s = String(v);
|
|
17
|
+
return s.length <= 4 ? "set (hidden)" : `set (…${s.slice(-4)})`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {object|null} config loaded config.json
|
|
22
|
+
* @param {object} [opts] { path, activeProvider, activeModel }
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
export function renderConfigSummary(config, opts = {}) {
|
|
26
|
+
const cfg = config || {};
|
|
27
|
+
const llm = cfg.llm || {};
|
|
28
|
+
const lines = ["Effective configuration:"];
|
|
29
|
+
if (opts.path) lines.push(` config file: ${opts.path}`);
|
|
30
|
+
|
|
31
|
+
lines.push(" llm:");
|
|
32
|
+
lines.push(` provider: ${llm.provider || "(unset → defaults to ollama)"}`);
|
|
33
|
+
lines.push(` model: ${llm.model || "(unset)"}`);
|
|
34
|
+
if (llm.visionModel) lines.push(` vision: ${llm.visionModel}`);
|
|
35
|
+
if (llm.baseUrl) lines.push(` baseUrl: ${llm.baseUrl}`);
|
|
36
|
+
lines.push(` apiKey: ${maskSecret(llm.apiKey)}`);
|
|
37
|
+
|
|
38
|
+
const ws = cfg.webSearch || {};
|
|
39
|
+
if (ws.provider || ws.apiKey) {
|
|
40
|
+
lines.push(" webSearch:");
|
|
41
|
+
lines.push(` provider: ${ws.provider || "(unset → auto)"}`);
|
|
42
|
+
lines.push(` apiKey: ${maskSecret(ws.apiKey)}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (opts.activeProvider || opts.activeModel) {
|
|
46
|
+
const ap = opts.activeProvider || "?";
|
|
47
|
+
const am = opts.activeModel || "?";
|
|
48
|
+
const differs = !!llm.provider && (ap !== llm.provider || am !== llm.model);
|
|
49
|
+
lines.push(
|
|
50
|
+
` active this session: ${ap} · ${am}` +
|
|
51
|
+
(differs ? " (overrides config)" : ""),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
lines.push(
|
|
56
|
+
" note: keys are hidden; env vars (e.g. *_API_KEY) can override config at runtime.",
|
|
57
|
+
);
|
|
58
|
+
return lines.join("\n");
|
|
59
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/export` REPL command — dump the LIVE in-memory conversation to a Markdown
|
|
3
|
+
* transcript (Claude-Code parity). Distinct from `cc export` (knowledge-base
|
|
4
|
+
* export) and from `cc session export` (which reads the persisted JSONL store):
|
|
5
|
+
* this renders the agent REPL's working `messages` array, so it captures
|
|
6
|
+
* exactly what's in context right now, persisted or not.
|
|
7
|
+
*
|
|
8
|
+
* Pure over the OpenAI-shaped message list the agent loop maintains:
|
|
9
|
+
* {role:"user"|"assistant"|"system", content} content: string | parts[]
|
|
10
|
+
* {role:"assistant", content, tool_calls:[{function:{name,arguments}}]}
|
|
11
|
+
* {role:"tool", content, tool_call_id}
|
|
12
|
+
* The REPL does the file I/O; this only produces text + a default filename.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const TOOL_BLOCK_CAP = 4000;
|
|
16
|
+
|
|
17
|
+
/** Render message content (string, or OpenAI multimodal parts[]) as text. */
|
|
18
|
+
function contentToText(content) {
|
|
19
|
+
if (content == null) return "";
|
|
20
|
+
if (typeof content === "string") return content;
|
|
21
|
+
if (Array.isArray(content)) {
|
|
22
|
+
return content
|
|
23
|
+
.map((part) => {
|
|
24
|
+
if (typeof part === "string") return part;
|
|
25
|
+
if (!part || typeof part !== "object") return "";
|
|
26
|
+
if (part.type === "text" || typeof part.text === "string") {
|
|
27
|
+
return part.text || "";
|
|
28
|
+
}
|
|
29
|
+
if (part.type === "image_url" || part.image_url) return "[image]";
|
|
30
|
+
return "";
|
|
31
|
+
})
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.join("\n");
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
return JSON.stringify(content, null, 2);
|
|
37
|
+
} catch {
|
|
38
|
+
return String(content);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function fence(body, lang = "") {
|
|
43
|
+
const text =
|
|
44
|
+
typeof body === "string"
|
|
45
|
+
? body
|
|
46
|
+
: (() => {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.stringify(body, null, 2);
|
|
49
|
+
} catch {
|
|
50
|
+
return String(body);
|
|
51
|
+
}
|
|
52
|
+
})();
|
|
53
|
+
const capped =
|
|
54
|
+
text.length > TOOL_BLOCK_CAP
|
|
55
|
+
? `${text.slice(0, TOOL_BLOCK_CAP)}\n… [truncated]`
|
|
56
|
+
: text;
|
|
57
|
+
const guard = capped.includes("```") ? "````" : "```";
|
|
58
|
+
return `${guard}${lang}\n${capped}\n${guard}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Pretty-print a tool_call's JSON-string arguments, falling back to raw. */
|
|
62
|
+
function prettyArgs(argStr) {
|
|
63
|
+
if (typeof argStr !== "string") return fence(argStr, "json");
|
|
64
|
+
try {
|
|
65
|
+
return fence(JSON.parse(argStr), "json");
|
|
66
|
+
} catch {
|
|
67
|
+
return fence(argStr);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const two = (n) => String(n).padStart(2, "0");
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A timestamped default filename, e.g. chainlesschain-export-20260613-130600.md.
|
|
75
|
+
* Takes a Date so callers/tests stay deterministic.
|
|
76
|
+
*/
|
|
77
|
+
export function defaultExportFilename(date = new Date()) {
|
|
78
|
+
const d = date instanceof Date && !isNaN(date) ? date : new Date(0);
|
|
79
|
+
const stamp =
|
|
80
|
+
`${d.getFullYear()}${two(d.getMonth() + 1)}${two(d.getDate())}` +
|
|
81
|
+
`-${two(d.getHours())}${two(d.getMinutes())}${two(d.getSeconds())}`;
|
|
82
|
+
return `chainlesschain-export-${stamp}.md`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Render the conversation as Markdown.
|
|
87
|
+
* @param {Array} messages the REPL's working message list
|
|
88
|
+
* @param {object} [meta] { provider, model, sessionId, exportedAt }
|
|
89
|
+
* @returns {string}
|
|
90
|
+
*/
|
|
91
|
+
export function renderConversationMarkdown(messages, meta = {}) {
|
|
92
|
+
const msgs = Array.isArray(messages) ? messages : [];
|
|
93
|
+
const L = ["# ChainlessChain Conversation Export", ""];
|
|
94
|
+
const bits = [];
|
|
95
|
+
if (meta.sessionId) bits.push(`session: ${meta.sessionId}`);
|
|
96
|
+
if (meta.provider) bits.push(`provider: ${meta.provider}`);
|
|
97
|
+
if (meta.model) bits.push(`model: ${meta.model}`);
|
|
98
|
+
if (meta.exportedAt) bits.push(`exported: ${meta.exportedAt}`);
|
|
99
|
+
if (bits.length) {
|
|
100
|
+
L.push(`> ${bits.join(" · ")}`, "");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let users = 0;
|
|
104
|
+
let assistants = 0;
|
|
105
|
+
for (const m of msgs) {
|
|
106
|
+
if (!m || typeof m !== "object") continue;
|
|
107
|
+
const role = m.role;
|
|
108
|
+
if (role === "user") {
|
|
109
|
+
users += 1;
|
|
110
|
+
L.push("## 👤 User", "", contentToText(m.content), "");
|
|
111
|
+
} else if (role === "assistant") {
|
|
112
|
+
const text = contentToText(m.content);
|
|
113
|
+
if (text) {
|
|
114
|
+
assistants += 1;
|
|
115
|
+
L.push("## 🤖 Assistant", "", text, "");
|
|
116
|
+
}
|
|
117
|
+
if (Array.isArray(m.tool_calls)) {
|
|
118
|
+
for (const tc of m.tool_calls) {
|
|
119
|
+
const name = tc?.function?.name || tc?.name || "?";
|
|
120
|
+
L.push(`**🔧 tool_call — \`${name}\`**`, "");
|
|
121
|
+
L.push(prettyArgs(tc?.function?.arguments ?? tc?.arguments), "");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} else if (role === "tool") {
|
|
125
|
+
L.push("**↩ tool_result**", "", fence(contentToText(m.content)), "");
|
|
126
|
+
} else if (role === "system") {
|
|
127
|
+
L.push("## ⚙ System", "", contentToText(m.content), "");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
L.push("---", `_${users} user / ${assistants} assistant turns_`, "");
|
|
132
|
+
return L.join("\n");
|
|
133
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/doctor` REPL command (Claude-Code parity) — a consolidated session-health
|
|
3
|
+
* readout. Where /config, /ide, /mcp, /permissions each show one subsystem,
|
|
4
|
+
* /doctor rolls them into a single pass/warn view and flags the common reasons
|
|
5
|
+
* a chat silently fails (no provider, provider set but no key, …).
|
|
6
|
+
*
|
|
7
|
+
* Pure and dependency-free: `buildDoctorChecks` turns already-resolved session
|
|
8
|
+
* state into a check list, `renderDoctor` formats it. The REPL gathers inputs.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Providers whose models run locally and need no API key. */
|
|
12
|
+
const FREE_PROVIDERS = ["ollama", "local", "llamacpp", "mediapipe"];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {object} input
|
|
16
|
+
* { config, ideTools?: string[], mcpServers?: Array, permissionRules?, settingsHooks? }
|
|
17
|
+
* @returns {Array<{level:'ok'|'warn'|'info'|'err', name:string, detail:string}>}
|
|
18
|
+
*/
|
|
19
|
+
export function buildDoctorChecks(input = {}) {
|
|
20
|
+
const { config, ideTools, mcpServers, permissionRules, settingsHooks } =
|
|
21
|
+
input;
|
|
22
|
+
const llm = (config && config.llm) || {};
|
|
23
|
+
const checks = [];
|
|
24
|
+
|
|
25
|
+
// LLM provider / model
|
|
26
|
+
if (!llm.provider) {
|
|
27
|
+
checks.push({
|
|
28
|
+
level: "warn",
|
|
29
|
+
name: "LLM provider",
|
|
30
|
+
detail:
|
|
31
|
+
"unset → defaults to ollama (local); set llm.provider or --provider",
|
|
32
|
+
});
|
|
33
|
+
} else {
|
|
34
|
+
checks.push({
|
|
35
|
+
level: "ok",
|
|
36
|
+
name: "LLM provider",
|
|
37
|
+
detail: `${llm.provider}${llm.model ? " · " + llm.model : " (no model set)"}`,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// API key for non-local providers
|
|
42
|
+
if (
|
|
43
|
+
llm.provider &&
|
|
44
|
+
!FREE_PROVIDERS.includes(String(llm.provider).toLowerCase()) &&
|
|
45
|
+
!llm.apiKey
|
|
46
|
+
) {
|
|
47
|
+
checks.push({
|
|
48
|
+
level: "warn",
|
|
49
|
+
name: "API key",
|
|
50
|
+
detail: `${llm.provider} has no apiKey set (config llm.apiKey or a *_API_KEY env var)`,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// IDE bridge
|
|
55
|
+
const ideCount = Array.isArray(ideTools) ? ideTools.length : 0;
|
|
56
|
+
checks.push(
|
|
57
|
+
ideCount > 0
|
|
58
|
+
? {
|
|
59
|
+
level: "ok",
|
|
60
|
+
name: "IDE bridge",
|
|
61
|
+
detail: `${ideCount} tools connected`,
|
|
62
|
+
}
|
|
63
|
+
: { level: "info", name: "IDE bridge", detail: "not connected" },
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// MCP servers
|
|
67
|
+
const mcpCount = Array.isArray(mcpServers) ? mcpServers.length : 0;
|
|
68
|
+
checks.push(
|
|
69
|
+
mcpCount > 0
|
|
70
|
+
? { level: "ok", name: "MCP servers", detail: `${mcpCount} connected` }
|
|
71
|
+
: { level: "info", name: "MCP servers", detail: "none" },
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Permission rules
|
|
75
|
+
const ruleCount = permissionRules
|
|
76
|
+
? (permissionRules.allow?.length || 0) +
|
|
77
|
+
(permissionRules.ask?.length || 0) +
|
|
78
|
+
(permissionRules.deny?.length || 0)
|
|
79
|
+
: 0;
|
|
80
|
+
checks.push({
|
|
81
|
+
level: "info",
|
|
82
|
+
name: "Permission rules",
|
|
83
|
+
detail: ruleCount > 0 ? `${ruleCount} rule(s)` : "none (default gate)",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// settings.json hooks
|
|
87
|
+
checks.push({
|
|
88
|
+
level: "info",
|
|
89
|
+
name: "settings.json hooks",
|
|
90
|
+
detail: settingsHooks ? "loaded" : "none",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return checks;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const ICON = { ok: "✓", warn: "⚠", err: "✗", info: "·" };
|
|
97
|
+
|
|
98
|
+
/** Render the check list as a readable health block. */
|
|
99
|
+
export function renderDoctor(checks) {
|
|
100
|
+
const list = Array.isArray(checks) ? checks : [];
|
|
101
|
+
const lines = ["Session health (/doctor):"];
|
|
102
|
+
for (const c of list) {
|
|
103
|
+
lines.push(` ${ICON[c.level] || "·"} ${c.name}: ${c.detail}`);
|
|
104
|
+
}
|
|
105
|
+
const problems = list.filter(
|
|
106
|
+
(c) => c.level === "warn" || c.level === "err",
|
|
107
|
+
).length;
|
|
108
|
+
lines.push(
|
|
109
|
+
problems === 0
|
|
110
|
+
? " no problems detected"
|
|
111
|
+
: ` ${problems} item(s) need attention`,
|
|
112
|
+
);
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/memory` REPL command (Claude-Code parity) — show the project-memory files
|
|
3
|
+
* the agent auto-loads into its system prompt (the cc.md > CLAUDE.md > AGENTS.md
|
|
4
|
+
* hierarchy + @imports + path-scoped rules), so "why does it know/behave like
|
|
5
|
+
* this?" is answerable in-session. Distinct from `#` (append a note to cc.md)
|
|
6
|
+
* and `cc memory recall` (the scoped MemoryStore).
|
|
7
|
+
*
|
|
8
|
+
* Pure: renders a loadProjectInstructions() result. The REPL does the I/O.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {{files?:Array<{path:string,scope:string,bytes?:number,truncated?:boolean}>,warnings?:string[]}} loaded
|
|
13
|
+
* @param {{enabled?:boolean}} [opts] enabled=false when CC_PROJECT_MEMORY=0
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
export function renderMemoryFiles(loaded, { enabled = true } = {}) {
|
|
17
|
+
const files = Array.isArray(loaded?.files) ? loaded.files : [];
|
|
18
|
+
const warnings = Array.isArray(loaded?.warnings) ? loaded.warnings : [];
|
|
19
|
+
const lines = ["Project memory (auto-loaded into the system prompt):"];
|
|
20
|
+
|
|
21
|
+
if (!enabled) {
|
|
22
|
+
lines.push(
|
|
23
|
+
" ⚠ disabled via CC_PROJECT_MEMORY=0 — the files below are NOT loaded this session",
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (files.length === 0) {
|
|
28
|
+
lines.push(" (none found — run `cc init` to generate a cc.md)");
|
|
29
|
+
return lines.join("\n");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let total = 0;
|
|
33
|
+
for (const f of files) {
|
|
34
|
+
const bytes = Number(f.bytes) || 0;
|
|
35
|
+
total += bytes;
|
|
36
|
+
lines.push(
|
|
37
|
+
` [${String(f.scope || "?").padEnd(7)}] ${f.path} ${bytes}B${f.truncated ? " (truncated)" : ""}`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
lines.push(
|
|
41
|
+
` ${files.length} file(s), ${total}B total · precedence cc.md > CLAUDE.md > AGENTS.md`,
|
|
42
|
+
);
|
|
43
|
+
for (const w of warnings) lines.push(` ⚠ ${w}`);
|
|
44
|
+
return lines.join("\n");
|
|
45
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/permissions` REPL command (Claude-Code parity) — show the allow / ask /
|
|
3
|
+
* deny rules in effect for THIS session (loaded from .claude/settings.json by
|
|
4
|
+
* settings-loader, applied by permission-rules in agent-core's executeTool).
|
|
5
|
+
* These rules are otherwise invisible mid-session; surfacing them tells the
|
|
6
|
+
* user exactly what the agent can do unprompted, what it asks about, and what
|
|
7
|
+
* is blocked.
|
|
8
|
+
*
|
|
9
|
+
* Pure: takes the effective rules ({allow,ask,deny}) + the contributing source
|
|
10
|
+
* files, returns plain text. The REPL does the I/O.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
function pushGroup(lines, label, marker, rules) {
|
|
14
|
+
const list = Array.isArray(rules) ? rules : [];
|
|
15
|
+
if (list.length === 0) return;
|
|
16
|
+
lines.push(` ${label}:`);
|
|
17
|
+
for (const r of list) lines.push(` ${marker} ${r}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {{allow?:string[],ask?:string[],deny?:string[]}|null} rules
|
|
22
|
+
* @param {{files?:string[]}} [opts]
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
export function renderPermissions(rules, { files = [] } = {}) {
|
|
26
|
+
const r = rules || { allow: [], ask: [], deny: [] };
|
|
27
|
+
const count =
|
|
28
|
+
(r.allow?.length || 0) + (r.ask?.length || 0) + (r.deny?.length || 0);
|
|
29
|
+
|
|
30
|
+
if (count === 0) {
|
|
31
|
+
return [
|
|
32
|
+
"Permission rules: none configured.",
|
|
33
|
+
" Tools run under the default gate — dangerous shell commands still",
|
|
34
|
+
" always require approval. Add rules in .claude/settings.json or via",
|
|
35
|
+
" `cc permissions add`.",
|
|
36
|
+
].join("\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const lines = ["Permission rules (effective this session):"];
|
|
40
|
+
// Render most-restrictive first to mirror the deny > ask > allow precedence.
|
|
41
|
+
pushGroup(lines, "deny (blocked)", "✗", r.deny);
|
|
42
|
+
pushGroup(lines, "ask (prompt first)", "?", r.ask);
|
|
43
|
+
pushGroup(lines, "allow (auto-approved)", "✓", r.allow);
|
|
44
|
+
if (Array.isArray(files) && files.length) {
|
|
45
|
+
lines.push(` sources: ${files.join(", ")}`);
|
|
46
|
+
}
|
|
47
|
+
lines.push(
|
|
48
|
+
" precedence: deny > ask > allow · dangerous shell commands are always denied",
|
|
49
|
+
);
|
|
50
|
+
return lines.join("\n");
|
|
51
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/sessions` REPL command — list recent RESUMABLE conversations (Claude-Code
|
|
3
|
+
* `/resume` parity, read-only half + mirrors the VS Code panel's /sessions).
|
|
4
|
+
* Where `/session` shows the CURRENT session, this lists past ones (across the
|
|
5
|
+
* DB + JSONL stores, via lib/recent-session.js listRecentSessions) with the
|
|
6
|
+
* ids you can pass to `cc agent --resume <id>`.
|
|
7
|
+
*
|
|
8
|
+
* Pure: renders the listRecentSessions row shape
|
|
9
|
+
* { id, title?, message_count?, updated_at?, _store? }
|
|
10
|
+
* The REPL gathers the rows + supplies the current id.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {Array} sessions listRecentSessions() rows
|
|
15
|
+
* @param {{currentId?:string, limit?:number}} [opts]
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
export function renderRecentSessions(sessions, opts = {}) {
|
|
19
|
+
const limit = opts.limit || 15;
|
|
20
|
+
const list = Array.isArray(sessions) ? sessions : [];
|
|
21
|
+
if (list.length === 0) {
|
|
22
|
+
return "No recent sessions found. Start chatting, or resume one later with `cc agent --resume <id>`.";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const lines = ["Recent sessions (resume with `cc agent --resume <id>`):"];
|
|
26
|
+
for (const s of list.slice(0, limit)) {
|
|
27
|
+
const id = String(s?.id || "");
|
|
28
|
+
if (!id) continue;
|
|
29
|
+
const current =
|
|
30
|
+
opts.currentId && id === opts.currentId ? " ← current" : "";
|
|
31
|
+
const store = s._store ? `[${s._store}]` : "";
|
|
32
|
+
const msgs = Number.isFinite(s.message_count)
|
|
33
|
+
? `${s.message_count} msgs`
|
|
34
|
+
: "";
|
|
35
|
+
const when = s.updated_at
|
|
36
|
+
? String(s.updated_at).slice(0, 19).replace("T", " ")
|
|
37
|
+
: "";
|
|
38
|
+
const title = s.title && s.title !== "Untitled" ? ` — ${s.title}` : "";
|
|
39
|
+
const meta = [store, msgs, when].filter(Boolean).join(" · ");
|
|
40
|
+
lines.push(` ${id.slice(0, 12)}${current} ${meta}${title}`);
|
|
41
|
+
}
|
|
42
|
+
if (list.length > limit) {
|
|
43
|
+
lines.push(` … +${list.length - limit} more`);
|
|
44
|
+
}
|
|
45
|
+
return lines.join("\n");
|
|
46
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/cost` REPL command — running token spend + estimated $ for the LIVE agent
|
|
3
|
+
* session (Claude-Code parity). Where `cc cost` reads persisted JSONL usage,
|
|
4
|
+
* this accumulates each turn's usage events IN MEMORY, so it reflects exactly
|
|
5
|
+
* this conversation and works even for anonymous (non-persisted) sessions.
|
|
6
|
+
*
|
|
7
|
+
* Pure and dependency-light (only the shared pricing lib); the REPL feeds it
|
|
8
|
+
* usage events and prints the rendered string.
|
|
9
|
+
*/
|
|
10
|
+
import { priceRollup, mergePricing } from "../lib/llm-pricing.js";
|
|
11
|
+
|
|
12
|
+
function num(n) {
|
|
13
|
+
const v = Number(n);
|
|
14
|
+
return Number.isFinite(v) ? v : 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fmtUsd(n) {
|
|
18
|
+
const v = Number(n) || 0;
|
|
19
|
+
if (v === 0) return "$0.00";
|
|
20
|
+
if (v < 0.01) return `$${v.toFixed(6)}`;
|
|
21
|
+
return `$${v.toFixed(4)}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A fresh accumulator: running totals + per provider/model rows. */
|
|
25
|
+
export function newCostStore() {
|
|
26
|
+
return {
|
|
27
|
+
total: { inputTokens: 0, outputTokens: 0, totalTokens: 0, calls: 0 },
|
|
28
|
+
byKey: new Map(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Fold the REPL's per-turn usage events ([{ provider, model, usage }]) into the
|
|
34
|
+
* store. Each `usage` is the raw LLM usage object (input_tokens/output_tokens/…
|
|
35
|
+
* with provider-variant aliases). Zero-token events are ignored. Mutates and
|
|
36
|
+
* returns the store.
|
|
37
|
+
*/
|
|
38
|
+
export function addUsage(store, events) {
|
|
39
|
+
const s = store || newCostStore();
|
|
40
|
+
for (const ev of Array.isArray(events) ? events : []) {
|
|
41
|
+
const u = ev && ev.usage ? ev.usage : null;
|
|
42
|
+
if (!u) continue;
|
|
43
|
+
const inT = num(u.input_tokens ?? u.prompt_tokens ?? u.inputTokens);
|
|
44
|
+
const outT = num(u.output_tokens ?? u.completion_tokens ?? u.outputTokens);
|
|
45
|
+
const totT = num(u.total_tokens ?? u.totalTokens ?? inT + outT);
|
|
46
|
+
if (inT === 0 && outT === 0 && totT === 0) continue;
|
|
47
|
+
s.total.inputTokens += inT;
|
|
48
|
+
s.total.outputTokens += outT;
|
|
49
|
+
s.total.totalTokens += totT;
|
|
50
|
+
s.total.calls += 1;
|
|
51
|
+
const key = `${ev.provider || "?"}/${ev.model || "?"}`;
|
|
52
|
+
let row = s.byKey.get(key);
|
|
53
|
+
if (!row) {
|
|
54
|
+
row = {
|
|
55
|
+
provider: ev.provider || null,
|
|
56
|
+
model: ev.model || null,
|
|
57
|
+
inputTokens: 0,
|
|
58
|
+
outputTokens: 0,
|
|
59
|
+
totalTokens: 0,
|
|
60
|
+
calls: 0,
|
|
61
|
+
};
|
|
62
|
+
s.byKey.set(key, row);
|
|
63
|
+
}
|
|
64
|
+
row.inputTokens += inT;
|
|
65
|
+
row.outputTokens += outT;
|
|
66
|
+
row.totalTokens += totT;
|
|
67
|
+
row.calls += 1;
|
|
68
|
+
}
|
|
69
|
+
return s;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Classify a priced model row into a cost CATEGORY by its role in the session
|
|
74
|
+
* (Claude-Code `/cost` breakdown parity). Roles are derived from config + the
|
|
75
|
+
* live session: the active model is "main", the configured vision model is
|
|
76
|
+
* "vision", any model in the fallback chain is "fallback", anything else (e.g.
|
|
77
|
+
* a model switched to mid-session) is "other".
|
|
78
|
+
*
|
|
79
|
+
* The active model wins over vision/fallback when names collide, so a session
|
|
80
|
+
* that never used vision shows everything as "main".
|
|
81
|
+
*
|
|
82
|
+
* @param {string} provider
|
|
83
|
+
* @param {string} model
|
|
84
|
+
* @param {{mainProvider?:string, mainModel?:string, visionModel?:string, fallbackModels?:string[]}} roles
|
|
85
|
+
* @returns {"main"|"vision"|"fallback"|"other"}
|
|
86
|
+
*/
|
|
87
|
+
export function classifyModelRole(provider, model, roles = {}) {
|
|
88
|
+
const lc = (x) => String(x || "").toLowerCase();
|
|
89
|
+
const p = lc(provider);
|
|
90
|
+
const m = lc(model);
|
|
91
|
+
if (
|
|
92
|
+
roles.mainModel &&
|
|
93
|
+
m === lc(roles.mainModel) &&
|
|
94
|
+
(!roles.mainProvider || p === lc(roles.mainProvider))
|
|
95
|
+
) {
|
|
96
|
+
return "main";
|
|
97
|
+
}
|
|
98
|
+
if (roles.visionModel && m === lc(roles.visionModel)) return "vision";
|
|
99
|
+
if (
|
|
100
|
+
Array.isArray(roles.fallbackModels) &&
|
|
101
|
+
roles.fallbackModels.some((fm) => lc(fm) === m)
|
|
102
|
+
) {
|
|
103
|
+
return "fallback";
|
|
104
|
+
}
|
|
105
|
+
return "other";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Group a priced rollup's per-model rows into categories. Operates on the output
|
|
110
|
+
* of priceRollup (rows carry cost/matched/free), so dollar sums are accurate and
|
|
111
|
+
* unpriced models are flagged rather than silently counted as $0.
|
|
112
|
+
*
|
|
113
|
+
* @returns {Array<{category,inputTokens,outputTokens,totalTokens,calls,cost,models,anyUnpriced}>}
|
|
114
|
+
* sorted by cost (then tokens) descending.
|
|
115
|
+
*/
|
|
116
|
+
export function categorizeByRole(pricedResult, roles = {}) {
|
|
117
|
+
const cats = new Map();
|
|
118
|
+
for (const row of pricedResult?.byModel || []) {
|
|
119
|
+
const category = classifyModelRole(row.provider, row.model, roles);
|
|
120
|
+
let c = cats.get(category);
|
|
121
|
+
if (!c) {
|
|
122
|
+
c = {
|
|
123
|
+
category,
|
|
124
|
+
inputTokens: 0,
|
|
125
|
+
outputTokens: 0,
|
|
126
|
+
totalTokens: 0,
|
|
127
|
+
calls: 0,
|
|
128
|
+
cost: 0,
|
|
129
|
+
models: [],
|
|
130
|
+
anyUnpriced: false,
|
|
131
|
+
};
|
|
132
|
+
cats.set(category, c);
|
|
133
|
+
}
|
|
134
|
+
c.inputTokens += num(row.inputTokens);
|
|
135
|
+
c.outputTokens += num(row.outputTokens);
|
|
136
|
+
c.totalTokens += num(row.totalTokens);
|
|
137
|
+
c.calls += num(row.calls);
|
|
138
|
+
if (row.matched && !row.free) c.cost += num(row.cost);
|
|
139
|
+
if (!row.matched && !row.free) c.anyUnpriced = true;
|
|
140
|
+
if (row.model && !c.models.includes(row.model)) c.models.push(row.model);
|
|
141
|
+
}
|
|
142
|
+
return Array.from(cats.values()).sort(
|
|
143
|
+
(a, b) => b.cost - a.cost || b.totalTokens - a.totalTokens,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Snapshot the store as the `{ total, byModel[] }` aggregate priceRollup wants. */
|
|
148
|
+
export function costAggregate(store) {
|
|
149
|
+
const s = store || newCostStore();
|
|
150
|
+
return {
|
|
151
|
+
total: { ...s.total },
|
|
152
|
+
byModel: Array.from(s.byKey.values()).sort(
|
|
153
|
+
(a, b) => b.totalTokens - a.totalTokens,
|
|
154
|
+
),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Render the live session cost. `pricingOverrides` is typically
|
|
160
|
+
* `config.llm.pricing`. Returns plain text (the REPL does the I/O).
|
|
161
|
+
*/
|
|
162
|
+
export function renderSessionCost(store, { pricingOverrides, roles } = {}) {
|
|
163
|
+
const agg = costAggregate(store);
|
|
164
|
+
if (agg.total.calls === 0) {
|
|
165
|
+
return "Session cost: no LLM calls yet this session.";
|
|
166
|
+
}
|
|
167
|
+
const table = mergePricing(pricingOverrides);
|
|
168
|
+
const result = priceRollup(agg, { table });
|
|
169
|
+
const lines = [];
|
|
170
|
+
lines.push("Session cost (estimated):");
|
|
171
|
+
lines.push(
|
|
172
|
+
` total: ${fmtUsd(result.cost.totalCost)} USD ` +
|
|
173
|
+
`(${result.total.totalTokens.toLocaleString()} tokens, ${result.total.calls} calls)`,
|
|
174
|
+
);
|
|
175
|
+
for (const row of result.byModel) {
|
|
176
|
+
const provider = (row.provider || "?").padEnd(10);
|
|
177
|
+
const model = (row.model || "?").padEnd(24);
|
|
178
|
+
const tokens = `in=${row.inputTokens} out=${row.outputTokens}`;
|
|
179
|
+
const price = row.free
|
|
180
|
+
? "free (local)"
|
|
181
|
+
: row.matched
|
|
182
|
+
? fmtUsd(row.cost)
|
|
183
|
+
: "unpriced";
|
|
184
|
+
lines.push(` ${provider} ${model} ${price} ${tokens}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Category breakdown (main / vision / fallback / other) — only worth showing
|
|
188
|
+
// when more than one category was actually used; a single-model session is
|
|
189
|
+
// already fully described by the per-model rows above.
|
|
190
|
+
if (roles) {
|
|
191
|
+
const cats = categorizeByRole(result, roles);
|
|
192
|
+
if (cats.length >= 2) {
|
|
193
|
+
const totalCost = num(result.cost.totalCost);
|
|
194
|
+
lines.push(" by category:");
|
|
195
|
+
for (const c of cats) {
|
|
196
|
+
const label = c.category.padEnd(9);
|
|
197
|
+
const price =
|
|
198
|
+
c.anyUnpriced && c.cost === 0 ? "unpriced" : fmtUsd(c.cost);
|
|
199
|
+
const pct =
|
|
200
|
+
totalCost > 0 ? ` (${Math.round((c.cost / totalCost) * 100)}%)` : "";
|
|
201
|
+
lines.push(
|
|
202
|
+
` ${label} ${price}${pct} in=${c.inputTokens} out=${c.outputTokens} ${c.calls} calls`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (result.unpriced.length > 0) {
|
|
209
|
+
lines.push(
|
|
210
|
+
` note: ${result.unpriced.length} model(s) have no rate — ` +
|
|
211
|
+
"tokens excluded from total. Add rates via config: llm.pricing.",
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
lines.push(" prices are estimates of public list rates (USD/1M tokens).");
|
|
215
|
+
return lines.join("\n");
|
|
216
|
+
}
|