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,325 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* permission-rules — Claude-Code `permissions.{allow,ask,deny}` rule engine.
|
|
5
|
+
*
|
|
6
|
+
* A rule is a string `Tool(pattern)` (or a bare `Tool` matching every call of
|
|
7
|
+
* that tool). `Tool` may be written with the Claude-Code umbrella name (Bash,
|
|
8
|
+
* Read, Write, Edit, WebFetch, Task, …) or this CLI's own tool name (run_shell,
|
|
9
|
+
* read_file, write_file, …) — both resolve to the same family.
|
|
10
|
+
*
|
|
11
|
+
* Bash(git push:*) → run_shell whose command starts with "git push"
|
|
12
|
+
* Bash(npm run test:*) → run_shell starting with "npm run test"
|
|
13
|
+
* Read(./src/**) → read_file/list_dir on a path under <cwd>/src
|
|
14
|
+
* Edit(//etc/**) → edit_file on an absolute path under /etc
|
|
15
|
+
* WebFetch(domain:example.com) → web_fetch of https://example.com/…
|
|
16
|
+
* Bash → every run_shell call
|
|
17
|
+
*
|
|
18
|
+
* Pure + self-contained (no glob dependency — `globToRegExp` is built in, the
|
|
19
|
+
* repo avoids pulling minimatch/picomatch). Decision precedence is
|
|
20
|
+
* deny > ask > allow; no match returns `{ decision: null }` so callers fall
|
|
21
|
+
* back to the existing risk-tier / shell-policy logic unchanged.
|
|
22
|
+
*
|
|
23
|
+
* This module only *decides*; wiring it into the agent tool loop is a separate
|
|
24
|
+
* step (so it can ship + be unit-tested in isolation).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const path = require("node:path");
|
|
28
|
+
const os = require("node:os");
|
|
29
|
+
|
|
30
|
+
const DECISIONS = Object.freeze({
|
|
31
|
+
DENY: "deny",
|
|
32
|
+
ASK: "ask",
|
|
33
|
+
ALLOW: "allow",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Umbrella (Claude-Code) tool name → the concrete CLI tool names it covers.
|
|
38
|
+
* A rule written with either side resolves to the same family.
|
|
39
|
+
*/
|
|
40
|
+
const TOOL_GROUPS = Object.freeze({
|
|
41
|
+
bash: ["run_shell"],
|
|
42
|
+
read: ["read_file", "list_dir"],
|
|
43
|
+
grep: ["search_files"],
|
|
44
|
+
glob: ["search_files"],
|
|
45
|
+
write: ["write_file"],
|
|
46
|
+
edit: ["edit_file", "edit_file_hashed"],
|
|
47
|
+
webfetch: ["web_fetch"],
|
|
48
|
+
websearch: ["web_search"],
|
|
49
|
+
task: ["spawn_sub_agent"],
|
|
50
|
+
skill: ["run_skill", "list_skills"],
|
|
51
|
+
runcode: ["run_code"],
|
|
52
|
+
git: ["git"],
|
|
53
|
+
todowrite: ["todo_write"],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/** Tools whose match target is a shell/command string (prefix-style matching). */
|
|
57
|
+
const COMMAND_TOOLS = new Set(["run_shell", "run_code", "git"]);
|
|
58
|
+
/** Tools whose match target is a filesystem path. */
|
|
59
|
+
const PATH_TOOLS = new Set([
|
|
60
|
+
"read_file",
|
|
61
|
+
"list_dir",
|
|
62
|
+
"search_files",
|
|
63
|
+
"write_file",
|
|
64
|
+
"edit_file",
|
|
65
|
+
"edit_file_hashed",
|
|
66
|
+
]);
|
|
67
|
+
/** Tools whose match target is a URL. */
|
|
68
|
+
const URL_TOOLS = new Set(["web_fetch"]);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Does a rule's tool token apply to the concrete tool being evaluated?
|
|
72
|
+
* `Bash` → run_shell; `read_file` → read_file only; unknown tokens (e.g.
|
|
73
|
+
* `mcp__srv__do`) fall back to a case-insensitive exact match.
|
|
74
|
+
*/
|
|
75
|
+
function toolMatches(ruleTool, actualTool) {
|
|
76
|
+
const r = String(ruleTool || "").toLowerCase();
|
|
77
|
+
const a = String(actualTool || "");
|
|
78
|
+
if (Object.prototype.hasOwnProperty.call(TOOL_GROUPS, r)) {
|
|
79
|
+
return TOOL_GROUPS[r].includes(a);
|
|
80
|
+
}
|
|
81
|
+
return r === a.toLowerCase();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parse a rule string into `{ raw, tool, pattern }` (pattern null = bare tool
|
|
86
|
+
* rule). Returns null for a malformed/empty rule so callers can skip it.
|
|
87
|
+
*/
|
|
88
|
+
function parseRule(rule) {
|
|
89
|
+
const raw = String(rule || "").trim();
|
|
90
|
+
if (!raw) return null;
|
|
91
|
+
const m = raw.match(/^([A-Za-z_][\w-]*)\s*(?:\(([\s\S]*)\))?$/);
|
|
92
|
+
if (!m) return null;
|
|
93
|
+
const tool = m[1];
|
|
94
|
+
const pattern = m[2] === undefined ? null : m[2].trim();
|
|
95
|
+
return { raw, tool, pattern };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Convert a glob (`*`, `**`, `?`) into an anchored RegExp. Slash-normalized. */
|
|
99
|
+
function globToRegExp(glob) {
|
|
100
|
+
const s = String(glob || "");
|
|
101
|
+
let re = "";
|
|
102
|
+
for (let i = 0; i < s.length; i++) {
|
|
103
|
+
const c = s[i];
|
|
104
|
+
if (c === "*") {
|
|
105
|
+
if (s[i + 1] === "*") {
|
|
106
|
+
re += ".*";
|
|
107
|
+
i++;
|
|
108
|
+
if (s[i + 1] === "/") i++; // `**/` also matches zero segments
|
|
109
|
+
} else {
|
|
110
|
+
re += "[^/]*";
|
|
111
|
+
}
|
|
112
|
+
} else if (c === "?") {
|
|
113
|
+
re += "[^/]";
|
|
114
|
+
} else {
|
|
115
|
+
re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return new RegExp("^" + re + "$");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Normalize a filesystem path to forward slashes for glob comparison. */
|
|
122
|
+
function toSlash(p) {
|
|
123
|
+
return String(p || "").replace(/\\/g, "/");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Resolve the concrete value a path-pattern should be matched against, and the
|
|
128
|
+
* pattern itself, both as absolute slash-normalized strings.
|
|
129
|
+
* ./x or x → relative to cwd
|
|
130
|
+
* //abs/x → absolute (leading `//` is Claude-Code's absolute marker)
|
|
131
|
+
* ~/x → home
|
|
132
|
+
*/
|
|
133
|
+
function resolvePathPattern(pattern, cwd) {
|
|
134
|
+
let pat = String(pattern || "");
|
|
135
|
+
if (pat.startsWith("//")) {
|
|
136
|
+
pat = pat.slice(1); // `//etc/**` → `/etc/**`
|
|
137
|
+
} else if (pat.startsWith("~/") || pat === "~") {
|
|
138
|
+
pat = path.join(os.homedir(), pat.slice(1));
|
|
139
|
+
} else {
|
|
140
|
+
pat = path.resolve(cwd || process.cwd(), pat);
|
|
141
|
+
}
|
|
142
|
+
return toSlash(pat);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Extract the match target (command / path / url) for a tool's args. */
|
|
146
|
+
function extractTarget(actualTool, args, cwd) {
|
|
147
|
+
const a = args || {};
|
|
148
|
+
if (COMMAND_TOOLS.has(actualTool)) {
|
|
149
|
+
return { kind: "command", value: String(a.command || "").trim() };
|
|
150
|
+
}
|
|
151
|
+
if (PATH_TOOLS.has(actualTool)) {
|
|
152
|
+
const raw = a.path || a.file_path || a.dir || a.directory || "";
|
|
153
|
+
if (!raw) return { kind: "path", value: null };
|
|
154
|
+
return {
|
|
155
|
+
kind: "path",
|
|
156
|
+
value: toSlash(path.resolve(cwd || process.cwd(), String(raw))),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (URL_TOOLS.has(actualTool)) {
|
|
160
|
+
return { kind: "url", value: String(a.url || "").trim() };
|
|
161
|
+
}
|
|
162
|
+
return { kind: "none", value: null };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Match a shell command against a Claude-Code Bash-style pattern. */
|
|
166
|
+
function matchCommand(pattern, command) {
|
|
167
|
+
const cmd = String(command || "").trim();
|
|
168
|
+
// `prefix:*` → starts-with prefix (CC idiom: Bash(git push:*))
|
|
169
|
+
if (pattern.endsWith(":*")) {
|
|
170
|
+
const prefix = pattern.slice(0, -2).trim();
|
|
171
|
+
return cmd === prefix || cmd.startsWith(prefix);
|
|
172
|
+
}
|
|
173
|
+
if (pattern.includes("*")) {
|
|
174
|
+
return globToRegExp(pattern).test(cmd);
|
|
175
|
+
}
|
|
176
|
+
return cmd === pattern;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Match a URL against a `domain:host` or plain glob pattern. */
|
|
180
|
+
function matchUrl(pattern, url) {
|
|
181
|
+
const u = String(url || "");
|
|
182
|
+
if (pattern.startsWith("domain:")) {
|
|
183
|
+
const host = pattern.slice("domain:".length).trim();
|
|
184
|
+
let actualHost = "";
|
|
185
|
+
try {
|
|
186
|
+
actualHost = new URL(u).host;
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
return globToRegExp(host).test(actualHost);
|
|
191
|
+
}
|
|
192
|
+
return globToRegExp(pattern).test(u);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Does `pattern` (the inside of `Tool(...)`, or null for a bare rule) match the
|
|
197
|
+
* given tool call? `null` pattern always matches.
|
|
198
|
+
*/
|
|
199
|
+
function matchPattern(pattern, actualTool, args, cwd) {
|
|
200
|
+
if (pattern === null) return true;
|
|
201
|
+
const target = extractTarget(actualTool, args, cwd);
|
|
202
|
+
if (target.kind === "command") {
|
|
203
|
+
return target.value ? matchCommand(pattern, target.value) : false;
|
|
204
|
+
}
|
|
205
|
+
if (target.kind === "url") {
|
|
206
|
+
return target.value ? matchUrl(pattern, target.value) : false;
|
|
207
|
+
}
|
|
208
|
+
if (target.kind === "path") {
|
|
209
|
+
if (!target.value) return false;
|
|
210
|
+
return globToRegExp(resolvePathPattern(pattern, cwd)).test(target.value);
|
|
211
|
+
}
|
|
212
|
+
// Tool has no match target but the rule specified a pattern → no match.
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Evaluate a tool call against a ruleset.
|
|
218
|
+
*
|
|
219
|
+
* @param {object} input
|
|
220
|
+
* @param {string} input.tool concrete tool name (run_shell, …)
|
|
221
|
+
* @param {object} [input.args] tool arguments
|
|
222
|
+
* @param {string} [input.cwd] base dir for relative path patterns
|
|
223
|
+
* @param {object} input.rules { allow:[], ask:[], deny:[] } of strings
|
|
224
|
+
* @returns {{ decision: 'deny'|'ask'|'allow'|null, rule: string|null }}
|
|
225
|
+
*/
|
|
226
|
+
function evaluatePermissionRules({ tool, args = {}, cwd, rules } = {}) {
|
|
227
|
+
const set = rules || {};
|
|
228
|
+
const order = [
|
|
229
|
+
[DECISIONS.DENY, set.deny],
|
|
230
|
+
[DECISIONS.ASK, set.ask],
|
|
231
|
+
[DECISIONS.ALLOW, set.allow],
|
|
232
|
+
];
|
|
233
|
+
for (const [decision, list] of order) {
|
|
234
|
+
if (!Array.isArray(list)) continue;
|
|
235
|
+
for (const entry of list) {
|
|
236
|
+
const parsed = parseRule(entry);
|
|
237
|
+
if (!parsed) continue;
|
|
238
|
+
if (!toolMatches(parsed.tool, tool)) continue;
|
|
239
|
+
if (matchPattern(parsed.pattern, tool, args, cwd)) {
|
|
240
|
+
return { decision, rule: parsed.raw };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return { decision: null, rule: null };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Concrete tool name → the umbrella token a suggested rule should be written
|
|
248
|
+
* with (Claude-Code style; what users expect to see in settings.json). */
|
|
249
|
+
const SUGGEST_UMBRELLA = Object.freeze({
|
|
250
|
+
run_shell: "Bash",
|
|
251
|
+
run_code: "Bash",
|
|
252
|
+
git: "git",
|
|
253
|
+
read_file: "Read",
|
|
254
|
+
list_dir: "Read",
|
|
255
|
+
search_files: "Grep",
|
|
256
|
+
write_file: "Write",
|
|
257
|
+
edit_file: "Edit",
|
|
258
|
+
edit_file_hashed: "Edit",
|
|
259
|
+
web_fetch: "WebFetch",
|
|
260
|
+
web_search: "WebSearch",
|
|
261
|
+
spawn_sub_agent: "Task",
|
|
262
|
+
run_skill: "Skill",
|
|
263
|
+
list_skills: "Skill",
|
|
264
|
+
todo_write: "TodoWrite",
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
/** Commands whose first token is a dispatcher — keep 2 tokens in the prefix. */
|
|
268
|
+
const MULTI_VERB = new Set([
|
|
269
|
+
"git", "npm", "npx", "yarn", "pnpm", "docker", "kubectl", "cargo",
|
|
270
|
+
"go", "pip", "pip3", "python", "python3", "node", "dotnet", "gh", "brew",
|
|
271
|
+
]);
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Suggest a sensible `allow` rule string for a tool call, for the interactive
|
|
275
|
+
* "always allow" / don't-ask-again flow. Commands → `Bash(<1-2 token prefix>:*)`;
|
|
276
|
+
* paths → `<Umbrella>(<dir>/**)`; urls → `WebFetch(domain:<host>)`; otherwise a
|
|
277
|
+
* bare tool umbrella. Returns null if nothing meaningful can be derived.
|
|
278
|
+
*/
|
|
279
|
+
function suggestAllowRule(tool, args = {}) {
|
|
280
|
+
const umbrella = SUGGEST_UMBRELLA[tool] || tool;
|
|
281
|
+
if (COMMAND_TOOLS.has(tool)) {
|
|
282
|
+
const cmd = String(args.command || "").trim();
|
|
283
|
+
if (!cmd) return umbrella;
|
|
284
|
+
const tokens = cmd.split(/\s+/);
|
|
285
|
+
const keep = MULTI_VERB.has(tokens[0]) && tokens.length > 1 ? 2 : 1;
|
|
286
|
+
const prefix = tokens.slice(0, keep).join(" ");
|
|
287
|
+
return `${umbrella}(${prefix}:*)`;
|
|
288
|
+
}
|
|
289
|
+
if (PATH_TOOLS.has(tool)) {
|
|
290
|
+
const raw = args.path || args.file_path || args.dir || args.directory || "";
|
|
291
|
+
if (!raw) return umbrella;
|
|
292
|
+
const slash = String(raw).replace(/\\/g, "/");
|
|
293
|
+
const dir = slash.includes("/") ? slash.slice(0, slash.lastIndexOf("/")) : ".";
|
|
294
|
+
const base = dir === "" ? "/" : dir;
|
|
295
|
+
const norm = /^(\.|\/|~)/.test(base) ? base : `./${base}`;
|
|
296
|
+
return `${umbrella}(${norm}/**)`;
|
|
297
|
+
}
|
|
298
|
+
if (URL_TOOLS.has(tool)) {
|
|
299
|
+
try {
|
|
300
|
+
return `${umbrella}(domain:${new URL(String(args.url || "")).host})`;
|
|
301
|
+
} catch {
|
|
302
|
+
return umbrella;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return umbrella;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
module.exports = {
|
|
309
|
+
DECISIONS,
|
|
310
|
+
TOOL_GROUPS,
|
|
311
|
+
COMMAND_TOOLS,
|
|
312
|
+
PATH_TOOLS,
|
|
313
|
+
URL_TOOLS,
|
|
314
|
+
toolMatches,
|
|
315
|
+
parseRule,
|
|
316
|
+
globToRegExp,
|
|
317
|
+
resolvePathPattern,
|
|
318
|
+
extractTarget,
|
|
319
|
+
matchCommand,
|
|
320
|
+
matchUrl,
|
|
321
|
+
matchPattern,
|
|
322
|
+
evaluatePermissionRules,
|
|
323
|
+
suggestAllowRule,
|
|
324
|
+
SUGGEST_UMBRELLA,
|
|
325
|
+
};
|
|
@@ -6,12 +6,19 @@
|
|
|
6
6
|
* 1. PROVIDER_DEFAULTS[provider] — hand-curated baseline per provider
|
|
7
7
|
* 2. MODEL_INFERENCE(modelId) — model-specific overrides (e.g. o1
|
|
8
8
|
* disables temperature, claude-opus
|
|
9
|
-
*
|
|
9
|
+
* gets a larger maxTokens)
|
|
10
10
|
* 3. callOverrides — whatever the caller passes
|
|
11
11
|
*
|
|
12
12
|
* Later layers win at leaf keys; objects are merged recursively, arrays are
|
|
13
13
|
* replaced (not concatenated) to keep behavior predictable.
|
|
14
14
|
*
|
|
15
|
+
* NOTE: extended *thinking* is NOT decided here. It is opt-in via the agent's
|
|
16
|
+
* `--thinking` flag and resolved, model-aware, by `_anthropicThinkingParams`
|
|
17
|
+
* in agent-core.js — the single source of truth. This module contributes only
|
|
18
|
+
* `maxTokens` to the Anthropic request (chatWithTools destructures just that
|
|
19
|
+
* from the merge); a stray `anthropic.thinking` here was a second, divergent
|
|
20
|
+
* config that was never read, so it has been removed.
|
|
21
|
+
*
|
|
15
22
|
* @module provider-options
|
|
16
23
|
*/
|
|
17
24
|
|
|
@@ -21,7 +28,6 @@ export const PROVIDER_DEFAULTS = Object.freeze({
|
|
|
21
28
|
anthropic: {
|
|
22
29
|
maxTokens: 8192,
|
|
23
30
|
temperature: 1.0,
|
|
24
|
-
anthropic: { thinking: { type: "disabled" } },
|
|
25
31
|
},
|
|
26
32
|
openai: {
|
|
27
33
|
maxTokens: 4096,
|
|
@@ -66,12 +72,10 @@ export function inferModelOverrides(modelId) {
|
|
|
66
72
|
return { temperature: undefined, reasoning: { effort: "medium" } };
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
// Claude Opus —
|
|
75
|
+
// Claude Opus — larger default output budget. (Extended thinking is opt-in
|
|
76
|
+
// via `--thinking`, decided by `_anthropicThinkingParams`, not here.)
|
|
70
77
|
if (id.includes("opus-4") || id.includes("opus-3")) {
|
|
71
|
-
return {
|
|
72
|
-
maxTokens: 16384,
|
|
73
|
-
anthropic: { thinking: { type: "enabled", budgetTokens: 8000 } },
|
|
74
|
-
};
|
|
78
|
+
return { maxTokens: 16384 };
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
// Claude Haiku — cheaper, smaller output by default.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* settings-hook-events — fire non-tool `.claude/settings.json` hooks
|
|
5
|
+
* (UserPromptSubmit, SessionStart, and generic observe events) that live
|
|
6
|
+
* outside the executeTool seam.
|
|
7
|
+
*
|
|
8
|
+
* Reuses the same loader (collectHooks) + JSON protocol (hook-runner). Tool
|
|
9
|
+
* hooks (PreToolUse/PostToolUse) are dispatched inline in agent-core; this is
|
|
10
|
+
* the prompt/session lifecycle half. DB-backed session hooks (session-hooks.js
|
|
11
|
+
* `fireUserPromptSubmit`) stay separate + observe-only; these settings hooks
|
|
12
|
+
* are decision-capable (block) and can inject `additionalContext`.
|
|
13
|
+
*
|
|
14
|
+
* A hook's non-block stdout becomes context to inject: a JSON
|
|
15
|
+
* `{ "additionalContext": "..." }` field, or — for convenience — plain
|
|
16
|
+
* (non-JSON) stdout text. Exit 2 / `{decision:block}` aborts the turn.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const { collectHooks } = require("./settings-hooks.cjs");
|
|
20
|
+
const { runHooks } = require("./hook-runner.cjs");
|
|
21
|
+
|
|
22
|
+
/** Join the context emitted by the hooks that ran (additionalContext / plain stdout). */
|
|
23
|
+
function aggregateContext(results) {
|
|
24
|
+
const parts = [];
|
|
25
|
+
for (const r of results || []) {
|
|
26
|
+
if (r.additionalContext) {
|
|
27
|
+
parts.push(String(r.additionalContext));
|
|
28
|
+
} else if (r.exitCode === 0 && r.stdout) {
|
|
29
|
+
const s = String(r.stdout).trim();
|
|
30
|
+
if (s && s[0] !== "{") parts.push(s); // plain stdout = context to inject
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return parts.length > 0 ? parts.join("\n") : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* UserPromptSubmit settings hooks. A `block`/`ask` decision aborts the turn;
|
|
38
|
+
* otherwise any emitted context is returned for the caller to inject.
|
|
39
|
+
* @returns {{ blocked:boolean, reason?:string, hook?:string, additionalContext:string|null }}
|
|
40
|
+
*/
|
|
41
|
+
function runUserPromptSubmitHooks(settingsHooks, { prompt, cwd, sessionId } = {}) {
|
|
42
|
+
if (!settingsHooks) return { blocked: false, additionalContext: null };
|
|
43
|
+
const matched = collectHooks(settingsHooks, "UserPromptSubmit", "");
|
|
44
|
+
if (matched.length === 0) return { blocked: false, additionalContext: null };
|
|
45
|
+
const payload = {
|
|
46
|
+
hook_event_name: "UserPromptSubmit",
|
|
47
|
+
prompt: String(prompt || ""),
|
|
48
|
+
cwd,
|
|
49
|
+
session_id: sessionId || null,
|
|
50
|
+
};
|
|
51
|
+
const outcome = runHooks(matched, payload, { cwd, event: "UserPromptSubmit" });
|
|
52
|
+
if (outcome.decision === "block" || outcome.decision === "ask") {
|
|
53
|
+
return {
|
|
54
|
+
blocked: true,
|
|
55
|
+
reason: outcome.reason,
|
|
56
|
+
hook: outcome.hook,
|
|
57
|
+
additionalContext: null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return { blocked: false, additionalContext: aggregateContext(outcome.results) };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* SessionStart settings hooks (observe + context injection). The `source`
|
|
65
|
+
* (startup / resume / clear) is the matcher target.
|
|
66
|
+
* @returns {{ additionalContext:string|null }}
|
|
67
|
+
*/
|
|
68
|
+
function runSessionStartHooks(settingsHooks, { source, cwd, sessionId } = {}) {
|
|
69
|
+
if (!settingsHooks) return { additionalContext: null };
|
|
70
|
+
const matched = collectHooks(settingsHooks, "SessionStart", source || "");
|
|
71
|
+
if (matched.length === 0) return { additionalContext: null };
|
|
72
|
+
const payload = {
|
|
73
|
+
hook_event_name: "SessionStart",
|
|
74
|
+
source: source || "startup",
|
|
75
|
+
cwd,
|
|
76
|
+
session_id: sessionId || null,
|
|
77
|
+
};
|
|
78
|
+
const outcome = runHooks(matched, payload, { cwd, event: "SessionStart" });
|
|
79
|
+
return { additionalContext: aggregateContext(outcome.results) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Generic observe-only fire (SessionEnd / Stop / PreCompact). Returns the raw
|
|
84
|
+
* runHooks outcome so callers can read a block reason; never gates flow here.
|
|
85
|
+
*/
|
|
86
|
+
function runObserveHooks(settingsHooks, event, payload = {}, { cwd, matchTarget } = {}) {
|
|
87
|
+
if (!settingsHooks) return { decision: "continue", results: [] };
|
|
88
|
+
const matched = collectHooks(settingsHooks, event, matchTarget || "");
|
|
89
|
+
if (matched.length === 0) return { decision: "continue", results: [] };
|
|
90
|
+
return runHooks(
|
|
91
|
+
matched,
|
|
92
|
+
{ hook_event_name: event, cwd, ...payload },
|
|
93
|
+
{ cwd, event },
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = {
|
|
98
|
+
runUserPromptSubmitHooks,
|
|
99
|
+
runSessionStartHooks,
|
|
100
|
+
runObserveHooks,
|
|
101
|
+
aggregateContext,
|
|
102
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* settings-hooks — load Claude-Code `hooks` blocks from `.claude/settings.json`
|
|
5
|
+
* and resolve which command hooks fire for a given event + tool.
|
|
6
|
+
*
|
|
7
|
+
* Schema (Claude-Code parity):
|
|
8
|
+
* {
|
|
9
|
+
* "hooks": {
|
|
10
|
+
* "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command",
|
|
11
|
+
* "command": "./guard.sh", "timeout": 60 } ] } ],
|
|
12
|
+
* "PostToolUse": [ ... ]
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* Hook arrays are **concatenated** across the settings hierarchy (user <
|
|
17
|
+
* project < .local < --settings) — order is significant and there is no dedup
|
|
18
|
+
* (unlike permission rules, which union). Distinct from the DB-backed
|
|
19
|
+
* `cc hook add` registry: those stay observe-only; only these settings hooks
|
|
20
|
+
* get the stdin-JSON decision protocol (see hook-runner.cjs) + blocking power.
|
|
21
|
+
*
|
|
22
|
+
* Pure + self-contained (`compileMatcher` is inlined rather than imported from
|
|
23
|
+
* the ESM hook-manager, which a .cjs cannot `require`). `_deps.fs/homedir`
|
|
24
|
+
* injection follows the CLI testing convention.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const path = require("node:path");
|
|
28
|
+
const os = require("node:os");
|
|
29
|
+
const fsDefault = require("node:fs");
|
|
30
|
+
const { SUGGEST_UMBRELLA } = require("./permission-rules.cjs");
|
|
31
|
+
|
|
32
|
+
const _deps = { fs: fsDefault, homedir: () => os.homedir() };
|
|
33
|
+
|
|
34
|
+
/** Events this loader understands (PreToolUse/PostToolUse are wired first). */
|
|
35
|
+
const HOOK_EVENTS = Object.freeze([
|
|
36
|
+
"PreToolUse",
|
|
37
|
+
"PostToolUse",
|
|
38
|
+
"UserPromptSubmit",
|
|
39
|
+
"Stop",
|
|
40
|
+
"SessionStart",
|
|
41
|
+
"SessionEnd",
|
|
42
|
+
"PreCompact",
|
|
43
|
+
"Notification",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
/** Same hierarchy as settings-loader (user < project < .local < --settings). */
|
|
47
|
+
function settingsFiles(cwd, explicitFile) {
|
|
48
|
+
const home = _deps.homedir();
|
|
49
|
+
const list = [
|
|
50
|
+
path.join(home, ".claude", "settings.json"),
|
|
51
|
+
path.join(cwd, ".claude", "settings.json"),
|
|
52
|
+
path.join(cwd, ".claude", "settings.local.json"),
|
|
53
|
+
];
|
|
54
|
+
if (explicitFile) list.push(path.resolve(cwd, explicitFile));
|
|
55
|
+
return list;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readJson(file, onWarn) {
|
|
59
|
+
try {
|
|
60
|
+
if (!_deps.fs.existsSync(file)) return null;
|
|
61
|
+
const parsed = JSON.parse(_deps.fs.readFileSync(file, "utf-8"));
|
|
62
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
63
|
+
} catch (err) {
|
|
64
|
+
const msg = `hooks: ignoring malformed ${file} (${err.message})`;
|
|
65
|
+
if (typeof onWarn === "function") onWarn(msg);
|
|
66
|
+
else process.stderr.write(msg + "\n");
|
|
67
|
+
return null; // fail-open
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Load + concatenate the `hooks` blocks across the hierarchy.
|
|
73
|
+
* @returns {{ hooks: Record<string, Array<{matcher:string|null, hooks:Array<{type,command,timeout}>}>>, files:string[] }}
|
|
74
|
+
*/
|
|
75
|
+
function loadHooks({ cwd = process.cwd(), settingsFile, onWarn } = {}) {
|
|
76
|
+
const merged = {};
|
|
77
|
+
const files = [];
|
|
78
|
+
for (const file of settingsFiles(cwd, settingsFile)) {
|
|
79
|
+
const data = readJson(file, onWarn);
|
|
80
|
+
const block =
|
|
81
|
+
data && data.hooks && typeof data.hooks === "object" ? data.hooks : null;
|
|
82
|
+
if (!block) continue;
|
|
83
|
+
let contributed = false;
|
|
84
|
+
for (const [event, groups] of Object.entries(block)) {
|
|
85
|
+
if (!Array.isArray(groups)) continue;
|
|
86
|
+
for (const g of groups) {
|
|
87
|
+
if (!g || typeof g !== "object" || !Array.isArray(g.hooks)) continue;
|
|
88
|
+
const cmds = g.hooks.filter(
|
|
89
|
+
(h) => h && h.type === "command" && h.command,
|
|
90
|
+
);
|
|
91
|
+
if (cmds.length === 0) continue;
|
|
92
|
+
if (!merged[event]) merged[event] = [];
|
|
93
|
+
merged[event].push({ matcher: g.matcher ?? null, hooks: cmds });
|
|
94
|
+
contributed = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (contributed) files.push(file);
|
|
98
|
+
}
|
|
99
|
+
return { hooks: merged, files };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Compile a matcher into a test fn (pipe / wildcard / regex). Inlined twin of
|
|
104
|
+
* hook-manager.compileMatcher so this .cjs has no ESM dependency.
|
|
105
|
+
*/
|
|
106
|
+
function compileMatcher(pattern) {
|
|
107
|
+
if (!pattern || pattern === "*") return () => true;
|
|
108
|
+
if (pattern.startsWith("/") && pattern.lastIndexOf("/") > 0) {
|
|
109
|
+
const i = pattern.lastIndexOf("/");
|
|
110
|
+
try {
|
|
111
|
+
const re = new RegExp(pattern.slice(1, i), pattern.slice(i + 1));
|
|
112
|
+
return (v) => re.test(v);
|
|
113
|
+
} catch {
|
|
114
|
+
/* fall through to wildcard */
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (pattern.includes("|")) {
|
|
118
|
+
const ms = pattern.split("|").map((p) => compileMatcher(p.trim()));
|
|
119
|
+
return (v) => ms.some((m) => m(v));
|
|
120
|
+
}
|
|
121
|
+
const esc = pattern
|
|
122
|
+
.replace(/[.+^${}()[\]\\]/g, "\\$&")
|
|
123
|
+
.replace(/\*/g, ".*")
|
|
124
|
+
.replace(/\?/g, ".");
|
|
125
|
+
const re = new RegExp(`^${esc}$`);
|
|
126
|
+
return (v) => re.test(v);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Concrete tool name → Claude-Code umbrella token (Bash, Read, …). */
|
|
130
|
+
function umbrellaFor(tool) {
|
|
131
|
+
return SUGGEST_UMBRELLA[tool] || tool;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Ordered command hooks for an event whose matcher matches the tool. The
|
|
136
|
+
* matcher is tested against BOTH the CC umbrella (`Bash`) and the raw tool
|
|
137
|
+
* name (`run_shell`), so settings written either way fire.
|
|
138
|
+
*
|
|
139
|
+
* @returns {Array<{command:string, timeout?:number}>}
|
|
140
|
+
*/
|
|
141
|
+
function collectHooks(hooksBlock, event, toolName) {
|
|
142
|
+
const groups = (hooksBlock && hooksBlock[event]) || [];
|
|
143
|
+
const umbrella = umbrellaFor(toolName);
|
|
144
|
+
const raw = String(toolName || "");
|
|
145
|
+
const out = [];
|
|
146
|
+
for (const g of groups) {
|
|
147
|
+
const fn = compileMatcher(g.matcher);
|
|
148
|
+
if (fn(umbrella) || (raw && fn(raw))) {
|
|
149
|
+
for (const h of g.hooks) out.push({ command: h.command, timeout: h.timeout });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = {
|
|
156
|
+
loadHooks,
|
|
157
|
+
collectHooks,
|
|
158
|
+
compileMatcher,
|
|
159
|
+
umbrellaFor,
|
|
160
|
+
settingsFiles,
|
|
161
|
+
HOOK_EVENTS,
|
|
162
|
+
_deps,
|
|
163
|
+
};
|