chainlesschain 0.162.161 → 0.162.163
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-DMn528q-.js → AIOps-BQ0kgfA1.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-D9ZcDCJL.js → ActionButton-Dr1MDANe.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-A_K-8PX0.js → Analytics-CZ5vsaXW.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-D4pFW3X6.js → AppLayout-BIBga881.js} +5 -5
- package/src/assets/web-panel/assets/{Artifacts-Dz46COAd.js → Artifacts-vpXHRU3u.js} +1 -1
- package/src/assets/web-panel/assets/{Audit-cbMjbyg0.js → Audit-B8exuoub.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-Dtf88GAj.js → BackgroundAgents-923evRRN.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-C5KRxn52.js → Backup-sr_sswlQ.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-Dd2UK3BN.js → BaseInput-CnW-4XKT.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-lptKTFdG.js → Chat-FZCsTZIL.js} +5 -5
- package/src/assets/web-panel/assets/ChatBubbleRenderer-DDPd9qT0.js +1 -0
- package/src/assets/web-panel/assets/{Checkbox-_yspG6rG.js → Checkbox-DFrxB59H.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BGq_GAha.js → Codegen-_b6WEel8.js} +1 -1
- package/src/assets/web-panel/assets/{Col-CePor71g.js → Col-CkbnnZ-K.js} +1 -1
- package/src/assets/web-panel/assets/{Community-CLOWhIT7.js → Community-BxM_82zW.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CivJt2vT.js → Compact-DYinP45R.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-Btmo1GGE.js → Compliance-DyXp3sSP.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Ct35OlG4.js → Cowork-BEr2DhW7.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-CTc2GmhB.js → Cron-B-Vwo1hN.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-ZtiBETk1.js → Crosschain-DbLxKddd.js} +1 -1
- package/src/assets/web-panel/assets/{DID-BNqMQQXw.js → DID-BsQ_LFw9.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-BmzZd-9V.js → Dashboard-lVlXQHu5.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-CMVgvP2M.js → Dropdown-Beo_foO7.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-CcqQFAfT.js → EmailListRenderer-DszHIXED.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-CTqB1_4V.js → FamilyGuardDashboard-DcAwoh81.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-CiKOOzlb.js → Federation-CuJLa2eb.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-Wk-OT0gr.js → FormItemContext-Cxd1lxhc.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DbEQT6ue.js → GenericCardRenderer-BBvq5Jsw.js} +1 -1
- package/src/assets/web-panel/assets/{Git-0ZvCPSD9.js → Git-WqPKetx6.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CmVuKkPb.js → Governance-CWYtY3ke.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-CTWE3Jwg.js → Inference-BnX1iiNV.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-CBEIF00L.js → KnowledgeGraph-BJuM8GN5.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DLhxDwVG.js → Logs-DqLMNntm.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-BqYwEYou.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-BgsCBrnc.js → Marketplace-_vGfmhNl.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-ClBUGDS2.js → McpTools-BeJDOkDj.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-CnG_ZnYw.js → Memory-Da0EEZDO.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BUOAbR1Z.js → MobileBridge-DSuzcCCy.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-Dnt_9-WB.js → MobileProjects-D0RAk4e8.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-CLjt4kCD.js → Mtc-BE9YgdA0.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-f24U9Aes.js → MtcAudit-fC2BpO5S.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-GrmGQbSq.js → Multisig-D24Xxqw3.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-DADaWn-n.js → NLProgramming-Bs3oGvS8.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-xtf5RX6C.js → Notes-BFjmAA-3.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-Cd27Rgtx.js → NotificationSettings-BX_9Bhv0.js} +1 -1
- package/src/assets/web-panel/assets/{OrderTableRenderer-fXRxlDjl.js → OrderTableRenderer-wjkZwJXI.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-joCFlNB0.js → Organization-DBcdsVEP.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-B1HoURq0.js → Overflow-DDyaF9vC.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-DiwsKbT7.js → P2P-BB78L4he.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-Xe-BsW6S.js → PdhVaultBrowser-DqTiZFum.js} +3 -3
- package/src/assets/web-panel/assets/{Permissions-C23qi8kl.js → Permissions-INbnWv4j.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-BS1syX2f.js → PersonalDataHub-BST-VmTq.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-DRYwwDyh.js → Pipeline-CE7_tS2k.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BQKaDHPH.js → Privacy-hK5L8dOB.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-BC6F9xiG.js → ProjectInit-BvkkUoDn.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-ClFPc1Nd.js → ProjectSettings-u7_EUtkm.js} +2 -2
- package/src/assets/web-panel/assets/Projects-5Fmt_8Nw.js +1 -0
- package/src/assets/web-panel/assets/{Providers-CLWuOUNt.js → Providers-B9eanarR.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-DiqULTxb.js → QrScannerModal-C0dX5N2f.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-9A03P-E_.js → QuickAsk-CKTpG-PZ.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CJZAnbq_.js → Recommend-Px_w_XQM.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-C9A1R77h.js → RemoteSession-Di0fnYkM.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-BIZc6Rky.js → Reputation-vesnWjXt.js} +1 -1
- package/src/assets/web-panel/assets/{Row-DfIPNtuG.js → Row-exbY6LkT.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-C5znFcQ1.js → RssFeed-B3O3x70t.js} +2 -2
- package/src/assets/web-panel/assets/{Search-DSs2z3OX.js → Search-uRA0DhNz.js} +1 -1
- package/src/assets/web-panel/assets/{Security-CV6LcrLY.js → Security-CViqdH6K.js} +4 -4
- package/src/assets/web-panel/assets/{Services-cmhoX0Xj.js → Services-CkXS97WN.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-tCsg2po7.js → Skeleton-DxW5WYnE.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-BK1KUDiy.js → Skills-CLxmGFse.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-yRgyeA82.js → Sla-Cr5fziXq.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-c1mo1mlp.js → SpeechSettings-H0JSb3cp.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-koIAfuT3.js → SyncSettings-DMYWMae7.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-DkkreAaB.js → Tasks-CbYyDdF4.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-fy5Ctf9M.js → Templates-Cpc8JjiK.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-Bw70Jf6S.js → Tenant-CRh77Jda.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-BKh6Rx-4.js → Terminal-4D8tNBO5.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-Dg54WP0c.js → TimelineRenderer-D41jzzbG.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BvvD5SxJ.js → Tokens-C_jWEy6b.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-B7Ug9x4e.js → Trigger-Dk1ZUhsy.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-BjvuMORY.js → Trust-CLVy4l7m.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-CZqhkx8p.js → UkeySign-C-FeqFDj.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-ClR50lhX.js → VideoEditing-CqVZzyMm.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DlfppYjx.js → Wallet-DX9BW9pu.js} +2 -2
- package/src/assets/web-panel/assets/{WebAuthn-BirgVr8B.js → WebAuthn-7hQDpWw_.js} +2 -2
- package/src/assets/web-panel/assets/{WorkflowEditor-B8DP_bax.js → WorkflowEditor-6XqUAO0j.js} +1 -1
- package/src/assets/web-panel/assets/{chat-DA8AUZzL.js → chat-5R_vkaJG.js} +1 -1
- package/src/assets/web-panel/assets/{colors-C38pLGp1.js → colors-huUPZdC-.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-81rtr98o.js → compact-item-CfJlgisE.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-D34ixq8t.js → createContext-jO8heofr.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-DeIrCfH-.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-D4bzqTrZ.js → hasIn-DY8Vxqx7.js} +1 -1
- package/src/assets/web-panel/assets/{index-E1OJRXs4.js → index-A48PtXw9.js} +1 -1
- package/src/assets/web-panel/assets/{index-D3zCCZRS.js → index-B300RPnT.js} +3 -3
- package/src/assets/web-panel/assets/{index-Cd3KuNK5.js → index-B74OXFxC.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dmo3dFHb.js → index-BAON7TzU.js} +1 -1
- package/src/assets/web-panel/assets/{index-Ddk0e_Pi.js → index-BCqxyvqp.js} +1 -1
- package/src/assets/web-panel/assets/{index-9qoVpPt6.js → index-BHE8OIls.js} +1 -1
- package/src/assets/web-panel/assets/{index-DBFSo1NF.js → index-BQS-uPOK.js} +1 -1
- package/src/assets/web-panel/assets/{index-DUQLXLQM.js → index-BRXOP09X.js} +1 -1
- package/src/assets/web-panel/assets/{index-D6GfvEQG.js → index-BYLBPpt8.js} +1 -1
- package/src/assets/web-panel/assets/{index-DFyWKOBD.js → index-BfCx4pFT.js} +1 -1
- package/src/assets/web-panel/assets/{index-BP-zkXK8.js → index-BliaT02i.js} +1 -1
- package/src/assets/web-panel/assets/{index-DMgnRwrK.js → index-BmqGerpC.js} +1 -1
- package/src/assets/web-panel/assets/{index-BIP62a6r.js → index-BsBtRWbA.js} +1 -1
- package/src/assets/web-panel/assets/{index-DkHqMnn3.js → index-BtEXSoPL.js} +1 -1
- package/src/assets/web-panel/assets/index-BxUKMRpP.js +1 -0
- package/src/assets/web-panel/assets/{index-B8mkLM4x.js → index-Bxax9vQ6.js} +1 -1
- package/src/assets/web-panel/assets/{index-ZRJRItm0.js → index-C2iVSFpy.js} +1 -1
- package/src/assets/web-panel/assets/{index-CP5alL4t.js → index-CA6sO5VD.js} +1 -1
- package/src/assets/web-panel/assets/{index-BS3M5H9V.js → index-CA90TZxG.js} +1 -1
- package/src/assets/web-panel/assets/{index-DvfCEJG-.js → index-CLhBh-qE.js} +1 -1
- package/src/assets/web-panel/assets/{index-tKiC8T-4.js → index-CRElRhyS.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0QArPQt.js → index-Cbe9G0V4.js} +1 -1
- package/src/assets/web-panel/assets/{index-COe_cOLJ.js → index-ChvHubX3.js} +1 -1
- package/src/assets/web-panel/assets/{index-BvL_3J_n.js → index-Cis_Ip7H.js} +1 -1
- package/src/assets/web-panel/assets/{index-F76w1NiL.js → index-Cjxbu68P.js} +1 -1
- package/src/assets/web-panel/assets/{index-BlxEphl7.js → index-CzKmteNS.js} +1 -1
- package/src/assets/web-panel/assets/{index-CuS-UPoj.js → index-D0pet5Uy.js} +1 -1
- package/src/assets/web-panel/assets/{index-CbhE3j5j.js → index-D1VLuww0.js} +1 -1
- package/src/assets/web-panel/assets/{index-D_2VYPr1.js → index-DXBcvUnf.js} +1 -1
- package/src/assets/web-panel/assets/{index-DakfFZ-k.js → index-Db1cLWir.js} +1 -1
- package/src/assets/web-panel/assets/{index-fI5sXMLg.js → index-DoI2sZm7.js} +1 -1
- package/src/assets/web-panel/assets/index-DscU9iPI.js +1 -0
- package/src/assets/web-panel/assets/{index-CoVgZw_b.js → index-KRHroryI.js} +1 -1
- package/src/assets/web-panel/assets/{index-BvD7b2IK.js → index-d1xkbtNz.js} +1 -1
- package/src/assets/web-panel/assets/{index-BtGxuQrO.js → index-dKx57io7.js} +1 -1
- package/src/assets/web-panel/assets/{index-BqIhLAbA.js → index-j3O2EEJe.js} +1 -1
- package/src/assets/web-panel/assets/{index-BXd2_LmH.js → index-p85YaaKw.js} +1 -1
- package/src/assets/web-panel/assets/{index-DFHa1HIg.js → index-v3pTaEQm.js} +1 -1
- package/src/assets/web-panel/assets/{index-D0Ih7Hwn.js → index-vXs3B8GJ.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-ByyvflIR.js → initDefaultProps-MCB7-raZ.js} +1 -1
- package/src/assets/web-panel/assets/{motion-B3FQm2By.js → motion-UWDF9swV.js} +1 -1
- package/src/assets/web-panel/assets/{move-BSfns6Ef.js → move-CsTmgXZU.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-CTG1Ieri.js → mtc-parser-fQy6KGLg.js} +1 -1
- package/src/assets/web-panel/assets/{omit-DObM_2j6.js → omit-woPnEjd1.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-DOfelQ1t.js → pickAttrs-Bs2dZ1gf.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-CHZILds_.js → placementArrow-C7EVgPkg.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-REbucKSx.js → responsiveObserve-BH2kYCTN.js} +1 -1
- package/src/assets/web-panel/assets/{slide-C6EZzD7o.js → slide-BFBP2V_e.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-BxkWS-g3.js → statusUtils-xdDxDYse.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-Chklyoq6.js → styleChecker-aAWgPPQf.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-CRVwKrrI.js → useFlexGapSupport-DkGSAWoO.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-DV24ik_J.js → useFs-BrOih40N.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-C7vIo4Pg.js → usePersonalDataHub-eIYrgMqO.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-By-v5GlF.js → vnode-B8lutNSE.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-B1w9Am3r.js → zoom-CffNre7V.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/commands/agenda.js +254 -23
- package/src/commands/agent.js +105 -11
- package/src/commands/context.js +125 -14
- package/src/commands/hook.js +181 -7
- package/src/commands/plugin.js +239 -1
- package/src/commands/review.js +352 -2
- package/src/data/changelog.json +14 -4
- package/src/gateways/ws/remote-session-protocol.js +39 -0
- package/src/gateways/ws/session-protocol.js +5 -2
- package/src/lib/agent-schedule-store.js +289 -10
- package/src/lib/agents.js +19 -0
- package/src/lib/background-agent-supervisor.js +34 -7
- package/src/lib/channels/channel-manager.js +54 -2
- package/src/lib/cli-context-engineering.js +9 -2
- package/src/lib/context-breakdown.js +116 -0
- package/src/lib/docs-drift.js +166 -0
- package/src/lib/doctor-checkup.js +128 -0
- package/src/lib/hook-event-bus.cjs +0 -0
- package/src/lib/hook-event-log.cjs +193 -0
- package/src/lib/interaction-adapter.js +34 -4
- package/src/lib/json-schema-output.js +75 -12
- package/src/lib/json-schema-validate.js +831 -0
- package/src/lib/lsp/__tests__/diagnostics-scheduler.test.js +93 -0
- package/src/lib/lsp/diagnostics-scheduler.js +127 -0
- package/src/lib/operation-fingerprint.js +95 -0
- package/src/lib/plugin-runtime/capabilities.js +371 -0
- package/src/lib/plugin-runtime/capability-consent.js +175 -0
- package/src/lib/plugin-runtime/manifest.js +30 -0
- package/src/lib/plugin-runtime/policy.js +62 -0
- package/src/lib/plugin-runtime/scopes.js +20 -4
- package/src/lib/project-instructions.js +128 -0
- package/src/lib/remote-approval-bridge.js +16 -0
- package/src/lib/repl-goal.js +132 -0
- package/src/lib/repl-rewind.js +52 -0
- package/src/lib/review-pipeline.js +227 -0
- package/src/lib/runtime-checkup.js +318 -0
- package/src/lib/schedule-planner.js +17 -1
- package/src/lib/session-lifecycle.js +305 -0
- package/src/lib/settings-hook-events.cjs +52 -13
- package/src/lib/settings-loader.cjs +42 -0
- package/src/lib/side-effect-ledger-store.js +88 -0
- package/src/lib/side-effect-ledger.js +296 -0
- package/src/lib/sub-agent-context.js +74 -5
- package/src/lib/subagent-contract.js +383 -0
- package/src/lib/subagent-inheritance.js +112 -0
- package/src/lib/telemetry-ids.js +190 -0
- package/src/lib/turn-binding-store.js +119 -0
- package/src/lib/turn-binding.js +397 -0
- package/src/repl/agent-repl.js +136 -0
- package/src/repl/permission-tier.js +28 -0
- package/src/runtime/agent-core.js +387 -14
- package/src/runtime/coding-agent-contract-shared.cjs +60 -1
- package/src/runtime/headless-runner.js +267 -0
- package/src/runtime/headless-stream.js +60 -1
- package/src/assets/web-panel/assets/ChatBubbleRenderer-C2KLtwHz.js +0 -1
- package/src/assets/web-panel/assets/MarkdownRenderer-DxywvhTU.js +0 -1
- package/src/assets/web-panel/assets/Projects-Dn-f5aYe.js +0 -1
- package/src/assets/web-panel/assets/devWarning-CHDIEOeN.js +0 -1
- package/src/assets/web-panel/assets/index-CY1NURWZ.js +0 -1
- package/src/assets/web-panel/assets/index-DokUYqpq.js +0 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-source token attribution for `cc context --sources` — the P1
|
|
3
|
+
* large-monorepo "/context 按来源分解" lever. The base `cc context` view buckets
|
|
4
|
+
* a session's stored messages by ROLE; this adds a BY-SOURCE view that also
|
|
5
|
+
* attributes the project-memory instruction files (cc.md / CLAUDE.md / rules /
|
|
6
|
+
* @imports) that would be loaded for the current working directory, each with
|
|
7
|
+
* its own token cost and provenance — so a user can see WHICH instruction file
|
|
8
|
+
* (or message class) fills the context window.
|
|
9
|
+
*
|
|
10
|
+
* Pure: token counting for instruction files is delegated to the injected
|
|
11
|
+
* estimator (the same CJK-aware `estimateTokens` the window sizing uses);
|
|
12
|
+
* message-role buckets already arrive pre-counted. No I/O.
|
|
13
|
+
*
|
|
14
|
+
* Scope note: skills and MCP tool schemas are runtime tool definitions loaded
|
|
15
|
+
* when an agent actually runs — they are NOT part of a persisted transcript nor
|
|
16
|
+
* of the project-instruction set, so they are intentionally not attributed here.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import path from "path";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Display path for an instruction source: relative-to-cwd when the file lives
|
|
23
|
+
* inside cwd, else the absolute path (a `~/.claude/…` user-scope file, or an
|
|
24
|
+
* @import that escaped the tree, is clearer shown in full).
|
|
25
|
+
*/
|
|
26
|
+
export function relativizeInstructionPath(absPath, cwd) {
|
|
27
|
+
if (!absPath) return "";
|
|
28
|
+
if (!cwd) return absPath;
|
|
29
|
+
try {
|
|
30
|
+
const rel = path.relative(cwd, absPath);
|
|
31
|
+
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) return absPath;
|
|
32
|
+
return rel.split(path.sep).join("/");
|
|
33
|
+
} catch {
|
|
34
|
+
return absPath;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Per-file instruction sources from `loadProjectInstructions().files`.
|
|
40
|
+
*
|
|
41
|
+
* @param {Array<{path,scope,content,truncated}>} files
|
|
42
|
+
* @param {(t:string)=>number} estimateTokens
|
|
43
|
+
* @param {string} [cwd]
|
|
44
|
+
* @returns {{ sources: Array<{scope,source,path,tokens,truncated}>, total:number }}
|
|
45
|
+
*/
|
|
46
|
+
export function breakdownInstructionSources(files, estimateTokens, cwd) {
|
|
47
|
+
const est = typeof estimateTokens === "function" ? estimateTokens : () => 0;
|
|
48
|
+
const list = Array.isArray(files) ? files : [];
|
|
49
|
+
const sources = list.map((f) => ({
|
|
50
|
+
scope: f?.scope || "project",
|
|
51
|
+
source: relativizeInstructionPath(f?.path, cwd),
|
|
52
|
+
path: f?.path || "",
|
|
53
|
+
tokens: est(typeof f?.content === "string" ? f.content : ""),
|
|
54
|
+
truncated: Boolean(f?.truncated),
|
|
55
|
+
}));
|
|
56
|
+
const total = sources.reduce((s, x) => s + x.tokens, 0);
|
|
57
|
+
return { sources, total };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// role bucket key → { label, kind } for the ranked source list.
|
|
61
|
+
const MESSAGE_SOURCE_ROWS = [
|
|
62
|
+
["system", "system messages", "message"],
|
|
63
|
+
["user", "user messages", "message"],
|
|
64
|
+
["assistant", "assistant messages", "message"],
|
|
65
|
+
["tool", "tool results", "message"],
|
|
66
|
+
["toolCalls", "tool calls", "tool_calls"],
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Merge the project-memory instruction total with the per-role message buckets
|
|
71
|
+
* into ONE ranked source list (largest first) with each source's share of the
|
|
72
|
+
* combined by-source total. The message buckets are the same ones
|
|
73
|
+
* `categorizeContext` produces; instructions are folded in as a single group so
|
|
74
|
+
* the ranked view stays readable (per-file detail lives in the instructions
|
|
75
|
+
* breakdown).
|
|
76
|
+
*
|
|
77
|
+
* @param {object} args
|
|
78
|
+
* @param {number} [args.instructionTotal=0] Σ instruction-file tokens
|
|
79
|
+
* @param {object} [args.buckets={}] categorizeContext buckets
|
|
80
|
+
* @param {object} [args.counts={}] categorizeContext counts
|
|
81
|
+
* @returns {{ sources: Array<{kind,source,tokens,count,share}>, total:number }}
|
|
82
|
+
*/
|
|
83
|
+
export function rankContextSources({
|
|
84
|
+
instructionTotal = 0,
|
|
85
|
+
buckets = {},
|
|
86
|
+
counts = {},
|
|
87
|
+
} = {}) {
|
|
88
|
+
const rows = [];
|
|
89
|
+
if (instructionTotal > 0) {
|
|
90
|
+
rows.push({
|
|
91
|
+
kind: "instructions",
|
|
92
|
+
source: "project memory (instructions)",
|
|
93
|
+
tokens: instructionTotal,
|
|
94
|
+
count: null,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
for (const [key, label, kind] of MESSAGE_SOURCE_ROWS) {
|
|
98
|
+
const tok = buckets[key] || 0;
|
|
99
|
+
if (tok > 0) {
|
|
100
|
+
rows.push({
|
|
101
|
+
kind,
|
|
102
|
+
source: label,
|
|
103
|
+
tokens: tok,
|
|
104
|
+
count: counts[key] ?? null,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const total = rows.reduce((s, x) => s + x.tokens, 0);
|
|
109
|
+
// Stable sort: tokens desc, then source name for deterministic ties.
|
|
110
|
+
rows.sort((a, b) => b.tokens - a.tokens || a.source.localeCompare(b.source));
|
|
111
|
+
const sources = rows.map((r) => ({
|
|
112
|
+
...r,
|
|
113
|
+
share: total ? r.tokens / total : 0,
|
|
114
|
+
}));
|
|
115
|
+
return { sources, total };
|
|
116
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-generated CLI reference + doc-drift detection (P2 "文档").
|
|
3
|
+
*
|
|
4
|
+
* The gap: commands, agent tools, flags, exit codes, and protocol fields are
|
|
5
|
+
* documented by hand and silently drift from the code. `command-manifest.json`
|
|
6
|
+
* (regenerated + CI-guarded) and the `AGENT_TOOLS` registry are the authoritative
|
|
7
|
+
* data sources; nothing turns them into a reference doc or checks a hand-written
|
|
8
|
+
* doc against them.
|
|
9
|
+
*
|
|
10
|
+
* This is the PURE core: build a canonical reference from the manifest + tool
|
|
11
|
+
* list, render it, and diff a documentation surface against the authoritative
|
|
12
|
+
* sets to catch drift in BOTH directions (documented-but-gone / present-but-
|
|
13
|
+
* undocumented). No I/O — the caller reads the manifest, tool names, and doc text.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Normalize the command-manifest.json commands into a stable reference list. */
|
|
17
|
+
export function extractCommands(manifest = {}) {
|
|
18
|
+
const commands = Array.isArray(manifest.commands) ? manifest.commands : [];
|
|
19
|
+
return commands
|
|
20
|
+
.map((c) => ({
|
|
21
|
+
name: String(c?.name || "").trim(),
|
|
22
|
+
aliases: Array.isArray(c?.aliases)
|
|
23
|
+
? c.aliases.map((a) => String(a).trim()).filter(Boolean)
|
|
24
|
+
: [],
|
|
25
|
+
summary: String(c?.summary || "").trim(),
|
|
26
|
+
}))
|
|
27
|
+
.filter((c) => c.name)
|
|
28
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Normalize the AGENT_TOOLS registry into a sorted, de-duped name list. Accepts
|
|
33
|
+
* plain strings, `{ name }` contracts, and OpenAI-style `{ function: { name } }`
|
|
34
|
+
* function-tool definitions.
|
|
35
|
+
*/
|
|
36
|
+
export function extractToolNames(tools = []) {
|
|
37
|
+
const names = (Array.isArray(tools) ? tools : [])
|
|
38
|
+
.map((t) => {
|
|
39
|
+
if (typeof t === "string") return t.trim();
|
|
40
|
+
const n = t?.name ?? t?.function?.name;
|
|
41
|
+
return String(n || "").trim();
|
|
42
|
+
})
|
|
43
|
+
.filter(Boolean);
|
|
44
|
+
return [...new Set(names)].sort();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Every command token a user can invoke: primary names + aliases. */
|
|
48
|
+
export function commandTokens(commands) {
|
|
49
|
+
const set = new Set();
|
|
50
|
+
for (const c of commands) {
|
|
51
|
+
set.add(c.name);
|
|
52
|
+
for (const a of c.aliases) set.add(a);
|
|
53
|
+
}
|
|
54
|
+
return set;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build the canonical reference object from the authoritative sources. This is
|
|
59
|
+
* the data a generator serializes; `renderReferenceMarkdown` renders it.
|
|
60
|
+
*/
|
|
61
|
+
export function buildCliReference({ manifest = {}, tools = [] } = {}) {
|
|
62
|
+
const commands = extractCommands(manifest);
|
|
63
|
+
const toolNames = extractToolNames(tools);
|
|
64
|
+
return {
|
|
65
|
+
commandCount: commands.length,
|
|
66
|
+
toolCount: toolNames.length,
|
|
67
|
+
commands,
|
|
68
|
+
tools: toolNames,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Deterministic markdown render of the reference (stable output for diffing). */
|
|
73
|
+
export function renderReferenceMarkdown(reference) {
|
|
74
|
+
const lines = [];
|
|
75
|
+
lines.push("# CLI Reference (generated)");
|
|
76
|
+
lines.push("");
|
|
77
|
+
lines.push(
|
|
78
|
+
"<!-- Generated from command-manifest.json + AGENT_TOOLS. Do not edit by hand. -->",
|
|
79
|
+
);
|
|
80
|
+
lines.push("");
|
|
81
|
+
lines.push(`## Commands (${reference.commandCount})`);
|
|
82
|
+
lines.push("");
|
|
83
|
+
for (const c of reference.commands) {
|
|
84
|
+
const alias = c.aliases.length ? ` (aliases: ${c.aliases.join(", ")})` : "";
|
|
85
|
+
lines.push(`- \`${c.name}\`${alias}${c.summary ? ` — ${c.summary}` : ""}`);
|
|
86
|
+
}
|
|
87
|
+
lines.push("");
|
|
88
|
+
lines.push(`## Agent tools (${reference.toolCount})`);
|
|
89
|
+
lines.push("");
|
|
90
|
+
for (const name of reference.tools) lines.push(`- \`${name}\``);
|
|
91
|
+
lines.push("");
|
|
92
|
+
return lines.join("\n");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Which authoritative command/tool tokens appear as literal `cc <token>` /
|
|
97
|
+
* backticked `<token>` mentions in the doc text. We look for word-boundary
|
|
98
|
+
* mentions; a token is "documented" if it appears anywhere in the doc.
|
|
99
|
+
*/
|
|
100
|
+
function isMentioned(doc, token) {
|
|
101
|
+
if (!token) return false;
|
|
102
|
+
const esc = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
103
|
+
return new RegExp(`(^|[^\\w-])${esc}([^\\w-]|$)`).test(doc);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Diff a documentation surface against the authoritative sets. Reports drift both
|
|
108
|
+
* ways so neither a NEW undocumented command nor a REMOVED-but-still-documented
|
|
109
|
+
* one slips through.
|
|
110
|
+
*
|
|
111
|
+
* @param {object} args
|
|
112
|
+
* manifest — command-manifest.json object
|
|
113
|
+
* tools — AGENT_TOOLS array
|
|
114
|
+
* doc — the documentation text to check
|
|
115
|
+
* knownTokens — optional extra tokens to treat as authoritative (e.g. flags,
|
|
116
|
+
* exit codes, protocol fields) to catch their drift too
|
|
117
|
+
* @returns {{ undocumentedCommands, undocumentedTools, staleCommandMentions,
|
|
118
|
+
* ok, counts }}
|
|
119
|
+
*/
|
|
120
|
+
export function detectDocDrift({
|
|
121
|
+
manifest = {},
|
|
122
|
+
tools = [],
|
|
123
|
+
doc = "",
|
|
124
|
+
knownTokens = [],
|
|
125
|
+
} = {}) {
|
|
126
|
+
const commands = extractCommands(manifest);
|
|
127
|
+
const toolNames = extractToolNames(tools);
|
|
128
|
+
const text = String(doc || "");
|
|
129
|
+
|
|
130
|
+
const undocumentedCommands = commands
|
|
131
|
+
.filter((c) => !isMentioned(text, c.name))
|
|
132
|
+
.map((c) => c.name);
|
|
133
|
+
const undocumentedTools = toolNames.filter((n) => !isMentioned(text, n));
|
|
134
|
+
|
|
135
|
+
// Tokens the doc claims as commands (in a `cc <token>` construct) that are not
|
|
136
|
+
// in the manifest → stale references to removed/renamed commands.
|
|
137
|
+
const known = commandTokens(commands);
|
|
138
|
+
for (const t of knownTokens) known.add(String(t));
|
|
139
|
+
const staleCommandMentions = [];
|
|
140
|
+
const seen = new Set();
|
|
141
|
+
const ccRe = /\bcc\s+([a-z][a-z0-9-]*)\b/g;
|
|
142
|
+
let m;
|
|
143
|
+
while ((m = ccRe.exec(text)) !== null) {
|
|
144
|
+
const tok = m[1];
|
|
145
|
+
if (!known.has(tok) && !seen.has(tok)) {
|
|
146
|
+
seen.add(tok);
|
|
147
|
+
staleCommandMentions.push(tok);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const counts = {
|
|
152
|
+
undocumentedCommands: undocumentedCommands.length,
|
|
153
|
+
undocumentedTools: undocumentedTools.length,
|
|
154
|
+
staleCommandMentions: staleCommandMentions.length,
|
|
155
|
+
};
|
|
156
|
+
return {
|
|
157
|
+
undocumentedCommands,
|
|
158
|
+
undocumentedTools,
|
|
159
|
+
staleCommandMentions,
|
|
160
|
+
counts,
|
|
161
|
+
ok:
|
|
162
|
+
counts.undocumentedCommands === 0 &&
|
|
163
|
+
counts.undocumentedTools === 0 &&
|
|
164
|
+
counts.staleCommandMentions === 0,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import { execSync } from "node:child_process";
|
|
27
27
|
import { join, basename } from "node:path";
|
|
28
28
|
import { getHomeDir, getConfigPath } from "./paths.js";
|
|
29
|
+
import { checkAgenda, checkInstructionFiles } from "./runtime-checkup.js";
|
|
29
30
|
|
|
30
31
|
export const CHECK_LEVELS = Object.freeze({
|
|
31
32
|
OK: "ok",
|
|
@@ -314,6 +315,58 @@ async function pluginSection(opts, _deps2) {
|
|
|
314
315
|
} catch (err) {
|
|
315
316
|
checks.push(failedCheck("plugins", "Installed plugins", err));
|
|
316
317
|
}
|
|
318
|
+
|
|
319
|
+
// Broken manifests + un-consented capabilities (Phase 3 capability gap).
|
|
320
|
+
// Distinct from the signature-lock check above: re-discover to get each
|
|
321
|
+
// plugin's parsed manifest (listInstalled drops it) so we can flag an invalid
|
|
322
|
+
// manifest and any DECLARED capability set the user has not consented to.
|
|
323
|
+
try {
|
|
324
|
+
const { discoverPlugins } = await import("./plugin-runtime/scopes.js");
|
|
325
|
+
const { isPluginCapabilityConsented } =
|
|
326
|
+
await import("./plugin-runtime/capability-consent.js");
|
|
327
|
+
const { checkPluginsAndLsp } = await import("./runtime-checkup.js");
|
|
328
|
+
const discovered =
|
|
329
|
+
discoverPlugins({ cwd: opts.cwd || process.cwd(), skipPolicy: true }) ||
|
|
330
|
+
[];
|
|
331
|
+
// Broken manifests → warn (shared runtime-checkup evaluator).
|
|
332
|
+
for (const f of checkPluginsAndLsp(
|
|
333
|
+
discovered.map((p) => ({
|
|
334
|
+
id: p.name,
|
|
335
|
+
healthy: p.manifest?.ok !== false,
|
|
336
|
+
kind: "plugin",
|
|
337
|
+
})),
|
|
338
|
+
)) {
|
|
339
|
+
checks.push(
|
|
340
|
+
check(
|
|
341
|
+
f.id,
|
|
342
|
+
`plugin ${f.ref}`,
|
|
343
|
+
CHECK_LEVELS.WARN,
|
|
344
|
+
`${f.message} — ${f.remediation}`,
|
|
345
|
+
),
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
// Declared capabilities not yet consented → warn.
|
|
349
|
+
for (const p of discovered) {
|
|
350
|
+
if (!p.manifest?.capabilitiesDeclared) continue;
|
|
351
|
+
const consented = isPluginCapabilityConsented(
|
|
352
|
+
{ name: p.name, scope: p.scope, version: p.version },
|
|
353
|
+
p.manifest.capabilities,
|
|
354
|
+
);
|
|
355
|
+
if (!consented) {
|
|
356
|
+
checks.push(
|
|
357
|
+
check(
|
|
358
|
+
"plugin-capability-unconsented",
|
|
359
|
+
`plugin ${p.name}`,
|
|
360
|
+
CHECK_LEVELS.WARN,
|
|
361
|
+
`declares capabilities not yet consented — run \`cc plugin consent ${p.name}\``,
|
|
362
|
+
),
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
} catch (err) {
|
|
367
|
+
checks.push(failedCheck("plugin-capabilities", "plugin capabilities", err));
|
|
368
|
+
}
|
|
369
|
+
|
|
317
370
|
return { id: "plugins", title: "Plugin trust", checks };
|
|
318
371
|
}
|
|
319
372
|
|
|
@@ -540,6 +593,80 @@ async function worktreeSection(opts, deps) {
|
|
|
540
593
|
return { id: "worktrees", title: "Git worktrees", checks };
|
|
541
594
|
}
|
|
542
595
|
|
|
596
|
+
// ── runtime checkup: agenda expiry + verbose instruction files ──────────────
|
|
597
|
+
// Only the gaps the OTHER sections don't already cover. Stale sessions,
|
|
598
|
+
// worktrees, orphan processes and lost background agents are handled by
|
|
599
|
+
// transcriptSection / worktreeSection / backgroundSection — surfacing them here
|
|
600
|
+
// too would double-report, so this section deliberately covers just the agenda
|
|
601
|
+
// schedule store and the instruction files (cc.md / AGENTS.md / CLAUDE.md).
|
|
602
|
+
const SEVERITY_TO_LEVEL = {
|
|
603
|
+
error: CHECK_LEVELS.ERR,
|
|
604
|
+
warn: CHECK_LEVELS.WARN,
|
|
605
|
+
info: CHECK_LEVELS.INFO,
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
async function runtimeSection(opts, deps) {
|
|
609
|
+
const checks = [];
|
|
610
|
+
const now = deps.now();
|
|
611
|
+
|
|
612
|
+
// Agenda schedule store — overdue/never-fired wakeups, retirable leftovers.
|
|
613
|
+
try {
|
|
614
|
+
const { AgentScheduleStore } = await import("./agent-schedule-store.js");
|
|
615
|
+
const store = new AgentScheduleStore();
|
|
616
|
+
const entries = store.list().map((e) => ({
|
|
617
|
+
id: e.id,
|
|
618
|
+
dueAt: e.kind === "wakeup" ? e.dueAt : e.nextAt,
|
|
619
|
+
recurring: e.kind !== "wakeup",
|
|
620
|
+
}));
|
|
621
|
+
for (const f of checkAgenda(entries, now)) {
|
|
622
|
+
checks.push(
|
|
623
|
+
check(
|
|
624
|
+
f.id,
|
|
625
|
+
`agenda ${f.ref}`,
|
|
626
|
+
SEVERITY_TO_LEVEL[f.severity] || CHECK_LEVELS.INFO,
|
|
627
|
+
`${f.message} — ${f.remediation}`,
|
|
628
|
+
),
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
} catch (err) {
|
|
632
|
+
checks.push(failedCheck("agenda", "agenda schedule store", err));
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Instruction files — flag verbose ones (byte size only; a derivability
|
|
636
|
+
// heuristic would need code analysis and isn't computed here).
|
|
637
|
+
try {
|
|
638
|
+
const cwd = opts.cwd || process.cwd();
|
|
639
|
+
const files = [];
|
|
640
|
+
for (const name of ["cc.md", "AGENTS.md", "CLAUDE.md"]) {
|
|
641
|
+
const p = join(cwd, name);
|
|
642
|
+
if (deps.existsSync(p)) {
|
|
643
|
+
try {
|
|
644
|
+
files.push({ path: name, bytes: deps.statSync(p).size });
|
|
645
|
+
} catch {
|
|
646
|
+
// raced/unreadable — skip
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
for (const f of checkInstructionFiles(files)) {
|
|
651
|
+
checks.push(
|
|
652
|
+
check(
|
|
653
|
+
f.id,
|
|
654
|
+
f.ref,
|
|
655
|
+
SEVERITY_TO_LEVEL[f.severity] || CHECK_LEVELS.INFO,
|
|
656
|
+
`${f.message} — ${f.remediation}`,
|
|
657
|
+
),
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
} catch (err) {
|
|
661
|
+
checks.push(failedCheck("instructions", "instruction files", err));
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (checks.length === 0) {
|
|
665
|
+
checks.push(check("runtime-clean", "runtime hygiene", CHECK_LEVELS.OK, ""));
|
|
666
|
+
}
|
|
667
|
+
return { id: "runtime", title: "Runtime checkup", checks };
|
|
668
|
+
}
|
|
669
|
+
|
|
543
670
|
/**
|
|
544
671
|
* Collect all checkup sections. Never throws — a failing subsystem becomes an
|
|
545
672
|
* `err` check inside its section.
|
|
@@ -557,6 +684,7 @@ export async function collectCheckupSections(opts = {}) {
|
|
|
557
684
|
transcriptSection,
|
|
558
685
|
backgroundSection,
|
|
559
686
|
worktreeSection,
|
|
687
|
+
runtimeSection,
|
|
560
688
|
]) {
|
|
561
689
|
try {
|
|
562
690
|
sections.push(await build(opts, deps));
|
|
Binary file
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* hook-event-log — a persistent, hash-chained, replay-safe log of the hook
|
|
5
|
+
* delivery envelopes produced by the unified event bus ([[hook-event-bus.cjs]]).
|
|
6
|
+
*
|
|
7
|
+
* The event bus already stamps every settings-hook delivery with a stable
|
|
8
|
+
* `event_id` (see [[settings-hook-events.cjs]] `withDeliveryId`), but until now
|
|
9
|
+
* the full envelope was discarded — so there was nothing to look up when a user
|
|
10
|
+
* wants `cc hook replay <event-id>`. This store persists each envelope to a
|
|
11
|
+
* tamper-evident JSONL log so the replay command has a real source.
|
|
12
|
+
*
|
|
13
|
+
* Design:
|
|
14
|
+
* - OPT-IN: recording only happens when `CC_HOOK_EVENT_LOG` is enabled, so the
|
|
15
|
+
* default hook-firing path is byte-for-byte unchanged (no new IO per turn).
|
|
16
|
+
* - Hash chain: each line is `{envelope, prevHash, hash}` where
|
|
17
|
+
* `hash = sha256(prevHash + stableStringify(envelope))`. Editing / deleting /
|
|
18
|
+
* reordering any recorded line breaks the chain (mirrors the session
|
|
19
|
+
* transcript integrity design), which `verifyHookEventChain` detects.
|
|
20
|
+
* - Size guard: the log rotates to `<file>.1` once it exceeds MAX_LOG_BYTES so
|
|
21
|
+
* an always-on session can't grow it without bound.
|
|
22
|
+
* - Pure over injected IO (`_deps.fs` / `_deps.os`) so it is fully unit-testable
|
|
23
|
+
* without touching the real home directory.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require("fs");
|
|
27
|
+
const os = require("os");
|
|
28
|
+
const path = require("path");
|
|
29
|
+
const crypto = require("crypto");
|
|
30
|
+
const { stableStringify } = require("./hook-event-bus.cjs");
|
|
31
|
+
|
|
32
|
+
const _deps = { fs, os };
|
|
33
|
+
|
|
34
|
+
/** Cap the always-on log at 5 MiB, then rotate to `<file>.1`. */
|
|
35
|
+
const MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
36
|
+
|
|
37
|
+
/** Default log location: `~/.chainlesschain/hook-events.jsonl`. */
|
|
38
|
+
function hookEventLogPath(homedir) {
|
|
39
|
+
const home = homedir || _deps.os.homedir();
|
|
40
|
+
return path.join(home, ".chainlesschain", "hook-events.jsonl");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Recording is opt-in — `CC_HOOK_EVENT_LOG=1` (or `true`) turns it on. */
|
|
44
|
+
function isHookEventLogEnabled(env = process.env) {
|
|
45
|
+
const v = String(env.CC_HOOK_EVENT_LOG || "").toLowerCase();
|
|
46
|
+
return v === "1" || v === "true" || v === "yes" || v === "on";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Hash one envelope against the previous chain hash (deterministic). */
|
|
50
|
+
function computeChainHash(prevHash, envelope) {
|
|
51
|
+
return crypto
|
|
52
|
+
.createHash("sha256")
|
|
53
|
+
.update(`${prevHash || "genesis"}\n${stableStringify(envelope)}`, "utf-8")
|
|
54
|
+
.digest("hex");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The hash the NEXT record must chain from (last parseable line's `hash`). */
|
|
58
|
+
function latestChainHash(text) {
|
|
59
|
+
const lines = String(text || "").split("\n");
|
|
60
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
61
|
+
if (!lines[i].trim()) continue;
|
|
62
|
+
try {
|
|
63
|
+
const rec = JSON.parse(lines[i]);
|
|
64
|
+
return typeof rec?.hash === "string" ? rec.hash : null;
|
|
65
|
+
} catch {
|
|
66
|
+
// half-written tail line — chain from the previous intact record
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Append a hook envelope to the chained log. Best-effort: never throws — a
|
|
75
|
+
* failure to record must not break the turn that fired the hook.
|
|
76
|
+
*
|
|
77
|
+
* @param {object} envelope a full envelope from buildHookEnvelope
|
|
78
|
+
* @param {object} [opts] { filePath, homedir }
|
|
79
|
+
* @returns {{ok:boolean, event_id?:string, hash?:string, reason?:string}}
|
|
80
|
+
*/
|
|
81
|
+
function appendHookEvent(envelope, { filePath, homedir } = {}) {
|
|
82
|
+
if (!envelope || typeof envelope !== "object" || !envelope.event_id) {
|
|
83
|
+
return { ok: false, reason: "invalid envelope (no event_id)" };
|
|
84
|
+
}
|
|
85
|
+
const f = _deps.fs;
|
|
86
|
+
const fp = filePath || hookEventLogPath(homedir);
|
|
87
|
+
try {
|
|
88
|
+
const dir = path.dirname(fp);
|
|
89
|
+
if (!f.existsSync(dir)) f.mkdirSync(dir, { recursive: true });
|
|
90
|
+
let prevHash = null;
|
|
91
|
+
if (f.existsSync(fp)) {
|
|
92
|
+
let rotated = false;
|
|
93
|
+
try {
|
|
94
|
+
if (f.statSync(fp).size > MAX_LOG_BYTES) {
|
|
95
|
+
f.renameSync(fp, `${fp}.1`);
|
|
96
|
+
rotated = true;
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
/* stat/rename best-effort */
|
|
100
|
+
}
|
|
101
|
+
if (!rotated) prevHash = latestChainHash(f.readFileSync(fp, "utf-8"));
|
|
102
|
+
}
|
|
103
|
+
const hash = computeChainHash(prevHash, envelope);
|
|
104
|
+
const line = `${JSON.stringify({ envelope, prevHash, hash })}\n`;
|
|
105
|
+
f.appendFileSync(fp, line, "utf-8");
|
|
106
|
+
return { ok: true, event_id: envelope.event_id, hash };
|
|
107
|
+
} catch (e) {
|
|
108
|
+
return { ok: false, reason: e.message };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Read every recorded record (skips malformed / half-written lines). */
|
|
113
|
+
function readHookEventRecords({ filePath, homedir } = {}) {
|
|
114
|
+
const f = _deps.fs;
|
|
115
|
+
const fp = filePath || hookEventLogPath(homedir);
|
|
116
|
+
if (!f.existsSync(fp)) return [];
|
|
117
|
+
const out = [];
|
|
118
|
+
for (const line of f.readFileSync(fp, "utf-8").split("\n")) {
|
|
119
|
+
if (!line.trim()) continue;
|
|
120
|
+
try {
|
|
121
|
+
out.push(JSON.parse(line));
|
|
122
|
+
} catch {
|
|
123
|
+
/* skip malformed / partial line */
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Look up the envelope for a delivery id. The LATEST matching record wins (an
|
|
131
|
+
* event_id can recur across processes; the most recent delivery is what a user
|
|
132
|
+
* replaying "the last one" means).
|
|
133
|
+
*/
|
|
134
|
+
function findHookEvent(eventId, { filePath, homedir } = {}) {
|
|
135
|
+
if (!eventId) return null;
|
|
136
|
+
const records = readHookEventRecords({ filePath, homedir });
|
|
137
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
138
|
+
if (records[i]?.envelope?.event_id === eventId) return records[i].envelope;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The most recent `limit` envelopes (newest last), for `cc hook events-log`. */
|
|
144
|
+
function listHookEvents({ limit = 20, filePath, homedir } = {}) {
|
|
145
|
+
const records = readHookEventRecords({ filePath, homedir });
|
|
146
|
+
const n = Math.max(0, Number(limit) || 0);
|
|
147
|
+
return records.slice(n ? -n : 0).map((r) => r.envelope);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Recompute the chain and report the first break, if any.
|
|
152
|
+
* @returns {{ok:boolean, length:number, brokenAt:number, reason?:string}}
|
|
153
|
+
*/
|
|
154
|
+
function verifyHookEventChain({ filePath, homedir } = {}) {
|
|
155
|
+
const records = readHookEventRecords({ filePath, homedir });
|
|
156
|
+
let prev = null;
|
|
157
|
+
for (let i = 0; i < records.length; i++) {
|
|
158
|
+
const { envelope, prevHash, hash } = records[i] || {};
|
|
159
|
+
if ((prevHash ?? null) !== prev) {
|
|
160
|
+
return {
|
|
161
|
+
ok: false,
|
|
162
|
+
length: records.length,
|
|
163
|
+
brokenAt: i,
|
|
164
|
+
reason: "prevHash does not match the previous record's hash",
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const expect = computeChainHash(prev, envelope);
|
|
168
|
+
if (expect !== hash) {
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
length: records.length,
|
|
172
|
+
brokenAt: i,
|
|
173
|
+
reason: "record hash does not match its envelope (tampered)",
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
prev = hash;
|
|
177
|
+
}
|
|
178
|
+
return { ok: true, length: records.length, brokenAt: -1 };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
MAX_LOG_BYTES,
|
|
183
|
+
hookEventLogPath,
|
|
184
|
+
isHookEventLogEnabled,
|
|
185
|
+
computeChainHash,
|
|
186
|
+
latestChainHash,
|
|
187
|
+
appendHookEvent,
|
|
188
|
+
readHookEventRecords,
|
|
189
|
+
findHookEvent,
|
|
190
|
+
listHookEvents,
|
|
191
|
+
verifyHookEventChain,
|
|
192
|
+
_deps,
|
|
193
|
+
};
|