chainlesschain 0.162.35 → 0.162.37
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-CJn02U42.js → AIOps-_oxz4VHy.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-ewURAAoy.js → ActionButton-uaeqFuDj.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-BiSadESb.js → Analytics-BPVV0OUf.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-BR0WOEug.js → AppLayout-ppCYKm3I.js} +4 -4
- package/src/assets/web-panel/assets/{Audit-CrqcYx0e.js → Audit-DFAY6umk.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-DtbSBn4e.js → Backup-pAPBFDyP.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-BjSc9j0o.js → BaseInput-BbBl0uT2.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-ixzrlCJE.js → Chat-Ct22JUnT.js} +6 -6
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-B78nEq05.js → ChatBubbleRenderer-DPlsLl22.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-UGYeSsgr.js → Checkbox-DEkCollc.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-B97OOAg4.js → Codegen-Tor-de39.js} +1 -1
- package/src/assets/web-panel/assets/{Col-D9aGkaZ6.js → Col-ojNrLQU7.js} +1 -1
- package/src/assets/web-panel/assets/{Community-Dc2v2RGS.js → Community-CLOGhqMF.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-B_FYlUQR.js → Compact-CYKNlSZ4.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-C4FiTHyC.js → Compliance-C5E6ABuA.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-CQ8j3LIg.js → Cowork-CHeEsZ3W.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-Dzjs9Z9Z.js → Cron-B4e1n2e7.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-BXI24uzI.js → Crosschain-DbNV8P9R.js} +1 -1
- package/src/assets/web-panel/assets/{DID-C-I4_d07.js → DID-C5_Tk3nC.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-BzzGh5mo.js → Dashboard-BhdV_c4N.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-Bh8H70De.js → Dropdown-CEi5AMtM.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-DI_qybJP.js → EmailListRenderer-DOhPiYng.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-DkKTsfc4.js → FamilyGuardDashboard-fu4NRP3X.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-DS7CmvVG.js → Federation-B7BtIWKL.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-CI97WsB5.js → FormItemContext-BmPWZVLP.js} +1 -1
- package/src/assets/web-panel/assets/GenericCardRenderer-hsOPNJq8.js +1 -0
- package/src/assets/web-panel/assets/{Git-CEh0gR2W.js → Git-Bi_EFBUH.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-kIr3tls2.js → Governance-emf2ubDK.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CC1GzyC1.js → Inference-B7KjKzkI.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-BNgTiWOB.js → KnowledgeGraph-uAaBK0F3.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-B2P10gB1.js → Logs-utK7hNpj.js} +2 -2
- package/src/assets/web-panel/assets/{Marketplace-HPfBvbFZ.js → Marketplace-CzQe6n3z.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-ByYotSKb.js → McpTools-CuAaJr51.js} +5 -5
- package/src/assets/web-panel/assets/{Memory-BGIAzFVS.js → Memory-CRuZZJ75.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-CroNYTAH.js → MobileBridge-Cp06wunh.js} +2 -2
- package/src/assets/web-panel/assets/MobileProjects-DJEdUwhr.js +1 -0
- package/src/assets/web-panel/assets/{Mtc-BqhyIwo9.js → Mtc-8YY4dR7g.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-BpEKOvx9.js → MtcAudit-BmPJYHar.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-DST1d_Qo.js → Multisig-d-ydyVdq.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-DlMsZcK_.js → NLProgramming-DA_ikw_n.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-C734UJvD.js → Notes-DIyF-fRe.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-C0-pPxvk.js → NotificationSettings-CzPZXEtK.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-BiLtg-LY.js +1 -0
- package/src/assets/web-panel/assets/{Organization-C5iHC_yW.js → Organization-DdDZ_Ap6.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-CovuHHVR.js → Overflow-BnMBkttv.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-Dx9QL-Gy.js → P2P-Es1050f-.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-IP1dEt6-.js → PdhVaultBrowser-CKkRmyn9.js} +4 -4
- package/src/assets/web-panel/assets/{Permissions-BrR1XZG5.js → Permissions-zU9n9cAD.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-BgqxVE5m.js → PersonalDataHub-BZi5Xwas.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-DzMk5HAz.js → Pipeline-CRfeGiFc.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-CDoLa6tk.js → Privacy-CQA_IgLA.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-Dy5gc6ve.js → ProjectInit-C9hmEvoT.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-DXy-k4hG.js → ProjectSettings-yXA72ws4.js} +2 -2
- package/src/assets/web-panel/assets/Projects-BpWS-qam.js +1 -0
- package/src/assets/web-panel/assets/Providers-Cxe55dRD.js +1 -0
- package/src/assets/web-panel/assets/{QuickAsk-B8KEHCnd.js → QuickAsk-Do0aUTQr.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DNVHGYYZ.js → Recommend--ysZHjyA.js} +1 -1
- package/src/assets/web-panel/assets/{Reputation-CaDhWP03.js → Reputation-BOBU8JrH.js} +1 -1
- package/src/assets/web-panel/assets/{Row-CrGLI02x.js → Row-C6X7bRKE.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BX7P8I6i.js → RssFeed-D8AwqlkQ.js} +3 -3
- package/src/assets/web-panel/assets/Search-Bi3rCZD4.js +1 -0
- package/src/assets/web-panel/assets/{Security-B6J7IFc1.js → Security-DxUDVrtY.js} +4 -4
- package/src/assets/web-panel/assets/{Services-vvdcO3mM.js → Services-BXXN7yC1.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-BoAoPTzZ.js → Skeleton-B3BR34tZ.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CyIQV5b3.js → Skills-BjYu8OQ1.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-BAQVgdZV.js → Sla-DDkCtD8w.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-Bxcn1Jkj.js → SpeechSettings-CGhYzP7V.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-Dpaj3hDM.js → SyncSettings-CYNKVAHA.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-Bwqo89En.js → Tasks-DavmlJpd.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-Bowcqifn.js → Templates-CQuYFf2C.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-DOkf85uG.js → Tenant-DdzZh8vE.js} +1 -1
- package/src/assets/web-panel/assets/Terminal-D75WeG9d.js +3 -0
- package/src/assets/web-panel/assets/{TimelineRenderer-B9A3zDXA.js → TimelineRenderer-DKOARnc_.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-jtVVqKFr.js → Tokens-D7QRNG8y.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-26Iw-iIl.js → Trigger-BCsqLZl4.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-DqY5ORrH.js → Trust-BarGUa6p.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-BFsbr3y7.js → UkeySign-pHrg5a8E.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-BtDbj3oa.js → VideoEditing-Dug3m1py.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-BAwmwHbk.js → Wallet-BfK3Z_Ez.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-DINJTsfq.js → WebAuthn-CYRdl9td.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-BEorm8SK.js → WorkflowEditor-DTW5AcqM.js} +1 -1
- package/src/assets/web-panel/assets/{chat-CE39-Dxg.js → chat-CCXz4j38.js} +1 -1
- package/src/assets/web-panel/assets/{colors-C_cLZ93a.js → colors-BJBOhAqa.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-BSioWA2c.js → compact-item-E9M6BQcM.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CGTk4mhN.js → createContext-Cg9CAws4.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-BrsbTJUv.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-Dl1fRwS_.js → hasIn-DhVtqv5L.js} +1 -1
- package/src/assets/web-panel/assets/{index-pngH1and.js → index--7o5YdL6.js} +1 -1
- package/src/assets/web-panel/assets/{index-BmbVyhk1.js → index-4N5lNXGP.js} +1 -1
- package/src/assets/web-panel/assets/{index-BnEPB1Mz.js → index-6-04M2Nx.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cxw3p73X.js → index-B111fZ21.js} +1 -1
- package/src/assets/web-panel/assets/{index-CST381Qf.js → index-B4NBF4Sa.js} +1 -1
- package/src/assets/web-panel/assets/{index-ChwpS1f0.js → index-B8bjEHrQ.js} +1 -1
- package/src/assets/web-panel/assets/{index--SWvw6yW.js → index-BAB0nGP7.js} +1 -1
- package/src/assets/web-panel/assets/{index-CAwVwBOL.js → index-BFZPRd0T.js} +1 -1
- package/src/assets/web-panel/assets/{index-hv4jUdG3.js → index-B_SMPD4L.js} +1 -1
- package/src/assets/web-panel/assets/{index-Qj2x55mz.js → index-BxSzyly9.js} +1 -1
- package/src/assets/web-panel/assets/{index-BWpfxzVm.js → index-ByazO4Q9.js} +1 -1
- package/src/assets/web-panel/assets/{index-Di6nvW1N.js → index-C-2dUIli.js} +1 -1
- package/src/assets/web-panel/assets/{index-BhqOTuMW.js → index-CFarAlXj.js} +1 -1
- package/src/assets/web-panel/assets/{index-CA6K7lZB.js → index-CFp-wdrQ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTKEXyaW.js → index-CJ8nNT8h.js} +1 -1
- package/src/assets/web-panel/assets/{index-iiZfONfx.js → index-CSiyjCYi.js} +1 -1
- package/src/assets/web-panel/assets/{index-DTpCUi0m.js → index-CUp_c8Le.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bvi14vJ7.js → index-CVR_s-pT.js} +1 -1
- package/src/assets/web-panel/assets/{index-DKEipmR8.js → index-Ca8BYV1g.js} +1 -1
- package/src/assets/web-panel/assets/{index-DJyeeygd.js → index-CeRlLp3F.js} +1 -1
- package/src/assets/web-panel/assets/{index-C9tq8Da8.js → index-ChsSljaN.js} +1 -1
- package/src/assets/web-panel/assets/{index-B2QiUEgK.js → index-CkTeBHI9.js} +1 -1
- package/src/assets/web-panel/assets/{index-OCxo0X6J.js → index-Cm1m7BJh.js} +1 -1
- package/src/assets/web-panel/assets/{index-DrWERr8C.js → index-ComyTKz-.js} +1 -1
- package/src/assets/web-panel/assets/{index-B016Fsqr.js → index-CznfPnOx.js} +3 -3
- package/src/assets/web-panel/assets/{index-CisXVbSt.js → index-D5yC2Ps8.js} +1 -1
- package/src/assets/web-panel/assets/{index-C-VVk1Jg.js → index-D7DXdf7x.js} +1 -1
- package/src/assets/web-panel/assets/{index-DDQx2YFc.js → index-DDcJO27F.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ds2RzRG0.js → index-DSQazU6J.js} +1 -1
- package/src/assets/web-panel/assets/index-DSTQDO-Y.js +1 -0
- package/src/assets/web-panel/assets/{index-C4JXchTG.js → index-DaFe1aqY.js} +1 -1
- package/src/assets/web-panel/assets/{index-BAhinBPR.js → index-DdhnGez0.js} +1 -1
- package/src/assets/web-panel/assets/{index-9_mmaR42.js → index-Di5LBXcE.js} +1 -1
- package/src/assets/web-panel/assets/{index-D9D4q-qI.js → index-Dwvewrul.js} +1 -1
- package/src/assets/web-panel/assets/{index-CbXnyoSO.js → index-MdXEhfdJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-II3JhQu2.js → index-_PNqQ5mE.js} +1 -1
- package/src/assets/web-panel/assets/index-c2U6LV3Q.js +1 -0
- package/src/assets/web-panel/assets/{index-C2ly7sCw.js → index-kz1oXl1a.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ceo9P9tQ.js → index-wkt-o5q5.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-GOhLA2-f.js → initDefaultProps-iyBaePF-.js} +1 -1
- package/src/assets/web-panel/assets/{motion-jqxFzHTx.js → motion-RWtj4rgu.js} +1 -1
- package/src/assets/web-panel/assets/{move-CSLsp6TA.js → move-CqPRVzpH.js} +1 -1
- package/src/assets/web-panel/assets/{omit-Cnlrb25c.js → omit-DsvJze25.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-CLqlxWWD.js → pickAttrs-B4tfZBhc.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BAWIWtul.js → placementArrow-KvHUwXMA.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-CSR1DayS.js → responsiveObserve-DGdJ-b7W.js} +1 -1
- package/src/assets/web-panel/assets/{slide-CNhoPJOp.js → slide-Cd6ebRmw.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BZiYHRHW.js → statusUtils-Bg9GcIAn.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-BMoY-Fm5.js → styleChecker-MQjKsG84.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DhtNdlaS.js → useFlexGapSupport-C241WujP.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DNPtDOZ4.js → useFs-CMpy7RS4.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-DTdjNvAI.js → usePersonalDataHub-BLHtapKb.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-C9zW9IJ2.js → vnode-DmcTV67c.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-D-6RYJJr.js → zoom-DHL8_0Y8.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agent.js +161 -6
- package/src/commands/agents.js +8 -2
- package/src/commands/cli-anything.js +14 -6
- package/src/commands/command.js +7 -2
- package/src/commands/hook.js +136 -28
- package/src/commands/ide.js +168 -0
- package/src/commands/loop.js +450 -0
- package/src/commands/mcp.js +92 -0
- package/src/commands/output-style.js +127 -0
- package/src/commands/permissions.js +211 -0
- package/src/commands/statusline.js +93 -0
- package/src/index.js +10 -2
- package/src/lib/agent-core.js +7 -0
- package/src/lib/agents.js +5 -0
- package/src/lib/hook-manager.js +1 -0
- package/src/lib/hook-runner.cjs +183 -0
- package/src/lib/ide-bridge.js +310 -0
- package/src/lib/image-input.js +156 -0
- package/src/lib/loop.js +198 -0
- package/src/lib/mcp-oauth.js +415 -0
- package/src/lib/output-styles.js +179 -0
- package/src/lib/permission-rules.cjs +325 -0
- package/src/lib/provider-options.js +11 -7
- package/src/lib/settings-hook-events.cjs +102 -0
- package/src/lib/settings-hooks.cjs +163 -0
- package/src/lib/settings-loader.cjs +244 -0
- package/src/lib/slash-commands.js +4 -0
- package/src/lib/status-line.cjs +204 -0
- package/src/lib/sub-agent-profiles.js +3 -0
- package/src/lib/web-search.js +487 -0
- package/src/repl/agent-repl.js +450 -35
- package/src/repl/slash-macro.js +45 -0
- package/src/runtime/agent-core.js +799 -21
- package/src/runtime/coding-agent-contract-shared.cjs +94 -4
- package/src/runtime/coding-agent-policy.cjs +24 -0
- package/src/runtime/headless-runner.js +162 -6
- package/src/runtime/headless-stream.js +133 -7
- package/src/runtime/mcp-config.js +161 -15
- package/src/runtime/policies/agent-policy.js +4 -0
- package/src/runtime/system-prompt.js +6 -1
- package/src/assets/web-panel/assets/GenericCardRenderer-Da27EdR4.js +0 -1
- package/src/assets/web-panel/assets/MobileProjects-CH-qnGEV.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-C7zT9eFc.js +0 -1
- package/src/assets/web-panel/assets/Projects-DvsaEbZR.js +0 -1
- package/src/assets/web-panel/assets/Providers-Demck9PO.js +0 -1
- package/src/assets/web-panel/assets/Search-laS6rz8M.js +0 -1
- package/src/assets/web-panel/assets/Terminal-v4MM9dCj.js +0 -3
- package/src/assets/web-panel/assets/devWarning-PObcVnJR.js +0 -1
- package/src/assets/web-panel/assets/index-BNwIzLyX.js +0 -1
- package/src/assets/web-panel/assets/index-Dh6FxR9B.js +0 -1
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp-oauth — OAuth 2.0 (Authorization Code + PKCE) for remote MCP servers,
|
|
3
|
+
* Claude-Code parity. A remote HTTP/SSE MCP server that requires OAuth is
|
|
4
|
+
* authorized once via `cc mcp login <url>`; the access token is stored and
|
|
5
|
+
* injected as `Authorization: Bearer …` on every connect (refreshed when it
|
|
6
|
+
* expires). Static `-H "Authorization: Bearer …"` headers still work for
|
|
7
|
+
* servers where you already hold a token.
|
|
8
|
+
*
|
|
9
|
+
* The flow (RFC 8414 metadata discovery + RFC 7591 dynamic registration +
|
|
10
|
+
* RFC 7636 PKCE):
|
|
11
|
+
* 1. discover the protected-resource / authorization-server metadata;
|
|
12
|
+
* 2. register a public client (or use a configured client_id);
|
|
13
|
+
* 3. open the browser to the authorize URL, catch the code on a localhost
|
|
14
|
+
* callback, exchange it (with the PKCE verifier) for tokens;
|
|
15
|
+
* 4. persist { access_token, refresh_token, expires_at, client_id, endpoints }.
|
|
16
|
+
*
|
|
17
|
+
* The pure pieces (PKCE, discovery, URL building, token exchange, the store)
|
|
18
|
+
* are `_deps`-injected (fetch / fs / homedir / crypto / http / spawn) so they
|
|
19
|
+
* unit-test without a network, browser, or real OAuth server. The interactive
|
|
20
|
+
* `authorizeInteractive` orchestrator is the thin glue over them.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import fsDefault from "node:fs";
|
|
24
|
+
import pathDefault from "node:path";
|
|
25
|
+
import { homedir as homedirDefault } from "node:os";
|
|
26
|
+
import crypto from "node:crypto";
|
|
27
|
+
import http from "node:http";
|
|
28
|
+
import { spawn } from "node:child_process";
|
|
29
|
+
|
|
30
|
+
export const _deps = {
|
|
31
|
+
fetch: (...a) => globalThis.fetch(...a),
|
|
32
|
+
fs: fsDefault,
|
|
33
|
+
homedir: homedirDefault,
|
|
34
|
+
randomBytes: (n) => crypto.randomBytes(n),
|
|
35
|
+
sha256: (s) => crypto.createHash("sha256").update(s).digest(),
|
|
36
|
+
createServer: (h) => http.createServer(h),
|
|
37
|
+
openBrowser: defaultOpenBrowser,
|
|
38
|
+
now: () => Date.now(),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const base64url = (buf) =>
|
|
42
|
+
Buffer.from(buf).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
43
|
+
|
|
44
|
+
/** RFC 7636 PKCE pair (S256). */
|
|
45
|
+
export function generatePkce() {
|
|
46
|
+
const verifier = base64url(_deps.randomBytes(32));
|
|
47
|
+
const challenge = base64url(_deps.sha256(verifier));
|
|
48
|
+
return { verifier, challenge, method: "S256" };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A random URL-safe state / nonce. */
|
|
52
|
+
export function randomState(bytes = 16) {
|
|
53
|
+
return base64url(_deps.randomBytes(bytes));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function fetchJson(url, opts) {
|
|
57
|
+
const res = await _deps.fetch(url, opts);
|
|
58
|
+
if (!res || !res.ok) {
|
|
59
|
+
const status = res ? res.status : "no-response";
|
|
60
|
+
let body = "";
|
|
61
|
+
try {
|
|
62
|
+
body = res ? await res.text() : "";
|
|
63
|
+
} catch {
|
|
64
|
+
/* ignore */
|
|
65
|
+
}
|
|
66
|
+
const err = new Error(`HTTP ${status}${body ? `: ${body.slice(0, 200)}` : ""}`);
|
|
67
|
+
err.status = res ? res.status : null;
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
return res.json();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Discover the authorization-server metadata for a remote MCP server. Tries the
|
|
75
|
+
* protected-resource doc first (which points at its authorization server), then
|
|
76
|
+
* the authorization-server doc directly at the origin.
|
|
77
|
+
* @returns {Promise<{issuer?,authorization_endpoint,token_endpoint,registration_endpoint?,scopes_supported?}>}
|
|
78
|
+
*/
|
|
79
|
+
export async function discoverAuthMetadata(serverUrl, { resourceMetadataUrl } = {}) {
|
|
80
|
+
const origin = new URL(serverUrl).origin;
|
|
81
|
+
// 1. protected-resource metadata (RFC 9728) → authorization_servers[]
|
|
82
|
+
let authServer = origin;
|
|
83
|
+
try {
|
|
84
|
+
const prUrl =
|
|
85
|
+
resourceMetadataUrl || `${origin}/.well-known/oauth-protected-resource`;
|
|
86
|
+
const pr = await fetchJson(prUrl);
|
|
87
|
+
if (Array.isArray(pr.authorization_servers) && pr.authorization_servers[0]) {
|
|
88
|
+
authServer = String(pr.authorization_servers[0]).replace(/\/$/, "");
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// no protected-resource doc → assume the origin is its own auth server
|
|
92
|
+
}
|
|
93
|
+
// 2. authorization-server metadata (RFC 8414)
|
|
94
|
+
const candidates = [
|
|
95
|
+
`${authServer}/.well-known/oauth-authorization-server`,
|
|
96
|
+
`${authServer}/.well-known/openid-configuration`,
|
|
97
|
+
];
|
|
98
|
+
let lastErr = null;
|
|
99
|
+
for (const url of candidates) {
|
|
100
|
+
try {
|
|
101
|
+
const md = await fetchJson(url);
|
|
102
|
+
if (md && md.authorization_endpoint && md.token_endpoint) {
|
|
103
|
+
return {
|
|
104
|
+
issuer: md.issuer || authServer,
|
|
105
|
+
authorization_endpoint: md.authorization_endpoint,
|
|
106
|
+
token_endpoint: md.token_endpoint,
|
|
107
|
+
registration_endpoint: md.registration_endpoint || null,
|
|
108
|
+
scopes_supported: md.scopes_supported || null,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
lastErr = err;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
throw new Error(
|
|
116
|
+
`could not discover OAuth metadata for ${serverUrl}${lastErr ? ` (${lastErr.message})` : ""}`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** RFC 7591 dynamic client registration → client_id (public client). */
|
|
121
|
+
export async function registerClient(metadata, { redirectUri, clientName = "chainlesschain-cli" } = {}) {
|
|
122
|
+
if (!metadata.registration_endpoint) {
|
|
123
|
+
throw new Error("server has no registration_endpoint and no --client-id was given");
|
|
124
|
+
}
|
|
125
|
+
const reg = await fetchJson(metadata.registration_endpoint, {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: { "content-type": "application/json" },
|
|
128
|
+
body: JSON.stringify({
|
|
129
|
+
client_name: clientName,
|
|
130
|
+
redirect_uris: [redirectUri],
|
|
131
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
132
|
+
response_types: ["code"],
|
|
133
|
+
token_endpoint_auth_method: "none",
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
if (!reg.client_id) throw new Error("registration did not return a client_id");
|
|
137
|
+
return { clientId: reg.client_id, clientSecret: reg.client_secret || null };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Build the authorize URL (Authorization Code + PKCE). */
|
|
141
|
+
export function buildAuthorizeUrl(metadata, { clientId, redirectUri, scope, codeChallenge, state, resource }) {
|
|
142
|
+
const u = new URL(metadata.authorization_endpoint);
|
|
143
|
+
u.searchParams.set("response_type", "code");
|
|
144
|
+
u.searchParams.set("client_id", clientId);
|
|
145
|
+
u.searchParams.set("redirect_uri", redirectUri);
|
|
146
|
+
u.searchParams.set("code_challenge", codeChallenge);
|
|
147
|
+
u.searchParams.set("code_challenge_method", "S256");
|
|
148
|
+
u.searchParams.set("state", state);
|
|
149
|
+
if (scope) u.searchParams.set("scope", scope);
|
|
150
|
+
if (resource) u.searchParams.set("resource", resource);
|
|
151
|
+
return u.toString();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function _tokenExpiresAt(tok) {
|
|
155
|
+
const ttl = Number(tok.expires_in);
|
|
156
|
+
return Number.isFinite(ttl) && ttl > 0 ? _deps.now() + ttl * 1000 : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Exchange an authorization code for tokens. */
|
|
160
|
+
export async function exchangeCodeForToken(metadata, { code, codeVerifier, clientId, clientSecret, redirectUri, resource }) {
|
|
161
|
+
const body = new URLSearchParams({
|
|
162
|
+
grant_type: "authorization_code",
|
|
163
|
+
code,
|
|
164
|
+
redirect_uri: redirectUri,
|
|
165
|
+
client_id: clientId,
|
|
166
|
+
code_verifier: codeVerifier,
|
|
167
|
+
});
|
|
168
|
+
if (clientSecret) body.set("client_secret", clientSecret);
|
|
169
|
+
if (resource) body.set("resource", resource);
|
|
170
|
+
const tok = await fetchJson(metadata.token_endpoint, {
|
|
171
|
+
method: "POST",
|
|
172
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
173
|
+
body: body.toString(),
|
|
174
|
+
});
|
|
175
|
+
return {
|
|
176
|
+
access_token: tok.access_token,
|
|
177
|
+
refresh_token: tok.refresh_token || null,
|
|
178
|
+
token_type: tok.token_type || "Bearer",
|
|
179
|
+
expires_at: _tokenExpiresAt(tok),
|
|
180
|
+
scope: tok.scope || undefined,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Refresh an access token. */
|
|
185
|
+
export async function refreshAccessToken(metadata, { refreshToken, clientId, clientSecret, resource }) {
|
|
186
|
+
const body = new URLSearchParams({
|
|
187
|
+
grant_type: "refresh_token",
|
|
188
|
+
refresh_token: refreshToken,
|
|
189
|
+
client_id: clientId,
|
|
190
|
+
});
|
|
191
|
+
if (clientSecret) body.set("client_secret", clientSecret);
|
|
192
|
+
if (resource) body.set("resource", resource);
|
|
193
|
+
const tok = await fetchJson(metadata.token_endpoint, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
196
|
+
body: body.toString(),
|
|
197
|
+
});
|
|
198
|
+
return {
|
|
199
|
+
access_token: tok.access_token,
|
|
200
|
+
refresh_token: tok.refresh_token || refreshToken,
|
|
201
|
+
token_type: tok.token_type || "Bearer",
|
|
202
|
+
expires_at: _tokenExpiresAt(tok),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── token store (~/.chainlesschain/mcp-oauth.json) ────────────────────────
|
|
207
|
+
|
|
208
|
+
/** Stable key for a server (origin) so http/https/path variants share a token. */
|
|
209
|
+
export function serverKey(serverUrl) {
|
|
210
|
+
try {
|
|
211
|
+
return new URL(serverUrl).origin;
|
|
212
|
+
} catch {
|
|
213
|
+
return String(serverUrl || "").trim();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function tokenStorePath() {
|
|
218
|
+
return pathDefault.join(_deps.homedir(), ".chainlesschain", "mcp-oauth.json");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function loadTokenStore() {
|
|
222
|
+
const file = tokenStorePath();
|
|
223
|
+
try {
|
|
224
|
+
if (!_deps.fs.existsSync(file)) return {};
|
|
225
|
+
const data = JSON.parse(_deps.fs.readFileSync(file, "utf-8"));
|
|
226
|
+
return data && typeof data === "object" ? data : {};
|
|
227
|
+
} catch {
|
|
228
|
+
return {};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function getStoredToken(serverUrl) {
|
|
233
|
+
return loadTokenStore()[serverKey(serverUrl)] || null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function saveStoredToken(serverUrl, record) {
|
|
237
|
+
const file = tokenStorePath();
|
|
238
|
+
const store = loadTokenStore();
|
|
239
|
+
store[serverKey(serverUrl)] = { server: serverKey(serverUrl), ...record };
|
|
240
|
+
_deps.fs.mkdirSync(pathDefault.dirname(file), { recursive: true });
|
|
241
|
+
_deps.fs.writeFileSync(file, JSON.stringify(store, null, 2) + "\n", "utf-8");
|
|
242
|
+
return store[serverKey(serverUrl)];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function deleteStoredToken(serverUrl) {
|
|
246
|
+
const file = tokenStorePath();
|
|
247
|
+
const store = loadTokenStore();
|
|
248
|
+
const key = serverKey(serverUrl);
|
|
249
|
+
if (!(key in store)) return false;
|
|
250
|
+
delete store[key];
|
|
251
|
+
try {
|
|
252
|
+
_deps.fs.writeFileSync(file, JSON.stringify(store, null, 2) + "\n", "utf-8");
|
|
253
|
+
} catch {
|
|
254
|
+
/* best-effort */
|
|
255
|
+
}
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** True when a record's access token is missing or within `skewMs` of expiry. */
|
|
260
|
+
export function isTokenExpired(record, { skewMs = 60_000 } = {}) {
|
|
261
|
+
if (!record || !record.access_token) return true;
|
|
262
|
+
if (!record.expires_at) return false; // no expiry info → assume valid
|
|
263
|
+
return _deps.now() >= record.expires_at - skewMs;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Return a valid bearer token for a server, refreshing if expired. Returns the
|
|
268
|
+
* access_token string, or null if there's no stored token / refresh failed.
|
|
269
|
+
*/
|
|
270
|
+
export async function ensureValidToken(serverUrl) {
|
|
271
|
+
const record = getStoredToken(serverUrl);
|
|
272
|
+
if (!record) return null;
|
|
273
|
+
if (!isTokenExpired(record)) return record.access_token;
|
|
274
|
+
if (!record.refresh_token || !record.endpoints?.token_endpoint) {
|
|
275
|
+
return record.access_token || null; // can't refresh → use what we have
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const tok = await refreshAccessToken(
|
|
279
|
+
{ token_endpoint: record.endpoints.token_endpoint },
|
|
280
|
+
{ refreshToken: record.refresh_token, clientId: record.client_id },
|
|
281
|
+
);
|
|
282
|
+
const updated = saveStoredToken(serverUrl, { ...record, ...tok });
|
|
283
|
+
return updated.access_token;
|
|
284
|
+
} catch {
|
|
285
|
+
return record.access_token || null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ── interactive orchestrator ──────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
function defaultOpenBrowser(url) {
|
|
292
|
+
const platform = process.platform;
|
|
293
|
+
const cmd =
|
|
294
|
+
platform === "win32" ? "cmd" : platform === "darwin" ? "open" : "xdg-open";
|
|
295
|
+
const args =
|
|
296
|
+
platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
297
|
+
try {
|
|
298
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
299
|
+
child.unref?.();
|
|
300
|
+
return true;
|
|
301
|
+
} catch {
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Wait for the OAuth redirect on a localhost callback server; resolve {code,state}. */
|
|
307
|
+
function waitForCallback({ port, host = "127.0.0.1", path = "/callback", timeout = 300_000 }) {
|
|
308
|
+
return new Promise((resolve, reject) => {
|
|
309
|
+
const server = _deps.createServer((req, res) => {
|
|
310
|
+
let u;
|
|
311
|
+
try {
|
|
312
|
+
u = new URL(req.url, `http://${host}:${port}`);
|
|
313
|
+
} catch {
|
|
314
|
+
res.writeHead(400);
|
|
315
|
+
res.end("bad request");
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (u.pathname !== path) {
|
|
319
|
+
res.writeHead(404);
|
|
320
|
+
res.end("not found");
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const code = u.searchParams.get("code");
|
|
324
|
+
const state = u.searchParams.get("state");
|
|
325
|
+
const error = u.searchParams.get("error");
|
|
326
|
+
res.writeHead(200, { "content-type": "text/html" });
|
|
327
|
+
res.end(
|
|
328
|
+
`<html><body style="font-family:sans-serif"><h3>${error ? "Authorization failed" : "Authorized — you can close this tab."}</h3></body></html>`,
|
|
329
|
+
);
|
|
330
|
+
server.close();
|
|
331
|
+
clearTimeout(timer);
|
|
332
|
+
if (error) reject(new Error(`authorization error: ${error}`));
|
|
333
|
+
else if (!code) reject(new Error("no authorization code in callback"));
|
|
334
|
+
else resolve({ code, state });
|
|
335
|
+
});
|
|
336
|
+
const timer = setTimeout(() => {
|
|
337
|
+
server.close();
|
|
338
|
+
reject(new Error("timed out waiting for the OAuth callback"));
|
|
339
|
+
}, timeout);
|
|
340
|
+
server.on("error", reject);
|
|
341
|
+
server.listen(port, host);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Run the full interactive Authorization Code + PKCE flow and persist the token.
|
|
347
|
+
*
|
|
348
|
+
* @param {string} serverUrl
|
|
349
|
+
* @param {object} [opts] { scope, clientId, port=53682, host, redirectPath,
|
|
350
|
+
* timeout, writeOut }
|
|
351
|
+
* @returns {Promise<{server, access_token, ...}>} the stored record
|
|
352
|
+
*/
|
|
353
|
+
export async function authorizeInteractive(serverUrl, opts = {}) {
|
|
354
|
+
const {
|
|
355
|
+
scope,
|
|
356
|
+
clientId: cfgClientId,
|
|
357
|
+
port = 53682,
|
|
358
|
+
host = "127.0.0.1",
|
|
359
|
+
redirectPath = "/callback",
|
|
360
|
+
timeout = 300_000,
|
|
361
|
+
writeOut = (s) => process.stdout.write(s),
|
|
362
|
+
} = opts;
|
|
363
|
+
|
|
364
|
+
const metadata = await discoverAuthMetadata(serverUrl);
|
|
365
|
+
const redirectUri = `http://${host}:${port}${redirectPath}`;
|
|
366
|
+
|
|
367
|
+
let clientId = cfgClientId;
|
|
368
|
+
let clientSecret = null;
|
|
369
|
+
if (!clientId) {
|
|
370
|
+
const reg = await registerClient(metadata, { redirectUri });
|
|
371
|
+
clientId = reg.clientId;
|
|
372
|
+
clientSecret = reg.clientSecret;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const pkce = generatePkce();
|
|
376
|
+
const state = randomState();
|
|
377
|
+
const authorizeUrl = buildAuthorizeUrl(metadata, {
|
|
378
|
+
clientId,
|
|
379
|
+
redirectUri,
|
|
380
|
+
scope: scope || (metadata.scopes_supported ? metadata.scopes_supported.join(" ") : undefined),
|
|
381
|
+
codeChallenge: pkce.challenge,
|
|
382
|
+
state,
|
|
383
|
+
resource: serverUrl,
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const callbackPromise = waitForCallback({ port, host, path: redirectPath, timeout });
|
|
387
|
+
const opened = _deps.openBrowser(authorizeUrl);
|
|
388
|
+
writeOut(
|
|
389
|
+
(opened
|
|
390
|
+
? "Opened your browser to authorize.\n"
|
|
391
|
+
: "Open this URL in your browser to authorize:\n") +
|
|
392
|
+
` ${authorizeUrl}\n`,
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
const { code, state: returnedState } = await callbackPromise;
|
|
396
|
+
if (returnedState !== state) throw new Error("OAuth state mismatch (possible CSRF)");
|
|
397
|
+
|
|
398
|
+
const tok = await exchangeCodeForToken(metadata, {
|
|
399
|
+
code,
|
|
400
|
+
codeVerifier: pkce.verifier,
|
|
401
|
+
clientId,
|
|
402
|
+
clientSecret,
|
|
403
|
+
redirectUri,
|
|
404
|
+
resource: serverUrl,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
return saveStoredToken(serverUrl, {
|
|
408
|
+
...tok,
|
|
409
|
+
client_id: clientId,
|
|
410
|
+
endpoints: {
|
|
411
|
+
authorization_endpoint: metadata.authorization_endpoint,
|
|
412
|
+
token_endpoint: metadata.token_endpoint,
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* output-styles — Claude-Code `/output-style` parity. A named, reusable persona
|
|
3
|
+
* layered onto the agent's system prompt, so `cc agent` can act as a different
|
|
4
|
+
* kind of assistant while keeping its core coding capabilities.
|
|
5
|
+
*
|
|
6
|
+
* Styles are markdown files with frontmatter (`name`, `description`); the body
|
|
7
|
+
* is appended to the system prompt (after the base + `--append-system-prompt`).
|
|
8
|
+
* Discovered from `.chainlesschain/output-styles/` and `.claude/output-styles/`
|
|
9
|
+
* (project) + `~/.claude/output-styles/` (personal); a couple of built-ins ship
|
|
10
|
+
* so it works with no files. The active style comes from `--output-style`, then
|
|
11
|
+
* the `outputStyle` field in `.claude/settings.json`.
|
|
12
|
+
*
|
|
13
|
+
* Pure + zero-dep frontmatter parse (mirrors slash-commands — no js-yaml).
|
|
14
|
+
* `_deps` injection (fs / homedir) for tests.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fsDefault from "node:fs";
|
|
18
|
+
import pathDefault from "node:path";
|
|
19
|
+
import { homedir as homedirDefault } from "node:os";
|
|
20
|
+
|
|
21
|
+
export const _deps = {
|
|
22
|
+
fs: fsDefault,
|
|
23
|
+
path: pathDefault,
|
|
24
|
+
homedir: homedirDefault,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Built-in styles (shadowed by a same-named file). `default` = no-op. */
|
|
28
|
+
export const BUILTIN_OUTPUT_STYLES = Object.freeze({
|
|
29
|
+
default: {
|
|
30
|
+
name: "default",
|
|
31
|
+
description: "Standard coding assistant (no persona overlay).",
|
|
32
|
+
body: "",
|
|
33
|
+
builtin: true,
|
|
34
|
+
},
|
|
35
|
+
explanatory: {
|
|
36
|
+
name: "explanatory",
|
|
37
|
+
description: "Explains the reasoning and trade-offs behind changes.",
|
|
38
|
+
body: [
|
|
39
|
+
"## Output style: Explanatory",
|
|
40
|
+
"As you work, weave in brief `★ Insight` notes that explain *why* you chose",
|
|
41
|
+
"an approach and any non-obvious trade-offs, so the user learns from the",
|
|
42
|
+
"changes. Keep insights short (1–2 sentences) and tied to what you just did;",
|
|
43
|
+
"do not pad routine steps.",
|
|
44
|
+
].join("\n"),
|
|
45
|
+
builtin: true,
|
|
46
|
+
},
|
|
47
|
+
learning: {
|
|
48
|
+
name: "learning",
|
|
49
|
+
description: "Collaborative — leaves small instructive pieces for the user.",
|
|
50
|
+
body: [
|
|
51
|
+
"## Output style: Learning",
|
|
52
|
+
"Work collaboratively. When a small, well-scoped piece of the task would be",
|
|
53
|
+
"instructive for the user to write themselves, pause and insert a",
|
|
54
|
+
"`TODO(you):` marker with a one-line explanation of what to do and why,",
|
|
55
|
+
"instead of writing that piece — then continue with the rest. Reserve this",
|
|
56
|
+
"for genuinely educational spots, not boilerplate.",
|
|
57
|
+
].join("\n"),
|
|
58
|
+
builtin: true,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/** Parse `--- ... ---` frontmatter (zero-dep, camelCases keys). */
|
|
63
|
+
function parseFrontmatter(content) {
|
|
64
|
+
const text = String(content || "");
|
|
65
|
+
const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
66
|
+
if (!m) return { data: {}, body: text.trim() };
|
|
67
|
+
const data = {};
|
|
68
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
69
|
+
const trimmed = line.trim();
|
|
70
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
71
|
+
const colon = trimmed.indexOf(":");
|
|
72
|
+
if (colon <= 0) continue;
|
|
73
|
+
const key = trimmed.slice(0, colon).trim();
|
|
74
|
+
let value = trimmed.slice(colon + 1).trim();
|
|
75
|
+
value = value.replace(/^(['"])([\s\S]*)\1$/, "$2");
|
|
76
|
+
const camel = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
77
|
+
data[camel] = value;
|
|
78
|
+
}
|
|
79
|
+
return { data, body: (m[2] || "").trim() };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Directories scanned for style files (project first, then personal). */
|
|
83
|
+
function styleDirs(cwd, home) {
|
|
84
|
+
const { path } = _deps;
|
|
85
|
+
return [
|
|
86
|
+
{ dir: path.join(cwd, ".chainlesschain", "output-styles"), scope: "project" },
|
|
87
|
+
{ dir: path.join(cwd, ".claude", "output-styles"), scope: "project" },
|
|
88
|
+
{ dir: path.join(home, ".claude", "output-styles"), scope: "personal" },
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Discover all styles: built-ins + files (a file shadows a built-in by name). */
|
|
93
|
+
export function discoverOutputStyles(cwd = process.cwd(), opts = {}) {
|
|
94
|
+
const { fs, path } = _deps;
|
|
95
|
+
const home = opts.home || _deps.homedir();
|
|
96
|
+
const byName = new Map();
|
|
97
|
+
for (const b of Object.values(BUILTIN_OUTPUT_STYLES)) {
|
|
98
|
+
byName.set(b.name, { ...b });
|
|
99
|
+
}
|
|
100
|
+
// Files win over built-ins; project wins over personal (scan personal first).
|
|
101
|
+
for (const { dir, scope } of styleDirs(cwd, home).reverse()) {
|
|
102
|
+
let entries;
|
|
103
|
+
try {
|
|
104
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
105
|
+
} catch {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
for (const e of entries) {
|
|
109
|
+
if (!e.isFile() || !e.name.endsWith(".md")) continue;
|
|
110
|
+
const file = path.join(dir, e.name);
|
|
111
|
+
let content;
|
|
112
|
+
try {
|
|
113
|
+
content = fs.readFileSync(file, "utf-8");
|
|
114
|
+
} catch {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const { data, body } = parseFrontmatter(content);
|
|
118
|
+
const name = (data.name || e.name.replace(/\.md$/, "")).trim();
|
|
119
|
+
if (!name) continue;
|
|
120
|
+
byName.set(name, {
|
|
121
|
+
name,
|
|
122
|
+
description: data.description || "",
|
|
123
|
+
body,
|
|
124
|
+
scope,
|
|
125
|
+
file,
|
|
126
|
+
builtin: false,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return [...byName.values()];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Get one style by name (case-insensitive), or null. */
|
|
134
|
+
export function getOutputStyle(name, cwd = process.cwd(), opts = {}) {
|
|
135
|
+
if (!name) return null;
|
|
136
|
+
const target = String(name).trim().toLowerCase();
|
|
137
|
+
return (
|
|
138
|
+
discoverOutputStyles(cwd, opts).find(
|
|
139
|
+
(s) => s.name.toLowerCase() === target,
|
|
140
|
+
) || null
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Read the `outputStyle` default from the settings.json hierarchy (last wins). */
|
|
145
|
+
export function settingsDefaultOutputStyle(cwd = process.cwd(), opts = {}) {
|
|
146
|
+
const { fs, path } = _deps;
|
|
147
|
+
const home = opts.home || _deps.homedir();
|
|
148
|
+
const files = [
|
|
149
|
+
path.join(home, ".claude", "settings.json"),
|
|
150
|
+
path.join(cwd, ".claude", "settings.json"),
|
|
151
|
+
path.join(cwd, ".claude", "settings.local.json"),
|
|
152
|
+
];
|
|
153
|
+
let value = null;
|
|
154
|
+
for (const f of files) {
|
|
155
|
+
try {
|
|
156
|
+
if (!fs.existsSync(f)) continue;
|
|
157
|
+
const data = JSON.parse(fs.readFileSync(f, "utf-8"));
|
|
158
|
+
if (data && typeof data.outputStyle === "string" && data.outputStyle.trim()) {
|
|
159
|
+
value = data.outputStyle.trim();
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
// ignore malformed
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return value;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Resolve the active style's body to append to the system prompt.
|
|
170
|
+
* Precedence: explicit name → settings.json `outputStyle` → none.
|
|
171
|
+
* Returns `{ name, body }` (body may be "" for `default`) or null if unresolved.
|
|
172
|
+
*/
|
|
173
|
+
export function resolveOutputStyle(explicitName, cwd = process.cwd(), opts = {}) {
|
|
174
|
+
const name = (explicitName || settingsDefaultOutputStyle(cwd, opts) || "").trim();
|
|
175
|
+
if (!name) return null;
|
|
176
|
+
const style = getOutputStyle(name, cwd, opts);
|
|
177
|
+
if (!style) return { name, body: "", missing: true };
|
|
178
|
+
return { name: style.name, body: style.body || "" };
|
|
179
|
+
}
|