chainlesschain 0.162.33 → 0.162.35
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/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-3TazCYWE.js → AIOps-CJn02U42.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-DUPN0PST.js → ActionButton-ewURAAoy.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CemvhkzD.js → Analytics-BiSadESb.js} +2 -2
- package/src/assets/web-panel/assets/{AppLayout-BL_tAU3M.js → AppLayout-BR0WOEug.js} +4 -4
- package/src/assets/web-panel/assets/{Audit-Dl9l-cxF.js → Audit-CrqcYx0e.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BKDDX75m.js → Backup-DtbSBn4e.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CDYePvMI.js → BaseInput-BjSc9j0o.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CGtR0sg3.js → Chat-ixzrlCJE.js} +4 -4
- package/src/assets/web-panel/assets/ChatBubbleRenderer-B78nEq05.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-CwYIHOOo.js → Checkbox-UGYeSsgr.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-CIF5tbtd.js → Codegen-B97OOAg4.js} +1 -1
- package/src/assets/web-panel/assets/{Col-z7d4kxeP.js → Col-D9aGkaZ6.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DUlDrqF7.js → Community-Dc2v2RGS.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CJ1o8QQR.js → Compact-B_FYlUQR.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-D3i9d_uO.js → Compliance-C4FiTHyC.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Wm7JTkfB.js → Cowork-CQ8j3LIg.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-B0QnHhZx.js → Cron-Dzjs9Z9Z.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-3yPrnNgd.js → Crosschain-BXI24uzI.js} +1 -1
- package/src/assets/web-panel/assets/{DID-cfdkiDWF.js → DID-C-I4_d07.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-DFkgM4gT.js → Dashboard-BzzGh5mo.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-YYWE81DL.js → Dropdown-Bh8H70De.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-BXfHK1Bn.js → EmailListRenderer-DI_qybJP.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-DInUxJ2G.js → FamilyGuardDashboard-DkKTsfc4.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DNUYeFsv.js → Federation-DS7CmvVG.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Cr7eVEBB.js → FormItemContext-CI97WsB5.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-_gF4cmDa.js → GenericCardRenderer-Da27EdR4.js} +1 -1
- package/src/assets/web-panel/assets/{Git-BqldmUbO.js → Git-CEh0gR2W.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BF59ZiQ8.js → Governance-kIr3tls2.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-Cy7y1eb9.js → Inference-CC1GzyC1.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-B3fVocTO.js → KnowledgeGraph-BNgTiWOB.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BDirsUVk.js → Logs-B2P10gB1.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-GhXpZgp2.js → Marketplace-HPfBvbFZ.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-0VvfIhKx.js → McpTools-ByYotSKb.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-CJLBgAUT.js → Memory-BGIAzFVS.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BMedY9Yg.js → MobileBridge-CroNYTAH.js} +2 -2
- package/src/assets/web-panel/assets/MobileProjects-CH-qnGEV.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-CgEuUg0g.js → Mtc-BqhyIwo9.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-1pWNe_xi.js → MtcAudit-BpEKOvx9.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-DPIQ7oZL.js → Multisig-DST1d_Qo.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-W__P_P4Z.js → NLProgramming-DlMsZcK_.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-C_MCDhFk.js → Notes-C734UJvD.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-CDFotapL.js → NotificationSettings-C0-pPxvk.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-Dtht0cEs.js → OrderTableRenderer-C7zT9eFc.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-D6lMumhD.js → Organization-C5iHC_yW.js} +2 -2
- package/src/assets/web-panel/assets/{Overflow-BMOvUMW6.js → Overflow-CovuHHVR.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-DsQTEw1t.js → P2P-Dx9QL-Gy.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-CncRtN1Z.js → PdhVaultBrowser-IP1dEt6-.js} +4 -4
- package/src/assets/web-panel/assets/{Permissions-DDC-DkUl.js → Permissions-BrR1XZG5.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-DVKY_NnT.js → PersonalDataHub-BgqxVE5m.js} +4 -4
- package/src/assets/web-panel/assets/{Pipeline-C7oDVTl-.js → Pipeline-DzMk5HAz.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-DReGvTEJ.js → Privacy-CDoLa6tk.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-C-j2dzxJ.js → ProjectInit-Dy5gc6ve.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-DcUsvFnc.js → ProjectSettings-DXy-k4hG.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-jSjWnmr6.js → Projects-DvsaEbZR.js} +1 -1
- package/src/assets/web-panel/assets/Providers-Demck9PO.js +1 -0
- package/src/assets/web-panel/assets/{QuickAsk-DdvLtpEU.js → QuickAsk-B8KEHCnd.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DPAi2zo3.js → Recommend-DNVHGYYZ.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-DJD7qXSI.js → Reputation-CaDhWP03.js} +1 -1
- package/src/assets/web-panel/assets/{Row-XERdPDHk.js → Row-CrGLI02x.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-Cl_VlCLg.js → RssFeed-BX7P8I6i.js} +3 -3
- package/src/assets/web-panel/assets/Search-laS6rz8M.js +1 -0
- package/src/assets/web-panel/assets/{Security-DjjCrw8v.js → Security-B6J7IFc1.js} +2 -2
- package/src/assets/web-panel/assets/{Services-BuWeB4YJ.js → Services-vvdcO3mM.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-VZXOKwC_.js → Skeleton-BoAoPTzZ.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-B76ONTfP.js → Skills-CyIQV5b3.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-DIj1KREq.js → Sla-BAQVgdZV.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-BrAp3Yk3.js → SpeechSettings-Bxcn1Jkj.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings--mJcpccF.js → SyncSettings-Dpaj3hDM.js} +2 -2
- package/src/assets/web-panel/assets/Tasks-Bwqo89En.js +1 -0
- package/src/assets/web-panel/assets/{Templates-kOBK6m1Z.js → Templates-Bowcqifn.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BjSzYPzn.js → Tenant-DOkf85uG.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-DwpY-Ay7.js → Terminal-v4MM9dCj.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-aoI0DazM.js → TimelineRenderer-B9A3zDXA.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-YwE0LqSZ.js → Tokens-jtVVqKFr.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-CwSKzvlX.js → Trigger-26Iw-iIl.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-B__Jqdzn.js → Trust-DqY5ORrH.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-mty0jwmx.js → UkeySign-BFsbr3y7.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-Ddsx_OQ6.js → VideoEditing-BtDbj3oa.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-D4Q8yXZm.js → Wallet-BAwmwHbk.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-CLUaKUr5.js → WebAuthn-DINJTsfq.js} +4 -4
- package/src/assets/web-panel/assets/{WorkflowEditor-Di5pOaeC.js → WorkflowEditor-BEorm8SK.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CELatHkT.js → chat-CE39-Dxg.js} +1 -1
- package/src/assets/web-panel/assets/{colors-CawDLjXV.js → colors-C_cLZ93a.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DeMp-K0j.js → compact-item-BSioWA2c.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-zY9kXivd.js → createContext-CGTk4mhN.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-PObcVnJR.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-VEBMW8E4.js → hasIn-Dl1fRwS_.js} +1 -1
- package/src/assets/web-panel/assets/{index-CHqvj9uz.js → index--SWvw6yW.js} +1 -1
- package/src/assets/web-panel/assets/{index-S9JZDSaa.js → index-9_mmaR42.js} +1 -1
- package/src/assets/web-panel/assets/{index-XFyv3Sg_.js → index-B016Fsqr.js} +3 -3
- package/src/assets/web-panel/assets/{index-BqJ2r12F.js → index-B2QiUEgK.js} +1 -1
- package/src/assets/web-panel/assets/{index-CtLZammH.js → index-BAhinBPR.js} +1 -1
- package/src/assets/web-panel/assets/index-BNwIzLyX.js +1 -0
- package/src/assets/web-panel/assets/{index-Cr7lnIeI.js → index-BWpfxzVm.js} +1 -1
- package/src/assets/web-panel/assets/{index-DXNe_zIP.js → index-BhqOTuMW.js} +1 -1
- package/src/assets/web-panel/assets/{index-GRNVdvoA.js → index-BmbVyhk1.js} +1 -1
- package/src/assets/web-panel/assets/{index-BFc0vBN9.js → index-BnEPB1Mz.js} +1 -1
- package/src/assets/web-panel/assets/{index-DSiL_W2n.js → index-Bvi14vJ7.js} +1 -1
- package/src/assets/web-panel/assets/{index-KcOEkUCM.js → index-C-VVk1Jg.js} +1 -1
- package/src/assets/web-panel/assets/{index-BfGGKoo8.js → index-C2ly7sCw.js} +1 -1
- package/src/assets/web-panel/assets/{index-fBNVDEf2.js → index-C4JXchTG.js} +1 -1
- package/src/assets/web-panel/assets/{index-vaD1iHg5.js → index-C9tq8Da8.js} +1 -1
- package/src/assets/web-panel/assets/{index-CyeYs7SG.js → index-CA6K7lZB.js} +1 -1
- package/src/assets/web-panel/assets/{index-BVkrfyuk.js → index-CAwVwBOL.js} +1 -1
- package/src/assets/web-panel/assets/{index-C0GhuYLk.js → index-CST381Qf.js} +1 -1
- package/src/assets/web-panel/assets/{index-SrQIPYq8.js → index-CbXnyoSO.js} +1 -1
- package/src/assets/web-panel/assets/{index-CHxHLv2b.js → index-Ceo9P9tQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-CtoauqWt.js → index-ChwpS1f0.js} +1 -1
- package/src/assets/web-panel/assets/{index-V3K9gvKR.js → index-CisXVbSt.js} +1 -1
- package/src/assets/web-panel/assets/{index-DALuVdhu.js → index-Cxw3p73X.js} +1 -1
- package/src/assets/web-panel/assets/{index-DhsfyHcg.js → index-D9D4q-qI.js} +1 -1
- package/src/assets/web-panel/assets/{index-b3ZuAreb.js → index-DDQx2YFc.js} +1 -1
- package/src/assets/web-panel/assets/{index-B8AZpx7d.js → index-DJyeeygd.js} +1 -1
- package/src/assets/web-panel/assets/{index-u_1aiNTA.js → index-DKEipmR8.js} +1 -1
- package/src/assets/web-panel/assets/{index-DClGYjBM.js → index-DTKEXyaW.js} +1 -1
- package/src/assets/web-panel/assets/{index-CbJZzK9B.js → index-DTpCUi0m.js} +1 -1
- package/src/assets/web-panel/assets/index-Dh6FxR9B.js +1 -0
- package/src/assets/web-panel/assets/{index-DPHe9NYG.js → index-Di6nvW1N.js} +1 -1
- package/src/assets/web-panel/assets/{index-8kqE_cVD.js → index-DrWERr8C.js} +1 -1
- package/src/assets/web-panel/assets/{index-CWbbB1MI.js → index-Ds2RzRG0.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dna2psGz.js → index-II3JhQu2.js} +1 -1
- package/src/assets/web-panel/assets/{index-BjctklSd.js → index-OCxo0X6J.js} +1 -1
- package/src/assets/web-panel/assets/{index-TfXODan7.js → index-Qj2x55mz.js} +1 -1
- package/src/assets/web-panel/assets/{index-JseP3-5X.js → index-hv4jUdG3.js} +1 -1
- package/src/assets/web-panel/assets/{index-VJnHvkv2.js → index-iiZfONfx.js} +1 -1
- package/src/assets/web-panel/assets/{index-CfZV3FXN.js → index-pngH1and.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-Sd7Eayz4.js → initDefaultProps-GOhLA2-f.js} +1 -1
- package/src/assets/web-panel/assets/{motion-DlToY72q.js → motion-jqxFzHTx.js} +1 -1
- package/src/assets/web-panel/assets/{move-DvS7EmAP.js → move-CSLsp6TA.js} +1 -1
- package/src/assets/web-panel/assets/{omit-CzLq4QKW.js → omit-Cnlrb25c.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-BcM75Jx_.js → pickAttrs-CLqlxWWD.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-B7xXXiwd.js → placementArrow-BAWIWtul.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-CrYPRB-g.js → responsiveObserve-CSR1DayS.js} +1 -1
- package/src/assets/web-panel/assets/{slide-CSYTtsRt.js → slide-CNhoPJOp.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-CeSuOVT_.js → statusUtils-BZiYHRHW.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-KiQethca.js → styleChecker-BMoY-Fm5.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-CSQnQdiv.js → useFlexGapSupport-DhtNdlaS.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-Br8Kr1pr.js → useFs-DNPtDOZ4.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-DGJtDcMm.js → usePersonalDataHub-DTdjNvAI.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-C-jVtGka.js → vnode-C9zW9IJ2.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CeWySTPF.js → zoom-D-6RYJJr.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +19 -0
- package/src/commands/agents.js +193 -0
- package/src/commands/command.js +187 -0
- package/src/commands/context.js +189 -0
- package/src/index.js +6 -0
- package/src/lib/agents.js +142 -0
- package/src/lib/slash-commands.js +201 -0
- package/src/repl/agent-repl.js +42 -1
- package/src/runtime/agent-core.js +253 -0
- package/src/runtime/headless-runner.js +30 -1
- package/src/runtime/headless-stream.js +65 -0
- package/src/runtime/mcp-config.js +100 -0
- package/src/runtime/policies/agent-policy.js +1 -0
- package/src/assets/web-panel/assets/ChatBubbleRenderer-DZjc9uKn.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-mdohgRlL.js +0 -1
- package/src/assets/web-panel/assets/Providers-DIpohWG5.js +0 -1
- package/src/assets/web-panel/assets/Search-C-poG9P5.js +0 -1
- package/src/assets/web-panel/assets/Tasks-DM8cMr83.js +0 -1
- package/src/assets/web-panel/assets/devWarning-zLjV7g6r.js +0 -1
- package/src/assets/web-panel/assets/index-CDtUWCtX.js +0 -1
- package/src/assets/web-panel/assets/index-d_RPqH7u.js +0 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agents — user-defined subagent definitions (Claude-Code parity).
|
|
3
|
+
*
|
|
4
|
+
* Markdown files under `.claude/agents/` (project) or `~/.claude/agents/`
|
|
5
|
+
* (personal) define named subagents. Each file's body IS the subagent's system
|
|
6
|
+
* prompt; frontmatter declares its metadata. Mirrors `.claude/commands/` (see
|
|
7
|
+
* slash-commands.js) but for *agents* rather than prompt macros — a file
|
|
8
|
+
* `review/security.md` is the agent `review:security`.
|
|
9
|
+
*
|
|
10
|
+
* Frontmatter (all optional):
|
|
11
|
+
* name override the filename-derived name
|
|
12
|
+
* description one-line summary (when to use this agent)
|
|
13
|
+
* tools allow-list — comma string or YAML array; omit = inherit all
|
|
14
|
+
* model model override for runs of this agent
|
|
15
|
+
*
|
|
16
|
+
* Project scope shadows personal on a name clash. Discovery + parse are pure
|
|
17
|
+
* (inject fs/path/home) so the whole thing is unit-testable.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fsDefault from "node:fs";
|
|
21
|
+
import pathDefault from "node:path";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import yaml from "js-yaml";
|
|
24
|
+
|
|
25
|
+
const _deps = { fs: fsDefault, path: pathDefault };
|
|
26
|
+
|
|
27
|
+
/** Split `--- ... ---` YAML frontmatter from the body, camelCasing keys. */
|
|
28
|
+
function parseFrontmatter(content) {
|
|
29
|
+
const text = String(content || "");
|
|
30
|
+
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
31
|
+
if (!m) return { data: {}, body: text.trim() };
|
|
32
|
+
let raw = {};
|
|
33
|
+
try {
|
|
34
|
+
raw = yaml.load(m[1]) || {};
|
|
35
|
+
} catch {
|
|
36
|
+
raw = {};
|
|
37
|
+
}
|
|
38
|
+
const data = {};
|
|
39
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
40
|
+
const camel = k.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
41
|
+
data[camel] = v;
|
|
42
|
+
}
|
|
43
|
+
return { data, body: (m[2] || "").trim() };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Normalize `tools` (comma string | array | null) into a string[] or null. */
|
|
47
|
+
export function normalizeTools(tools) {
|
|
48
|
+
if (tools == null) return null;
|
|
49
|
+
const list = Array.isArray(tools)
|
|
50
|
+
? tools
|
|
51
|
+
: String(tools).split(/[,\s]+/);
|
|
52
|
+
const out = list.map((t) => String(t).trim()).filter(Boolean);
|
|
53
|
+
return out.length > 0 ? out : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Directories scanned for agent files — project first (shadows personal). */
|
|
57
|
+
export function agentDirs(cwd = process.cwd(), opts = {}) {
|
|
58
|
+
const path = opts.deps?.path || _deps.path;
|
|
59
|
+
const home = opts.home || homedir();
|
|
60
|
+
return [
|
|
61
|
+
{ dir: path.join(cwd, ".claude", "agents"), scope: "project" },
|
|
62
|
+
{ dir: path.join(home, ".claude", "agents"), scope: "personal" },
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Recursively collect `*.md` files under `dir` as `{file, rel}` (rel uses /). */
|
|
67
|
+
function walkMd(dir, { fs, path }, base = dir, acc = []) {
|
|
68
|
+
let entries;
|
|
69
|
+
try {
|
|
70
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
71
|
+
} catch {
|
|
72
|
+
return acc;
|
|
73
|
+
}
|
|
74
|
+
for (const e of entries) {
|
|
75
|
+
const full = path.join(dir, e.name);
|
|
76
|
+
if (e.isDirectory()) {
|
|
77
|
+
walkMd(full, { fs, path }, base, acc);
|
|
78
|
+
} else if (e.isFile() && e.name.endsWith(".md")) {
|
|
79
|
+
const rel = path.relative(base, full).replace(/\\/g, "/");
|
|
80
|
+
acc.push({ file: full, rel });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return acc;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Agent name from a relative path: `review/security.md` → `review:security`. */
|
|
87
|
+
function nameFromRel(rel) {
|
|
88
|
+
return rel.replace(/\.md$/, "").replace(/\//g, ":");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Parse one agent file into its metadata + system prompt (the body). */
|
|
92
|
+
export function parseAgentFile(file, scope, opts = {}) {
|
|
93
|
+
const fs = opts.deps?.fs || _deps.fs;
|
|
94
|
+
let content;
|
|
95
|
+
try {
|
|
96
|
+
content = fs.readFileSync(file, "utf-8");
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
const { data, body } = parseFrontmatter(content);
|
|
101
|
+
return {
|
|
102
|
+
file,
|
|
103
|
+
scope,
|
|
104
|
+
name: data.name || null, // resolved against the path in discoverAgents
|
|
105
|
+
description: data.description || "",
|
|
106
|
+
tools: normalizeTools(data.tools),
|
|
107
|
+
model: data.model || null,
|
|
108
|
+
systemPrompt: body || "",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Discover all agents across both scopes. Project shadows personal by name.
|
|
114
|
+
* @returns {Array<{name, scope, file, description, tools, model, systemPrompt}>}
|
|
115
|
+
*/
|
|
116
|
+
export function discoverAgents(cwd = process.cwd(), opts = {}) {
|
|
117
|
+
const fs = opts.deps?.fs || _deps.fs;
|
|
118
|
+
const path = opts.deps?.path || _deps.path;
|
|
119
|
+
const byName = new Map();
|
|
120
|
+
// Personal first, then project — so project overwrites on clash.
|
|
121
|
+
const dirs = agentDirs(cwd, opts).reverse();
|
|
122
|
+
for (const { dir, scope } of dirs) {
|
|
123
|
+
for (const { file, rel } of walkMd(dir, { fs, path })) {
|
|
124
|
+
const meta = parseAgentFile(file, scope, opts);
|
|
125
|
+
if (!meta) continue;
|
|
126
|
+
// Explicit frontmatter `name` wins; else derive from the path.
|
|
127
|
+
const name = meta.name || nameFromRel(rel);
|
|
128
|
+
byName.set(name, { ...meta, name });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Look up one agent by name (accepts `review:security` or `review/security`). */
|
|
135
|
+
export function getAgent(name, cwd = process.cwd(), opts = {}) {
|
|
136
|
+
const wanted = String(name || "")
|
|
137
|
+
.replace(/^\//, "")
|
|
138
|
+
.replace(/\//g, ":");
|
|
139
|
+
return discoverAgents(cwd, opts).find((a) => a.name === wanted) || null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export { _deps };
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* slash-commands — user-defined command templates (Claude-Code parity).
|
|
3
|
+
*
|
|
4
|
+
* Markdown files under `.claude/commands/` (project) or `~/.claude/commands/`
|
|
5
|
+
* (personal) become reusable command macros. Distinct from skills (skills are
|
|
6
|
+
* AI-invoked capability bundles; these are user-authored prompt macros you run
|
|
7
|
+
* explicitly). A file `git/commit.md` is the command `git:commit`.
|
|
8
|
+
*
|
|
9
|
+
* Frontmatter (all optional): `description`, `argument-hint`, `allowed-tools`,
|
|
10
|
+
* `model`. Body is the prompt template, with substitutions applied at run time:
|
|
11
|
+
* $ARGUMENTS → all args joined by space
|
|
12
|
+
* $1 $2 … $9 → positional args (missing → empty string)
|
|
13
|
+
* !`<cmd>` → run <cmd> in a shell, splice in its stdout (bang exec)
|
|
14
|
+
* @path → splice in file/dir contents (via file-ref-expander)
|
|
15
|
+
*
|
|
16
|
+
* Project scope shadows personal scope on a name clash. Discovery + parse +
|
|
17
|
+
* expand are pure (inject fs/exec/cwd) so the whole thing is unit-testable.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fsDefault from "node:fs";
|
|
21
|
+
import pathDefault from "node:path";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { execSync as execSyncDefault } from "node:child_process";
|
|
24
|
+
import { expandFileRefs } from "../runtime/file-ref-expander.js";
|
|
25
|
+
|
|
26
|
+
const _deps = { fs: fsDefault, path: pathDefault, execSync: execSyncDefault };
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Split `--- ... ---` frontmatter from the body and camelCase the keys (so
|
|
30
|
+
* `argument-hint` → `argumentHint`). A minimal `key: value` scalar parser —
|
|
31
|
+
* command frontmatter only carries simple scalars (description, argument-hint,
|
|
32
|
+
* allowed-tools, model). Deliberately NOT js-yaml (an undeclared CLI dep that
|
|
33
|
+
* only resolves via workspace hoisting) and NOT skill-loader's parseSkillMd
|
|
34
|
+
* (its import chain drags native deps that crash vitest). Returns `{ data, body }`.
|
|
35
|
+
*/
|
|
36
|
+
function parseFrontmatter(content) {
|
|
37
|
+
const text = String(content || "");
|
|
38
|
+
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
39
|
+
if (!m) return { data: {}, body: text.trim() };
|
|
40
|
+
const data = {};
|
|
41
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
42
|
+
const trimmed = line.trim();
|
|
43
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
44
|
+
const colon = trimmed.indexOf(":");
|
|
45
|
+
if (colon <= 0) continue;
|
|
46
|
+
const key = trimmed.slice(0, colon).trim();
|
|
47
|
+
let value = trimmed.slice(colon + 1).trim();
|
|
48
|
+
// Strip one layer of surrounding quotes.
|
|
49
|
+
value = value.replace(/^(['"])([\s\S]*)\1$/, "$2");
|
|
50
|
+
const camel = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
51
|
+
data[camel] = value;
|
|
52
|
+
}
|
|
53
|
+
return { data, body: (m[2] || "").trim() };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Default shell timeout for `!`cmd`` bang execution. */
|
|
57
|
+
export const BANG_TIMEOUT_MS = 10_000;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The directories scanned for command files, project first (it shadows personal
|
|
61
|
+
* on a name clash). `opts.home` overrides the personal root for tests.
|
|
62
|
+
*/
|
|
63
|
+
export function commandDirs(cwd = process.cwd(), opts = {}) {
|
|
64
|
+
const path = opts.deps?.path || _deps.path;
|
|
65
|
+
const home = opts.home || homedir();
|
|
66
|
+
return [
|
|
67
|
+
{ dir: path.join(cwd, ".claude", "commands"), scope: "project" },
|
|
68
|
+
{ dir: path.join(home, ".claude", "commands"), scope: "personal" },
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Recursively collect `*.md` files under `dir` as `{file, rel}` (rel uses /). */
|
|
73
|
+
function walkMd(dir, { fs, path }, base = dir, acc = []) {
|
|
74
|
+
let entries;
|
|
75
|
+
try {
|
|
76
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
77
|
+
} catch {
|
|
78
|
+
return acc;
|
|
79
|
+
}
|
|
80
|
+
for (const e of entries) {
|
|
81
|
+
const full = path.join(dir, e.name);
|
|
82
|
+
if (e.isDirectory()) {
|
|
83
|
+
walkMd(full, { fs, path }, base, acc);
|
|
84
|
+
} else if (e.isFile() && e.name.endsWith(".md")) {
|
|
85
|
+
const rel = path.relative(base, full).replace(/\\/g, "/");
|
|
86
|
+
acc.push({ file: full, rel });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return acc;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Command name from a relative path: `git/commit.md` → `git:commit`. */
|
|
93
|
+
function nameFromRel(rel) {
|
|
94
|
+
return rel.replace(/\.md$/, "").replace(/\//g, ":");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Parse one command file into its metadata + body (no expansion yet). */
|
|
98
|
+
export function parseCommandFile(file, scope, opts = {}) {
|
|
99
|
+
const fs = opts.deps?.fs || _deps.fs;
|
|
100
|
+
let content;
|
|
101
|
+
try {
|
|
102
|
+
content = fs.readFileSync(file, "utf-8");
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const { data, body } = parseFrontmatter(content);
|
|
107
|
+
return {
|
|
108
|
+
file,
|
|
109
|
+
scope,
|
|
110
|
+
description: data.description || "",
|
|
111
|
+
argumentHint: data.argumentHint || "",
|
|
112
|
+
allowedTools: data.allowedTools || null,
|
|
113
|
+
model: data.model || null,
|
|
114
|
+
body: body || "",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Discover all commands across both scopes. Project shadows personal by name.
|
|
120
|
+
* @returns {Array<{name, scope, file, description, argumentHint, allowedTools, model}>}
|
|
121
|
+
*/
|
|
122
|
+
export function discoverCommands(cwd = process.cwd(), opts = {}) {
|
|
123
|
+
const fs = opts.deps?.fs || _deps.fs;
|
|
124
|
+
const path = opts.deps?.path || _deps.path;
|
|
125
|
+
const byName = new Map();
|
|
126
|
+
// Personal first, then project — so project overwrites on clash.
|
|
127
|
+
const dirs = commandDirs(cwd, opts).reverse();
|
|
128
|
+
for (const { dir, scope } of dirs) {
|
|
129
|
+
for (const { file, rel } of walkMd(dir, { fs, path })) {
|
|
130
|
+
const meta = parseCommandFile(file, scope, opts);
|
|
131
|
+
if (!meta) continue;
|
|
132
|
+
const name = nameFromRel(rel);
|
|
133
|
+
byName.set(name, { name, ...meta });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Look up one command by name (accepts `git:commit` or `git/commit`). */
|
|
140
|
+
export function getCommand(name, cwd = process.cwd(), opts = {}) {
|
|
141
|
+
const wanted = String(name || "")
|
|
142
|
+
.replace(/^\//, "")
|
|
143
|
+
.replace(/\//g, ":");
|
|
144
|
+
return discoverCommands(cwd, opts).find((c) => c.name === wanted) || null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Substitute $ARGUMENTS and $1..$9 in `text`. */
|
|
148
|
+
export function substituteArgs(text, args = []) {
|
|
149
|
+
const list = Array.isArray(args) ? args : [];
|
|
150
|
+
let out = text.replace(/\$ARGUMENTS/g, list.join(" "));
|
|
151
|
+
out = out.replace(/\$([1-9])/g, (_, d) => list[Number(d) - 1] ?? "");
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Run every `!`cmd`` and replace it with the command's stdout (best-effort). */
|
|
156
|
+
function runBangs(text, { cwd, execSync }) {
|
|
157
|
+
return text.replace(/!`([^`]+)`/g, (_, cmd) => {
|
|
158
|
+
try {
|
|
159
|
+
const out = execSync(cmd, {
|
|
160
|
+
cwd,
|
|
161
|
+
encoding: "utf-8",
|
|
162
|
+
timeout: BANG_TIMEOUT_MS,
|
|
163
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
164
|
+
});
|
|
165
|
+
return String(out).trim();
|
|
166
|
+
} catch (err) {
|
|
167
|
+
return `[command failed: ${cmd} — ${err.message?.split("\n")[0] || err}]`;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Expand a command into a final prompt string.
|
|
174
|
+
* 1. $ARGUMENTS / $1..$9 substitution
|
|
175
|
+
* 2. !`cmd` bang execution (skipped when opts.allowBang === false)
|
|
176
|
+
* 3. @path file references (via file-ref-expander)
|
|
177
|
+
*
|
|
178
|
+
* @param {object} command output of getCommand/parseCommandFile (needs .body)
|
|
179
|
+
* @param {string[]} args
|
|
180
|
+
* @param {object} [opts] { cwd, allowBang, deps:{ execSync } }
|
|
181
|
+
* @returns {{ prompt:string, warnings:string[] }}
|
|
182
|
+
*/
|
|
183
|
+
export function expandCommand(command, args = [], opts = {}) {
|
|
184
|
+
const cwd = opts.cwd || process.cwd();
|
|
185
|
+
const execSync = opts.deps?.execSync || _deps.execSync;
|
|
186
|
+
const warnings = [];
|
|
187
|
+
|
|
188
|
+
let text = substituteArgs(command.body || "", args);
|
|
189
|
+
|
|
190
|
+
if (opts.allowBang !== false) {
|
|
191
|
+
text = runBangs(text, { cwd, execSync });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// @file expansion appends a <referenced-files> block when anything resolves.
|
|
195
|
+
const expanded = expandFileRefs(text, { cwd });
|
|
196
|
+
for (const w of expanded.warnings) warnings.push(w);
|
|
197
|
+
|
|
198
|
+
return { prompt: expanded.prompt, warnings };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export { _deps };
|
package/src/repl/agent-repl.js
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
} from "../lib/session-hooks.js";
|
|
59
59
|
import { HookEvents } from "../lib/hook-manager.js";
|
|
60
60
|
import { IterationBudget } from "../lib/iteration-budget.js";
|
|
61
|
+
import { loadMcpConfig } from "../runtime/mcp-config.js";
|
|
61
62
|
import {
|
|
62
63
|
AGENT_TOOLS,
|
|
63
64
|
buildSystemPrompt,
|
|
@@ -354,6 +355,10 @@ export async function startAgentRepl(options = {}) {
|
|
|
354
355
|
// and applies bundle manifest metadata (model/provider override, agentId).
|
|
355
356
|
let _bundleResolved = null;
|
|
356
357
|
let _bundleMcpClient = null;
|
|
358
|
+
// --mcp-config (interactive parity with headless): ad-hoc MCP servers loaded
|
|
359
|
+
// for this session via the shared mcp-config engine. Holds {mcpClient,
|
|
360
|
+
// extraToolDefinitions, externalToolExecutors, externalToolDescriptors}.
|
|
361
|
+
let _adhocMcp = null;
|
|
357
362
|
if (options.bundlePath) {
|
|
358
363
|
try {
|
|
359
364
|
const { loadBundle } =
|
|
@@ -446,6 +451,28 @@ export async function startAgentRepl(options = {}) {
|
|
|
446
451
|
}
|
|
447
452
|
}
|
|
448
453
|
|
|
454
|
+
// --mcp-config: connect ad-hoc MCP servers for this interactive session and
|
|
455
|
+
// expose their tools to the LLM (Claude-Code parity with headless). Reuses the
|
|
456
|
+
// shared engine, so tools surface as mcp__<server>__<tool>. Best-effort: a bad
|
|
457
|
+
// config is reported but never aborts the REPL.
|
|
458
|
+
if (options.mcpConfig) {
|
|
459
|
+
try {
|
|
460
|
+
_adhocMcp = await loadMcpConfig(options.mcpConfig, {
|
|
461
|
+
writeErr: (s) => process.stderr.write(s),
|
|
462
|
+
});
|
|
463
|
+
const toolCount = _adhocMcp.extraToolDefinitions.length;
|
|
464
|
+
logger.log(
|
|
465
|
+
chalk.gray(
|
|
466
|
+
`MCP: ${_adhocMcp.connected.length} server(s), ${toolCount} tool(s) ` +
|
|
467
|
+
`(mcp__<server>__<tool>)`,
|
|
468
|
+
),
|
|
469
|
+
);
|
|
470
|
+
} catch (mcpErr) {
|
|
471
|
+
logger.log(chalk.yellow(`MCP: --mcp-config failed — ${mcpErr.message}`));
|
|
472
|
+
_adhocMcp = null;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
449
476
|
// Apply bundle approval policy to this session (after both gate and sessionId are ready)
|
|
450
477
|
if (_bundleResolved?.approvalPolicy?.default && _approvalGate && sessionId) {
|
|
451
478
|
try {
|
|
@@ -1567,7 +1594,12 @@ export async function startAgentRepl(options = {}) {
|
|
|
1567
1594
|
checkpointSession: sessionId,
|
|
1568
1595
|
prepareCall,
|
|
1569
1596
|
approvalGate: _approvalGate,
|
|
1570
|
-
|
|
1597
|
+
// MCP: --mcp-config (ad-hoc) wins; bundle MCP is the fallback. The 3
|
|
1598
|
+
// tool channels expose --mcp-config servers' tools to the LLM directly.
|
|
1599
|
+
mcpClient: _adhocMcp?.mcpClient || _bundleMcpClient || undefined,
|
|
1600
|
+
extraToolDefinitions: _adhocMcp?.extraToolDefinitions,
|
|
1601
|
+
externalToolExecutors: _adhocMcp?.externalToolExecutors,
|
|
1602
|
+
externalToolDescriptors: _adhocMcp?.externalToolDescriptors,
|
|
1571
1603
|
chatFn: _fallbackChatFn,
|
|
1572
1604
|
});
|
|
1573
1605
|
|
|
@@ -1778,6 +1810,15 @@ export async function startAgentRepl(options = {}) {
|
|
|
1778
1810
|
}
|
|
1779
1811
|
}
|
|
1780
1812
|
|
|
1813
|
+
// Disconnect ad-hoc (--mcp-config) MCP servers
|
|
1814
|
+
if (_adhocMcp?.mcpClient) {
|
|
1815
|
+
try {
|
|
1816
|
+
await _adhocMcp.mcpClient.disconnectAll();
|
|
1817
|
+
} catch (_e) {
|
|
1818
|
+
// Non-critical
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1781
1822
|
// Shutdown runtime
|
|
1782
1823
|
try {
|
|
1783
1824
|
await shutdown();
|
|
@@ -1729,6 +1729,19 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
1729
1729
|
? baseUrl
|
|
1730
1730
|
: "https://api.anthropic.com/v1";
|
|
1731
1731
|
|
|
1732
|
+
// Real token streaming (--include-partial-messages): stream the SSE response
|
|
1733
|
+
// and forward text deltas live, assembling tool_use blocks back into the
|
|
1734
|
+
// same {message, usage} shape the non-streaming path returns.
|
|
1735
|
+
if (typeof options.onToken === "function") {
|
|
1736
|
+
return await _chatAnthropicStreaming(
|
|
1737
|
+
`${url}/messages`,
|
|
1738
|
+
{ ...body, stream: true },
|
|
1739
|
+
{ "x-api-key": key, "anthropic-version": "2023-06-01" },
|
|
1740
|
+
options.onToken,
|
|
1741
|
+
signal,
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1732
1745
|
const response = await fetch(`${url}/messages`, {
|
|
1733
1746
|
method: "POST",
|
|
1734
1747
|
headers: {
|
|
@@ -1798,6 +1811,27 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
1798
1811
|
volcengine: "doubao-seed-1-6-251015",
|
|
1799
1812
|
};
|
|
1800
1813
|
|
|
1814
|
+
// Real token streaming (--include-partial-messages) for every OpenAI-compatible
|
|
1815
|
+
// provider (openai / deepseek / dashscope / mistral / gemini / volcengine):
|
|
1816
|
+
// stream the SSE response, forward content deltas live, and reassemble the
|
|
1817
|
+
// delta-fragmented tool_calls into the standard {message, usage} shape.
|
|
1818
|
+
if (typeof options.onToken === "function") {
|
|
1819
|
+
return await _chatOpenAIStreaming(
|
|
1820
|
+
`${url}/chat/completions`,
|
|
1821
|
+
{
|
|
1822
|
+
model: model || defaultModels[provider] || "gpt-4o-mini",
|
|
1823
|
+
messages,
|
|
1824
|
+
tools,
|
|
1825
|
+
stream: true,
|
|
1826
|
+
stream_options: { include_usage: true },
|
|
1827
|
+
},
|
|
1828
|
+
key,
|
|
1829
|
+
options.onToken,
|
|
1830
|
+
signal,
|
|
1831
|
+
provider,
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1801
1835
|
const response = await fetch(`${url}/chat/completions`, {
|
|
1802
1836
|
method: "POST",
|
|
1803
1837
|
headers: {
|
|
@@ -1934,6 +1968,225 @@ async function _chatOllamaStreaming(apiUrl, body, onToken, signal) {
|
|
|
1934
1968
|
return _ollamaFinalize(state);
|
|
1935
1969
|
}
|
|
1936
1970
|
|
|
1971
|
+
// ─── Anthropic streaming (SSE → {message, usage}, tool_use reassembled) ──────
|
|
1972
|
+
//
|
|
1973
|
+
// Anthropic /messages with stream:true emits SSE: message_start (input usage),
|
|
1974
|
+
// content_block_start (text or tool_use header), content_block_delta
|
|
1975
|
+
// (text_delta → onToken, or input_json_delta accumulating a tool's JSON args),
|
|
1976
|
+
// message_delta (output usage). We reduce per `data:` line and finalize into
|
|
1977
|
+
// the same shape chatWithTools returns non-streamed.
|
|
1978
|
+
|
|
1979
|
+
function _anthropicInitState() {
|
|
1980
|
+
return { text: "", blocks: {}, inputTokens: 0, outputTokens: 0 };
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
function _anthropicReduceLine(state, raw, onToken) {
|
|
1984
|
+
const line = (raw || "").trim();
|
|
1985
|
+
if (!line.startsWith("data:")) return state;
|
|
1986
|
+
const payload = line.slice(5).trim();
|
|
1987
|
+
if (!payload) return state;
|
|
1988
|
+
let obj;
|
|
1989
|
+
try {
|
|
1990
|
+
obj = JSON.parse(payload);
|
|
1991
|
+
} catch {
|
|
1992
|
+
return state;
|
|
1993
|
+
}
|
|
1994
|
+
if (obj.type === "message_start") {
|
|
1995
|
+
state.inputTokens =
|
|
1996
|
+
Number(obj.message?.usage?.input_tokens) || state.inputTokens;
|
|
1997
|
+
} else if (obj.type === "content_block_start") {
|
|
1998
|
+
const cb = obj.content_block || {};
|
|
1999
|
+
state.blocks[obj.index] =
|
|
2000
|
+
cb.type === "tool_use"
|
|
2001
|
+
? { type: "tool_use", id: cb.id, name: cb.name, json: "" }
|
|
2002
|
+
: { type: "text" };
|
|
2003
|
+
} else if (obj.type === "content_block_delta") {
|
|
2004
|
+
const d = obj.delta || {};
|
|
2005
|
+
if (d.type === "text_delta" && d.text) {
|
|
2006
|
+
state.text += d.text;
|
|
2007
|
+
if (typeof onToken === "function") {
|
|
2008
|
+
try {
|
|
2009
|
+
onToken(d.text);
|
|
2010
|
+
} catch {
|
|
2011
|
+
// a failing UI hook must never break the run
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
} else if (d.type === "input_json_delta" && state.blocks[obj.index]) {
|
|
2015
|
+
state.blocks[obj.index].json += d.partial_json || "";
|
|
2016
|
+
}
|
|
2017
|
+
} else if (obj.type === "message_delta") {
|
|
2018
|
+
state.outputTokens = Number(obj.usage?.output_tokens) || state.outputTokens;
|
|
2019
|
+
}
|
|
2020
|
+
return state;
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
function _anthropicFinalize(state) {
|
|
2024
|
+
const toolCalls = [];
|
|
2025
|
+
for (const k of Object.keys(state.blocks)) {
|
|
2026
|
+
const b = state.blocks[k];
|
|
2027
|
+
if (b.type === "tool_use") {
|
|
2028
|
+
let input = {};
|
|
2029
|
+
try {
|
|
2030
|
+
input = b.json ? JSON.parse(b.json) : {};
|
|
2031
|
+
} catch {
|
|
2032
|
+
input = {};
|
|
2033
|
+
}
|
|
2034
|
+
toolCalls.push({
|
|
2035
|
+
id: b.id,
|
|
2036
|
+
type: "function",
|
|
2037
|
+
function: { name: b.name, arguments: JSON.stringify(input) },
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
const message = { role: "assistant", content: state.text };
|
|
2042
|
+
if (toolCalls.length) message.tool_calls = toolCalls;
|
|
2043
|
+
const out = { message };
|
|
2044
|
+
if (state.inputTokens || state.outputTokens) {
|
|
2045
|
+
out.usage = {
|
|
2046
|
+
input_tokens: state.inputTokens,
|
|
2047
|
+
output_tokens: state.outputTokens,
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
return out;
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
/** Pure reducer over Anthropic SSE lines — exported for tests (no HTTP). */
|
|
2054
|
+
export function _accumulateAnthropicStream(lines, onToken) {
|
|
2055
|
+
const state = _anthropicInitState();
|
|
2056
|
+
for (const line of lines) _anthropicReduceLine(state, line, onToken);
|
|
2057
|
+
return _anthropicFinalize(state);
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
async function _chatAnthropicStreaming(apiUrl, body, extraHeaders, onToken, signal) {
|
|
2061
|
+
const response = await fetch(apiUrl, {
|
|
2062
|
+
method: "POST",
|
|
2063
|
+
headers: { "Content-Type": "application/json", ...extraHeaders },
|
|
2064
|
+
signal,
|
|
2065
|
+
body: JSON.stringify(body),
|
|
2066
|
+
});
|
|
2067
|
+
if (!response.ok) {
|
|
2068
|
+
throw new Error(`Anthropic error: ${response.status}`);
|
|
2069
|
+
}
|
|
2070
|
+
const state = _anthropicInitState();
|
|
2071
|
+
const reader = response.body.getReader();
|
|
2072
|
+
const decoder = new TextDecoder();
|
|
2073
|
+
let buf = "";
|
|
2074
|
+
for (;;) {
|
|
2075
|
+
const { done, value } = await reader.read();
|
|
2076
|
+
if (done) break;
|
|
2077
|
+
buf += decoder.decode(value, { stream: true });
|
|
2078
|
+
const lines = buf.split("\n");
|
|
2079
|
+
buf = lines.pop() || "";
|
|
2080
|
+
for (const line of lines) _anthropicReduceLine(state, line, onToken);
|
|
2081
|
+
}
|
|
2082
|
+
if (buf.trim()) _anthropicReduceLine(state, buf, onToken);
|
|
2083
|
+
return _anthropicFinalize(state);
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
// ─── OpenAI-compatible streaming (SSE → {message, usage}) ────────────────────
|
|
2087
|
+
//
|
|
2088
|
+
// `data:` lines carry choices[0].delta.{content, tool_calls[]}; tool_calls
|
|
2089
|
+
// arrive fragmented and keyed by `index` (name in the first chunk, arguments
|
|
2090
|
+
// concatenated across chunks). usage rides the terminal chunk when
|
|
2091
|
+
// stream_options.include_usage was requested. Terminator: `data: [DONE]`.
|
|
2092
|
+
|
|
2093
|
+
function _openaiInitState() {
|
|
2094
|
+
return { text: "", tools: [], inputTokens: 0, outputTokens: 0 };
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
function _openaiReduceLine(state, raw, onToken) {
|
|
2098
|
+
const line = (raw || "").trim();
|
|
2099
|
+
if (!line.startsWith("data:")) return state;
|
|
2100
|
+
const payload = line.slice(5).trim();
|
|
2101
|
+
if (!payload || payload === "[DONE]") return state;
|
|
2102
|
+
let obj;
|
|
2103
|
+
try {
|
|
2104
|
+
obj = JSON.parse(payload);
|
|
2105
|
+
} catch {
|
|
2106
|
+
return state;
|
|
2107
|
+
}
|
|
2108
|
+
const delta = obj.choices?.[0]?.delta;
|
|
2109
|
+
if (delta?.content) {
|
|
2110
|
+
state.text += delta.content;
|
|
2111
|
+
if (typeof onToken === "function") {
|
|
2112
|
+
try {
|
|
2113
|
+
onToken(delta.content);
|
|
2114
|
+
} catch {
|
|
2115
|
+
// a failing UI hook must never break the run
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
if (Array.isArray(delta?.tool_calls)) {
|
|
2120
|
+
for (const tc of delta.tool_calls) {
|
|
2121
|
+
const idx = tc.index ?? 0;
|
|
2122
|
+
if (!state.tools[idx]) state.tools[idx] = { id: undefined, name: "", args: "" };
|
|
2123
|
+
if (tc.id) state.tools[idx].id = tc.id;
|
|
2124
|
+
if (tc.function?.name) state.tools[idx].name = tc.function.name;
|
|
2125
|
+
if (tc.function?.arguments) state.tools[idx].args += tc.function.arguments;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
if (obj.usage) {
|
|
2129
|
+
state.inputTokens = Number(obj.usage.prompt_tokens) || state.inputTokens;
|
|
2130
|
+
state.outputTokens =
|
|
2131
|
+
Number(obj.usage.completion_tokens) || state.outputTokens;
|
|
2132
|
+
}
|
|
2133
|
+
return state;
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
function _openaiFinalize(state) {
|
|
2137
|
+
const toolCalls = state.tools.filter(Boolean).map((t) => ({
|
|
2138
|
+
id: t.id || `call_${t.name || "tool"}`,
|
|
2139
|
+
type: "function",
|
|
2140
|
+
function: { name: t.name, arguments: t.args || "{}" },
|
|
2141
|
+
}));
|
|
2142
|
+
const message = { role: "assistant", content: state.text };
|
|
2143
|
+
if (toolCalls.length) message.tool_calls = toolCalls;
|
|
2144
|
+
const out = { message };
|
|
2145
|
+
if (state.inputTokens || state.outputTokens) {
|
|
2146
|
+
out.usage = {
|
|
2147
|
+
input_tokens: state.inputTokens,
|
|
2148
|
+
output_tokens: state.outputTokens,
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
return out;
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
/** Pure reducer over OpenAI-compatible SSE lines — exported for tests. */
|
|
2155
|
+
export function _accumulateOpenAIStream(lines, onToken) {
|
|
2156
|
+
const state = _openaiInitState();
|
|
2157
|
+
for (const line of lines) _openaiReduceLine(state, line, onToken);
|
|
2158
|
+
return _openaiFinalize(state);
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
async function _chatOpenAIStreaming(apiUrl, body, apiKey, onToken, signal, provider) {
|
|
2162
|
+
const response = await fetch(apiUrl, {
|
|
2163
|
+
method: "POST",
|
|
2164
|
+
headers: {
|
|
2165
|
+
"Content-Type": "application/json",
|
|
2166
|
+
Authorization: `Bearer ${apiKey}`,
|
|
2167
|
+
},
|
|
2168
|
+
signal,
|
|
2169
|
+
body: JSON.stringify(body),
|
|
2170
|
+
});
|
|
2171
|
+
if (!response.ok) {
|
|
2172
|
+
throw new Error(`${provider} API error: ${response.status}`);
|
|
2173
|
+
}
|
|
2174
|
+
const state = _openaiInitState();
|
|
2175
|
+
const reader = response.body.getReader();
|
|
2176
|
+
const decoder = new TextDecoder();
|
|
2177
|
+
let buf = "";
|
|
2178
|
+
for (;;) {
|
|
2179
|
+
const { done, value } = await reader.read();
|
|
2180
|
+
if (done) break;
|
|
2181
|
+
buf += decoder.decode(value, { stream: true });
|
|
2182
|
+
const lines = buf.split("\n");
|
|
2183
|
+
buf = lines.pop() || "";
|
|
2184
|
+
for (const line of lines) _openaiReduceLine(state, line, onToken);
|
|
2185
|
+
}
|
|
2186
|
+
if (buf.trim()) _openaiReduceLine(state, buf, onToken);
|
|
2187
|
+
return _openaiFinalize(state);
|
|
2188
|
+
}
|
|
2189
|
+
|
|
1937
2190
|
function _normalizeAnthropicResponse(data) {
|
|
1938
2191
|
const content = data.content || [];
|
|
1939
2192
|
const textBlocks = content.filter((b) => b.type === "text");
|