chainlesschain 0.162.156 → 0.162.158
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/README.md +1 -1
- package/package.json +2 -2
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-DjBXlypQ.js → AIOps-BwpTJTN_.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-Cv9er9bp.js → ActionButton-BtQOoo3l.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-CLKDjSNs.js → Analytics-Duf_rXZa.js} +3 -3
- package/src/assets/web-panel/assets/AppLayout-XQK3wjvB.js +9 -0
- package/src/assets/web-panel/assets/Artifacts-CTVt96zW.css +1 -0
- package/src/assets/web-panel/assets/Artifacts-DQvJoQ6E.js +1 -0
- package/src/assets/web-panel/assets/{Audit-CXafD178.js → Audit-DNjOGQLh.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-CP5c-GSd.js → BackgroundAgents-FH9RAfB5.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-BbYLQfyt.js → Backup-ZIbrR4-U.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-Bqe0MwZL.js → BaseInput-DSt3NaND.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-jNA_yUPH.js → Chat-CY-wvbjv.js} +6 -6
- package/src/assets/web-panel/assets/Chat-KSGPtU0L.css +1 -0
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-BOFLF01s.js → ChatBubbleRenderer-BAV9V-t-.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-Dlzz-zD1.js → Checkbox-8JVadE_B.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-BAsDQjGf.js → Codegen-CN83nPYI.js} +1 -1
- package/src/assets/web-panel/assets/{Col-Ju0HrAuS.js → Col-xPSvqdN8.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DynJd43Z.js → Community-D78cnIjG.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CabbZVww.js → Compact-CSw6Csbp.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-DW4BmAoh.js → Compliance-E_HhjcYR.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-Bk1S_7X-.js → Cowork-DybeQjmm.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-By4sh_NM.js → Cron-DS0BJeLY.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-DdZdbS1C.js → Crosschain-BOwVgkbs.js} +1 -1
- package/src/assets/web-panel/assets/{DID-CkL-sQyS.js → DID-DRUr9YuX.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-g_mOpumg.js → Dashboard-Cht-yUKW.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-DtHyE7Ao.js → Dropdown-BLr_EJ9X.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Kpvov5Wr.js → EmailListRenderer-Drexuh5W.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-C4kZoePR.js → FamilyGuardDashboard-D9Q2Kq63.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-BAwWoK2N.js → Federation-C-Z1xI-T.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DeI0i5f8.js → FormItemContext-DLHPMkQF.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-CkcKRtos.js → GenericCardRenderer-DxxEdn2r.js} +1 -1
- package/src/assets/web-panel/assets/{Git-kX6jjI9y.js → Git-syATcaBt.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-BXJCBm6x.js → Governance-CYe_r8Zf.js} +1 -1
- package/src/assets/web-panel/assets/Inference-GWxErB7k.js +1 -0
- package/src/assets/web-panel/assets/{KnowledgeGraph-3Dnuf1Wh.js → KnowledgeGraph-qQvVCZoi.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-lVAZCtUB.js → Logs-BenAU2W8.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-7agE3Or1.js +1 -0
- package/src/assets/web-panel/assets/MarkdownRenderer-DbzhFwvz.css +1 -0
- package/src/assets/web-panel/assets/Marketplace-D4Jn5fpD.js +1 -0
- package/src/assets/web-panel/assets/{McpTools-CqjzefRU.js → McpTools-DtAwoqiQ.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-BFiPAxk6.js → Memory-BI_O50I8.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-D0O43VwF.js → MobileBridge-CQWE2lqx.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-B3now48t.js → MobileProjects-CVtTqgtg.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-DybhtaCS.js → Mtc-Dnr16qsn.js} +2 -2
- package/src/assets/web-panel/assets/{MtcAudit-D7MmY4F0.js → MtcAudit-CQ5Xe4-a.js} +5 -5
- package/src/assets/web-panel/assets/{Multisig-wZvGJnB8.js → Multisig-Bjr8O-wy.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-BNPpgfvn.js → NLProgramming-CEe-KirU.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-B6FS3KBj.js → Notes-spKJIKF9.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-CK0yla9U.js → NotificationSettings-DRp83LFG.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-h1A0XB2a.js +1 -0
- package/src/assets/web-panel/assets/{Organization-geqgwN1T.js → Organization-BlgB0IYg.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-BWJYZFbC.js → Overflow-a_35HqZI.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-DnU1505F.js → P2P-CSNHCNPC.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-BGY74KnC.js → PdhVaultBrowser-C8M1JH-1.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-CgArrWtQ.js → Permissions-DmjJi9Ms.js} +4 -4
- package/src/assets/web-panel/assets/{PersonalDataHub-C2vYE61a.js → PersonalDataHub-DfuwRyf4.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-CBYLI4yW.js → Pipeline-B2-S4ekE.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-B-PdW9aN.js → Privacy-B1dOiBLC.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-CuWQegbV.js → ProjectInit-_pNmc-DX.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-4d32m_cS.js → ProjectSettings-DmwG2Ii1.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-VLA7ttwd.js → Projects-COATmHxF.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-B_Izb_oa.js → Providers-C6HSjg1d.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-C6B_ndXV.js → QrScannerModal-CrDmS7l1.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-C6tavL-y.js → QuickAsk-CfKvWt-x.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-CF-LaZ6x.js → Recommend-DMa6Kuzv.js} +1 -1
- package/src/assets/web-panel/assets/RemoteSession-DrktUo5x.js +4 -0
- package/src/assets/web-panel/assets/Reputation-BTPQ1vjL.js +1 -0
- package/src/assets/web-panel/assets/{Row-DO0fd48A.js → Row-B6Q7QkMG.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-BV5LDzFR.js → RssFeed-B1IJhAmX.js} +3 -3
- package/src/assets/web-panel/assets/Search-xbea1kGD.js +1 -0
- package/src/assets/web-panel/assets/{Security-DwSrWRCw.js → Security-7rs0cO3F.js} +4 -4
- package/src/assets/web-panel/assets/{Services-BEf1QyM4.js → Services-0fjixK0S.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-LZXTpovy.js → Skeleton-B_rVq9Cu.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-SKgc0Zug.js → Skills-C5uM90b9.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-C_QVOtkN.js → Sla-U4pYCIZH.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-v5TW2V-X.js → SpeechSettings-Cdz4yTWx.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CQPjHdqU.js → SyncSettings-Me3aetHv.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-DCd-uYcI.js → Tasks-Cf6aNkL_.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-62VDIjtj.js → Templates-CYatk4tU.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BnFQ0pNy.js → Tenant-DIMKLw7o.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-D3v0MGSj.js → Terminal-C_cp-uSJ.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-BI1Ka-cu.js → TimelineRenderer-cUPVNADp.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-BwX1jXYP.js → Tokens-CiaEIXVf.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-CSA8TqNx.js → Trigger-DB1WU5lQ.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-Bn3IjCx7.js → Trust-Cfht9Ijy.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-pUbOAqNi.js → UkeySign-K3P4Vt9S.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-Db2RercF.js → VideoEditing-DWjhjXRh.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-CpR1BVAl.js → Wallet-D9-Q1Kan.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-Yxtkn7Zp.js → WebAuthn-BDszy-oV.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-CYLe9a2E.js → WorkflowEditor-DIhij4rA.js} +1 -1
- package/src/assets/web-panel/assets/{chat-gsOGdak0.js → chat-D7VCiKbh.js} +1 -1
- package/src/assets/web-panel/assets/{colors-D9fN299a.js → colors-BvOd9pVn.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-vkd7s-ay.js → compact-item-DoqXBiAO.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-C7YkbMyy.js → createContext-CrKRzUtp.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-nDB--2xr.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-ig7reRSU.js → hasIn-BWAev4WC.js} +1 -1
- package/src/assets/web-panel/assets/{icons-DEmcnxBf.js → icons-DG5UD-6K.js} +4 -4
- package/src/assets/web-panel/assets/{index-W52sHRdr.js → index-1poMpMU0.js} +1 -1
- package/src/assets/web-panel/assets/{index-DZAxCnjS.js → index-8g5TJCZ3.js} +1 -1
- package/src/assets/web-panel/assets/{index-9TxdMYju.js → index-B0vGEbJT.js} +1 -1
- package/src/assets/web-panel/assets/index-B1_pRTtB.js +1 -0
- package/src/assets/web-panel/assets/{index-DhdXbsXX.js → index-B30NvR21.js} +1 -1
- package/src/assets/web-panel/assets/{index-B7QRpKif.js → index-BCyP071m.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bt-9ZhIu.js → index-BHOcVYrx.js} +1 -1
- package/src/assets/web-panel/assets/{index-BYe9HRSt.js → index-BN9m4Ux2.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkP17Z95.js → index-BWWsaimP.js} +1 -1
- package/src/assets/web-panel/assets/{index-CDWTycrg.js → index-BXyl8O23.js} +1 -1
- package/src/assets/web-panel/assets/{index-CspGymmg.js → index-BZGk8AbZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-DFfxF7Vt.js → index-Bf9YlbDn.js} +1 -1
- package/src/assets/web-panel/assets/{index-DQ8U7kzC.js → index-Bj7EgmOp.js} +1 -1
- package/src/assets/web-panel/assets/{index-D2yhtk6a.js → index-BkgnGX7v.js} +1 -1
- package/src/assets/web-panel/assets/{index-FzGr6CVk.js → index-BoYrS_L2.js} +1 -1
- package/src/assets/web-panel/assets/{index-BMw48d19.js → index-Bq_ctRpz.js} +1 -1
- package/src/assets/web-panel/assets/{index-CAkz1f1r.js → index-C5kRmj3P.js} +1 -1
- package/src/assets/web-panel/assets/{index-SDrcQXO1.js → index-CDGLdn12.js} +1 -1
- package/src/assets/web-panel/assets/{index-gj8kxcAi.js → index-CEPHMSih.js} +1 -1
- package/src/assets/web-panel/assets/{index-PkH39qsp.js → index-CEXfKSiI.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHHN_KTS.js → index-CSb-tnIo.js} +1 -1
- package/src/assets/web-panel/assets/{index-gpurplyq.js → index-CmJ6xKBO.js} +1 -1
- package/src/assets/web-panel/assets/{index-BfpVcXKa.js → index-CoD31Ndm.js} +1 -1
- package/src/assets/web-panel/assets/{index-CGzGe5tv.js → index-Cx7M7PSF.js} +1 -1
- package/src/assets/web-panel/assets/{index-BhftELHI.js → index-D23L1V76.js} +3 -3
- package/src/assets/web-panel/assets/{index-DPguq3X7.js → index-DV-Rfmfx.js} +4 -4
- package/src/assets/web-panel/assets/{index-BpGlLbRv.js → index-DlGu7As6.js} +1 -1
- package/src/assets/web-panel/assets/{index-C8HZMygt.js → index-DvDOpsJB.js} +1 -1
- package/src/assets/web-panel/assets/{index-DuwbKHoS.js → index-HwHdTusY.js} +1 -1
- package/src/assets/web-panel/assets/{index-C7AY39sg.js → index-ISvPZ05r.js} +1 -1
- package/src/assets/web-panel/assets/{index-nX8YE6cj.js → index-JzUmJMvn.js} +1 -1
- package/src/assets/web-panel/assets/{index-DvCJXmrx.js → index-KR1-Em9C.js} +1 -1
- package/src/assets/web-panel/assets/{index-Co2NYyew.js → index-PDxGjKRu.js} +1 -1
- package/src/assets/web-panel/assets/index-TMM9FuW-.js +1 -0
- package/src/assets/web-panel/assets/{index-D6s_kbjt.js → index-YGDy7nNq.js} +1 -1
- package/src/assets/web-panel/assets/{index-Dy_TKZ9h.js → index-gmxqu5Kk.js} +1 -1
- package/src/assets/web-panel/assets/{index-DFM1oNe1.js → index-oY45XrkG.js} +1 -1
- package/src/assets/web-panel/assets/{index-bVbgYBhv.js → index-ww1QtLnX.js} +1 -1
- package/src/assets/web-panel/assets/{index-BvDEukXe.js → index-zH-tuuGZ.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-BrgfVRg9.js → initDefaultProps-XW-OR3mL.js} +1 -1
- package/src/assets/web-panel/assets/{motion-D_3xmwac.js → motion-CeuEHh20.js} +1 -1
- package/src/assets/web-panel/assets/{move-DDDJdlxw.js → move-Dva79Keg.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-BK2uEWwn.js → mtc-parser-C0B6bD2j.js} +1 -1
- package/src/assets/web-panel/assets/{omit-BPHKbDcf.js → omit-AkZvO-a2.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-LNnTscuz.js → pickAttrs-B4r55qGc.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-CvmTZll5.js → placementArrow-BLH-z7YE.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-eBj7qi2J.js → responsiveObserve-ksEGZ2Wf.js} +1 -1
- package/src/assets/web-panel/assets/{slide-D68hHkrM.js → slide-Cth4OD1Z.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-daTlaeo0.js → statusUtils-9S83ezxw.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-DtyKMtHB.js → styleChecker-7yQmQGZ4.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-ekFwFs9h.js → useFlexGapSupport-DpGnMmPc.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-jRaSEoF4.js → useFs-C4zDZIXH.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-DD5nGzjA.js → usePersonalDataHub-CoLICdUZ.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-KcgLUiom.js → vnode-CKyH-35X.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-Cyk2PQ-s.js → zoom-DRDGit17.js} +1 -1
- package/src/assets/web-panel/index.html +2 -2
- package/src/command-manifest.json +8 -1
- package/src/commands/agent.js +49 -5
- package/src/commands/artifacts.js +161 -0
- package/src/commands/browse.js +204 -2
- package/src/data/changelog.json +13 -2
- package/src/gateways/ws/artifact-protocol.js +219 -0
- package/src/gateways/ws/message-dispatcher.js +6 -0
- package/src/gateways/ws/ws-server.js +32 -0
- package/src/index.js +6 -0
- package/src/lib/accessibility.js +55 -0
- package/src/lib/artifact-store.js +236 -0
- package/src/lib/chrome-connector.js +349 -0
- package/src/lib/hook-runner.cjs +47 -7
- package/src/lib/ide-context.js +84 -0
- package/src/lib/permission-rules.cjs +26 -3
- package/src/lib/plugin-runtime/scopes.js +12 -0
- package/src/lib/remote-control.js +35 -0
- package/src/lib/safe-mode.js +40 -0
- package/src/lib/shell-selector.cjs +203 -0
- package/src/lib/skill-loader.js +19 -0
- package/src/lib/slash-dispatch.js +34 -0
- package/src/lib/web-ui-server.js +120 -7
- package/src/repl/agent-repl.js +330 -116
- package/src/repl/remote-approval.js +111 -0
- package/src/runtime/agent-core.js +129 -8
- package/src/runtime/coding-agent-contract-shared.cjs +105 -0
- package/src/runtime/coding-agent-policy.cjs +31 -0
- package/src/runtime/headless-stream.js +51 -1
- package/src/runtime/policies/agent-policy.js +4 -0
- package/src/assets/web-panel/assets/AppLayout-CfyZNzmJ.js +0 -9
- package/src/assets/web-panel/assets/Chat-DDh58PCf.css +0 -1
- package/src/assets/web-panel/assets/Inference-ujD2JSKo.js +0 -1
- package/src/assets/web-panel/assets/Marketplace-qnzNPm-A.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-CXfPHIpy.js +0 -1
- package/src/assets/web-panel/assets/RemoteSession-DBqTTQ9_.js +0 -4
- package/src/assets/web-panel/assets/Reputation-B01FALUi.js +0 -1
- package/src/assets/web-panel/assets/Search-CTti4_MP.js +0 -1
- package/src/assets/web-panel/assets/devWarning-DEcR2h6k.js +0 -1
- package/src/assets/web-panel/assets/index-BEibanjg.js +0 -1
- package/src/assets/web-panel/assets/index-z_25tU3p.js +0 -1
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* artifact WS protocol — exposes the agent-published deliverable store
|
|
3
|
+
* (lib/artifact-store.js, P1 #10) to web-panel / IDE clients so an
|
|
4
|
+
* "Artifacts" panel can list deliverables, preview them (text inline /
|
|
5
|
+
* images as base64), and manage retention.
|
|
6
|
+
*
|
|
7
|
+
* Routes (request → reply):
|
|
8
|
+
* artifact-list {session?, kind?} → {type:"artifact-list", artifacts}
|
|
9
|
+
* artifact-show {artifactId} → {type:"artifact-show", artifact}
|
|
10
|
+
* artifact-content {artifactId, maxBytes?} → {type:"artifact-content", artifactId,
|
|
11
|
+
* mime, encoding, content, size,
|
|
12
|
+
* truncated, previewable}
|
|
13
|
+
* artifact-remove {artifactId} → {type:"artifact-remove", removed, found}
|
|
14
|
+
* artifact-clean {} → {type:"artifact-clean", removed}
|
|
15
|
+
*
|
|
16
|
+
* Preview policy: text mimes stream as utf8 (capped, `truncated` flagged);
|
|
17
|
+
* images as base64 (hard cap — an oversized image reports previewable:false
|
|
18
|
+
* instead of flooding the socket); everything else is metadata-only
|
|
19
|
+
* (previewable:false, the user has `cc artifacts open <id>` locally).
|
|
20
|
+
*
|
|
21
|
+
* The stored-copy filename comes from index.jsonl; serving is refused if it
|
|
22
|
+
* is not a plain basename, so a hand-tampered index row can never turn the
|
|
23
|
+
* content route into an arbitrary-file read.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { readFileSync, statSync } from "node:fs";
|
|
28
|
+
|
|
29
|
+
/** Text preview cap (utf8 chars ≈ bytes for the common case). */
|
|
30
|
+
export const TEXT_PREVIEW_CAP = 256 * 1024;
|
|
31
|
+
/** Image preview cap — beyond this the panel gets metadata only. */
|
|
32
|
+
export const IMAGE_PREVIEW_CAP = 8 * 1024 * 1024;
|
|
33
|
+
|
|
34
|
+
const TEXT_APPLICATION_MIMES = new Set([
|
|
35
|
+
"application/json",
|
|
36
|
+
"application/yaml",
|
|
37
|
+
"application/xml",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
async function loadStore() {
|
|
41
|
+
const { ArtifactStore } = await import("../../lib/artifact-store.js");
|
|
42
|
+
return new ArtifactStore();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sendError(server, id, ws, code, message) {
|
|
46
|
+
server._send(ws, { id, type: "error", code, message });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Classify how (whether) an artifact can be previewed inline. */
|
|
50
|
+
export function previewKindForMime(mime) {
|
|
51
|
+
const m = String(mime || "").toLowerCase();
|
|
52
|
+
if (m.startsWith("text/") || TEXT_APPLICATION_MIMES.has(m)) return "text";
|
|
53
|
+
if (m.startsWith("image/")) return "image";
|
|
54
|
+
return "none";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function handleArtifactList(server, id, ws, message) {
|
|
58
|
+
try {
|
|
59
|
+
const store = await loadStore();
|
|
60
|
+
let artifacts = store.list({ sessionId: message.session || undefined });
|
|
61
|
+
if (message.kind) {
|
|
62
|
+
artifacts = artifacts.filter((e) => e.kind === String(message.kind));
|
|
63
|
+
}
|
|
64
|
+
server._send(ws, { id, type: "artifact-list", artifacts });
|
|
65
|
+
} catch (err) {
|
|
66
|
+
sendError(server, id, ws, "ARTIFACT_LIST_FAILED", err.message);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function handleArtifactShow(server, id, ws, message) {
|
|
71
|
+
try {
|
|
72
|
+
if (!message.artifactId) {
|
|
73
|
+
return sendError(server, id, ws, "NO_ARTIFACT_ID", "artifactId required");
|
|
74
|
+
}
|
|
75
|
+
const store = await loadStore();
|
|
76
|
+
const entry = store.get(message.artifactId);
|
|
77
|
+
if (!entry) {
|
|
78
|
+
return sendError(
|
|
79
|
+
server,
|
|
80
|
+
id,
|
|
81
|
+
ws,
|
|
82
|
+
"ARTIFACT_NOT_FOUND",
|
|
83
|
+
`Artifact not found: ${message.artifactId}`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
server._send(ws, {
|
|
87
|
+
id,
|
|
88
|
+
type: "artifact-show",
|
|
89
|
+
artifact: { ...entry, storedPath: store.storedPath(entry) },
|
|
90
|
+
});
|
|
91
|
+
} catch (err) {
|
|
92
|
+
sendError(server, id, ws, "ARTIFACT_SHOW_FAILED", err.message);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function handleArtifactContent(server, id, ws, message) {
|
|
97
|
+
try {
|
|
98
|
+
if (!message.artifactId) {
|
|
99
|
+
return sendError(server, id, ws, "NO_ARTIFACT_ID", "artifactId required");
|
|
100
|
+
}
|
|
101
|
+
const store = await loadStore();
|
|
102
|
+
const entry = store.get(message.artifactId);
|
|
103
|
+
if (!entry) {
|
|
104
|
+
return sendError(
|
|
105
|
+
server,
|
|
106
|
+
id,
|
|
107
|
+
ws,
|
|
108
|
+
"ARTIFACT_NOT_FOUND",
|
|
109
|
+
`Artifact not found: ${message.artifactId}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
// Traversal guard: the stored filename must be a plain basename. A
|
|
113
|
+
// tampered index row ("file":"../../secret") must not read outside the
|
|
114
|
+
// files dir.
|
|
115
|
+
if (
|
|
116
|
+
!entry.file ||
|
|
117
|
+
entry.file !== path.basename(entry.file) ||
|
|
118
|
+
entry.file.includes("..")
|
|
119
|
+
) {
|
|
120
|
+
return sendError(
|
|
121
|
+
server,
|
|
122
|
+
id,
|
|
123
|
+
ws,
|
|
124
|
+
"ARTIFACT_BAD_FILE",
|
|
125
|
+
"artifact index row has a non-basename stored filename — refusing to serve it",
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const previewKind = previewKindForMime(entry.mime);
|
|
129
|
+
const base = {
|
|
130
|
+
id,
|
|
131
|
+
type: "artifact-content",
|
|
132
|
+
artifactId: entry.id,
|
|
133
|
+
mime: entry.mime,
|
|
134
|
+
size: entry.size,
|
|
135
|
+
};
|
|
136
|
+
if (previewKind === "none") {
|
|
137
|
+
return server._send(ws, {
|
|
138
|
+
...base,
|
|
139
|
+
previewable: false,
|
|
140
|
+
reason: `no inline preview for ${entry.mime} — use \`cc artifacts open ${entry.id}\` locally`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const storedPath = store.storedPath(entry);
|
|
144
|
+
let actualSize;
|
|
145
|
+
try {
|
|
146
|
+
actualSize = statSync(storedPath).size;
|
|
147
|
+
} catch {
|
|
148
|
+
return sendError(
|
|
149
|
+
server,
|
|
150
|
+
id,
|
|
151
|
+
ws,
|
|
152
|
+
"ARTIFACT_FILE_MISSING",
|
|
153
|
+
`stored copy is missing: ${entry.file}`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (previewKind === "image") {
|
|
157
|
+
if (actualSize > IMAGE_PREVIEW_CAP) {
|
|
158
|
+
return server._send(ws, {
|
|
159
|
+
...base,
|
|
160
|
+
previewable: false,
|
|
161
|
+
reason: `image exceeds the ${IMAGE_PREVIEW_CAP} byte preview cap`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
const body = readFileSync(storedPath);
|
|
165
|
+
return server._send(ws, {
|
|
166
|
+
...base,
|
|
167
|
+
previewable: true,
|
|
168
|
+
encoding: "base64",
|
|
169
|
+
truncated: false,
|
|
170
|
+
content: body.toString("base64"),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
// text
|
|
174
|
+
const cap = Math.min(
|
|
175
|
+
Number(message.maxBytes) > 0 ? Number(message.maxBytes) : TEXT_PREVIEW_CAP,
|
|
176
|
+
TEXT_PREVIEW_CAP,
|
|
177
|
+
);
|
|
178
|
+
const text = readFileSync(storedPath, "utf-8");
|
|
179
|
+
const truncated = text.length > cap;
|
|
180
|
+
server._send(ws, {
|
|
181
|
+
...base,
|
|
182
|
+
previewable: true,
|
|
183
|
+
encoding: "utf8",
|
|
184
|
+
truncated,
|
|
185
|
+
content: truncated ? text.slice(0, cap) : text,
|
|
186
|
+
});
|
|
187
|
+
} catch (err) {
|
|
188
|
+
sendError(server, id, ws, "ARTIFACT_CONTENT_FAILED", err.message);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function handleArtifactRemove(server, id, ws, message) {
|
|
193
|
+
try {
|
|
194
|
+
if (!message.artifactId) {
|
|
195
|
+
return sendError(server, id, ws, "NO_ARTIFACT_ID", "artifactId required");
|
|
196
|
+
}
|
|
197
|
+
const store = await loadStore();
|
|
198
|
+
const found = store.remove(message.artifactId);
|
|
199
|
+
server._send(ws, {
|
|
200
|
+
id,
|
|
201
|
+
type: "artifact-remove",
|
|
202
|
+
artifactId: message.artifactId,
|
|
203
|
+
removed: found ? message.artifactId : null,
|
|
204
|
+
found,
|
|
205
|
+
});
|
|
206
|
+
} catch (err) {
|
|
207
|
+
sendError(server, id, ws, "ARTIFACT_REMOVE_FAILED", err.message);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function handleArtifactClean(server, id, ws, message) {
|
|
212
|
+
try {
|
|
213
|
+
const store = await loadStore();
|
|
214
|
+
const { removed } = store.cleanupExpired();
|
|
215
|
+
server._send(ws, { id, type: "artifact-clean", removed });
|
|
216
|
+
} catch (err) {
|
|
217
|
+
sendError(server, id, ws, "ARTIFACT_CLEAN_FAILED", err.message);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -185,6 +185,12 @@ export function createWsMessageDispatcher(server) {
|
|
|
185
185
|
"bg-stop": () => server._handleBgStop(clientId, id, ws, message),
|
|
186
186
|
"bg-rename": () => server._handleBgRename(id, ws, message),
|
|
187
187
|
"bg-resume": () => server._handleBgResume(id, ws, message),
|
|
188
|
+
"artifact-list": () => server._handleArtifactList(id, ws, message),
|
|
189
|
+
"artifact-show": () => server._handleArtifactShow(id, ws, message),
|
|
190
|
+
"artifact-content": () =>
|
|
191
|
+
server._handleArtifactContent(id, ws, message),
|
|
192
|
+
"artifact-remove": () => server._handleArtifactRemove(id, ws, message),
|
|
193
|
+
"artifact-clean": () => server._handleArtifactClean(id, ws, message),
|
|
188
194
|
"chat.intent.understand": () =>
|
|
189
195
|
server._handleChatIntentUnderstand(id, ws, message),
|
|
190
196
|
"chat.intent.understand-stream": () =>
|
|
@@ -106,6 +106,13 @@ import {
|
|
|
106
106
|
handleBgStopTurn,
|
|
107
107
|
handleBgView,
|
|
108
108
|
} from "./background-agent-protocol.js";
|
|
109
|
+
import {
|
|
110
|
+
handleArtifactClean,
|
|
111
|
+
handleArtifactContent,
|
|
112
|
+
handleArtifactList,
|
|
113
|
+
handleArtifactRemove,
|
|
114
|
+
handleArtifactShow,
|
|
115
|
+
} from "./artifact-protocol.js";
|
|
109
116
|
|
|
110
117
|
const __filename = fileURLToPath(import.meta.url);
|
|
111
118
|
const __dirname = dirname(__filename);
|
|
@@ -717,6 +724,31 @@ export class ChainlessChainWSServer extends EventEmitter {
|
|
|
717
724
|
return handleBgResume(this, id, ws, message);
|
|
718
725
|
}
|
|
719
726
|
|
|
727
|
+
/** @private — list agent-published artifacts (metadata only) */
|
|
728
|
+
async _handleArtifactList(id, ws, message) {
|
|
729
|
+
return handleArtifactList(this, id, ws, message);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/** @private — one artifact's metadata + stored path */
|
|
733
|
+
async _handleArtifactShow(id, ws, message) {
|
|
734
|
+
return handleArtifactShow(this, id, ws, message);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/** @private — inline preview content (text utf8 / image base64, capped) */
|
|
738
|
+
async _handleArtifactContent(id, ws, message) {
|
|
739
|
+
return handleArtifactContent(this, id, ws, message);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** @private — remove one artifact (stored copy + index row) */
|
|
743
|
+
async _handleArtifactRemove(id, ws, message) {
|
|
744
|
+
return handleArtifactRemove(this, id, ws, message);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/** @private — drop expired artifacts (TTL) */
|
|
748
|
+
async _handleArtifactClean(id, ws, message) {
|
|
749
|
+
return handleArtifactClean(this, id, ws, message);
|
|
750
|
+
}
|
|
751
|
+
|
|
720
752
|
/** @private */
|
|
721
753
|
async _handleCompressionStats(id, ws, message) {
|
|
722
754
|
return handleCompressionStats(this, id, ws, message);
|
package/src/index.js
CHANGED
|
@@ -162,6 +162,9 @@ import { registerAgendaCommand } from "./commands/agenda.js";
|
|
|
162
162
|
// Batch: dynamic worktree batch — split a change into parallel units (第四阶段 #4)
|
|
163
163
|
import { registerBatchCommand } from "./commands/batch.js";
|
|
164
164
|
|
|
165
|
+
// Artifacts: agent-published deliverable store (P1 #10)
|
|
166
|
+
import { registerArtifactsCommand } from "./commands/artifacts.js";
|
|
167
|
+
|
|
165
168
|
// Web UI
|
|
166
169
|
import { registerUiCommand } from "./commands/ui.js";
|
|
167
170
|
import { registerPackCommand } from "./commands/pack.js";
|
|
@@ -598,6 +601,9 @@ export function createProgram(opts = {}) {
|
|
|
598
601
|
// Batch: dynamic worktree batch
|
|
599
602
|
registerBatchCommand(program);
|
|
600
603
|
|
|
604
|
+
// Artifacts: agent-published deliverable store
|
|
605
|
+
registerArtifactsCommand(program);
|
|
606
|
+
|
|
601
607
|
// Web UI
|
|
602
608
|
registerUiCommand(program);
|
|
603
609
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cc agent --ax-screen-reader` — screen-reader friendly output.
|
|
3
|
+
*
|
|
4
|
+
* Screen readers linearize the terminal: colors are invisible, and anything
|
|
5
|
+
* that repaints in place (the context status line) reads as noisy duplicate
|
|
6
|
+
* lines. This mode therefore:
|
|
7
|
+
* - forces the `mono` theme (chalk level 0 — no ANSI color codes) in the
|
|
8
|
+
* interactive REPL (consumed via CC_SCREEN_READER in agent-repl.js);
|
|
9
|
+
* - disables the repainting status line (reuses the existing
|
|
10
|
+
* CC_STATUSLINE=0 kill-switch).
|
|
11
|
+
* Headless (-p) text output is already plain sequential lines.
|
|
12
|
+
*
|
|
13
|
+
* Ambient activation: CC_SCREEN_READER=1 (so wrappers/launchers can turn it
|
|
14
|
+
* on without threading a flag through every entry point).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** env switches applied by --ax-screen-reader. */
|
|
18
|
+
export const SCREEN_READER_SWITCHES = Object.freeze({
|
|
19
|
+
CC_SCREEN_READER: "1", // REPL: force mono theme / no ANSI decorations
|
|
20
|
+
CC_STATUSLINE: "0", // in-place repainting status line off
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Apply screen-reader mode to an env (default process.env).
|
|
25
|
+
* @returns {string[]} the switch names applied (for the startup notice)
|
|
26
|
+
*/
|
|
27
|
+
export function applyScreenReaderMode(env = process.env) {
|
|
28
|
+
for (const [k, v] of Object.entries(SCREEN_READER_SWITCHES)) {
|
|
29
|
+
env[k] = v;
|
|
30
|
+
}
|
|
31
|
+
return Object.keys(SCREEN_READER_SWITCHES);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether screen-reader mode is requested — the explicit `--ax-screen-reader`
|
|
36
|
+
* flag or the ambient CC_SCREEN_READER env var (1 / true / yes / on).
|
|
37
|
+
* @param {{ axScreenReader?: boolean }} [opts]
|
|
38
|
+
* @param {object} [env=process.env]
|
|
39
|
+
* @returns {boolean}
|
|
40
|
+
*/
|
|
41
|
+
export function screenReaderRequested(opts = {}, env = process.env) {
|
|
42
|
+
if (opts && opts.axScreenReader === true) return true;
|
|
43
|
+
return screenReaderActive(env);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Whether screen-reader mode is ACTIVE in the environment (what downstream
|
|
48
|
+
* consumers — the REPL theme init — should check).
|
|
49
|
+
* @param {object} [env=process.env]
|
|
50
|
+
* @returns {boolean}
|
|
51
|
+
*/
|
|
52
|
+
export function screenReaderActive(env = process.env) {
|
|
53
|
+
const raw = env.CC_SCREEN_READER;
|
|
54
|
+
return raw != null && /^(1|true|yes|on)$/i.test(String(raw).trim());
|
|
55
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ArtifactStore — durable home for agent DELIVERABLES (gap-analysis P1 #10).
|
|
3
|
+
*
|
|
4
|
+
* An agent run produces important files — a Markdown report, a patch, a
|
|
5
|
+
* screenshot, test logs, review findings JSON — that otherwise live as loose
|
|
6
|
+
* paths the user has to remember. `publish_artifact` copies such a file into
|
|
7
|
+
* `~/.chainlesschain/artifacts/` with metadata (title, kind, mime, sha256,
|
|
8
|
+
* session), so:
|
|
9
|
+
* - the session transcript only records the small METADATA object (the tool
|
|
10
|
+
* result), never the file body;
|
|
11
|
+
* - `cc artifacts` lists/inspects/cleans them across sessions;
|
|
12
|
+
* - web-panel / remote surfaces can later serve previews from one place.
|
|
13
|
+
*
|
|
14
|
+
* Layout:
|
|
15
|
+
* ~/.chainlesschain/artifacts/index.jsonl one metadata row per artifact
|
|
16
|
+
* ~/.chainlesschain/artifacts/files/<id><ext> the stored copy
|
|
17
|
+
*
|
|
18
|
+
* Per-row tolerant JSONL (a corrupt line never poisons the store); injected
|
|
19
|
+
* clock + dir for unit tests; TTL-based expiry with an explicit cleanup call
|
|
20
|
+
* (`cc artifacts clean` / opportunistic on publish).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import fs from "fs";
|
|
24
|
+
import os from "os";
|
|
25
|
+
import path from "path";
|
|
26
|
+
import crypto from "crypto";
|
|
27
|
+
|
|
28
|
+
export const ARTIFACT_KINDS = Object.freeze([
|
|
29
|
+
"report",
|
|
30
|
+
"patch",
|
|
31
|
+
"screenshot",
|
|
32
|
+
"log",
|
|
33
|
+
"data",
|
|
34
|
+
"other",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
/** Copy-size guard: an "artifact" is a deliverable, not a build tree. */
|
|
38
|
+
export const MAX_ARTIFACT_BYTES = 100 * 1024 * 1024; // 100 MB
|
|
39
|
+
|
|
40
|
+
/** Default retention before `clean` removes an artifact. */
|
|
41
|
+
export const DEFAULT_TTL_DAYS = 30;
|
|
42
|
+
|
|
43
|
+
const MIME_BY_EXT = Object.freeze({
|
|
44
|
+
".md": "text/markdown",
|
|
45
|
+
".markdown": "text/markdown",
|
|
46
|
+
".txt": "text/plain",
|
|
47
|
+
".log": "text/plain",
|
|
48
|
+
".json": "application/json",
|
|
49
|
+
".html": "text/html",
|
|
50
|
+
".htm": "text/html",
|
|
51
|
+
".csv": "text/csv",
|
|
52
|
+
".xml": "application/xml",
|
|
53
|
+
".yaml": "application/yaml",
|
|
54
|
+
".yml": "application/yaml",
|
|
55
|
+
".patch": "text/x-patch",
|
|
56
|
+
".diff": "text/x-patch",
|
|
57
|
+
".png": "image/png",
|
|
58
|
+
".jpg": "image/jpeg",
|
|
59
|
+
".jpeg": "image/jpeg",
|
|
60
|
+
".gif": "image/gif",
|
|
61
|
+
".webp": "image/webp",
|
|
62
|
+
".svg": "image/svg+xml",
|
|
63
|
+
".pdf": "application/pdf",
|
|
64
|
+
".zip": "application/zip",
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export function artifactsDir(homedir = os.homedir()) {
|
|
68
|
+
// Test/deployment override (same convention as CC_BACKGROUND_AGENTS_DIR).
|
|
69
|
+
if (process.env.CC_ARTIFACTS_DIR) return process.env.CC_ARTIFACTS_DIR;
|
|
70
|
+
return path.join(homedir, ".chainlesschain", "artifacts");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function mimeForExt(ext) {
|
|
74
|
+
return (
|
|
75
|
+
MIME_BY_EXT[String(ext || "").toLowerCase()] || "application/octet-stream"
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeKind(kind) {
|
|
80
|
+
const k = typeof kind === "string" ? kind.trim().toLowerCase() : "";
|
|
81
|
+
return ARTIFACT_KINDS.includes(k) ? k : "other";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export class ArtifactStore {
|
|
85
|
+
constructor({ dir = null, now = () => Date.now() } = {}) {
|
|
86
|
+
this.dir = dir || artifactsDir();
|
|
87
|
+
this._now = typeof now === "function" ? now : () => now;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_indexFile() {
|
|
91
|
+
return path.join(this.dir, "index.jsonl");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
_filesDir() {
|
|
95
|
+
return path.join(this.dir, "files");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
_ensureDirs() {
|
|
99
|
+
fs.mkdirSync(this._filesDir(), { recursive: true });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Publish a file as an artifact (copy + metadata row).
|
|
104
|
+
* @param {{ filePath: string, title?: string, kind?: string,
|
|
105
|
+
* sessionId?: string|null, ttlDays?: number }} opts
|
|
106
|
+
* @returns {object} the metadata entry (what the tool returns — no body)
|
|
107
|
+
*/
|
|
108
|
+
publish({ filePath, title, kind, sessionId = null, ttlDays } = {}) {
|
|
109
|
+
if (!filePath) throw new Error("publish requires a filePath");
|
|
110
|
+
const abs = path.resolve(filePath);
|
|
111
|
+
let stat;
|
|
112
|
+
try {
|
|
113
|
+
stat = fs.statSync(abs);
|
|
114
|
+
} catch {
|
|
115
|
+
throw new Error(`artifact source not found: ${abs}`);
|
|
116
|
+
}
|
|
117
|
+
if (!stat.isFile()) {
|
|
118
|
+
throw new Error(`artifact source is not a regular file: ${abs}`);
|
|
119
|
+
}
|
|
120
|
+
if (stat.size > MAX_ARTIFACT_BYTES) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`artifact too large (${stat.size} bytes > ${MAX_ARTIFACT_BYTES}); artifacts are deliverables, not build output`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const body = fs.readFileSync(abs);
|
|
126
|
+
const sha256 = crypto.createHash("sha256").update(body).digest("hex");
|
|
127
|
+
const ext = path.extname(abs);
|
|
128
|
+
const id = `art_${this._now().toString(36)}_${crypto
|
|
129
|
+
.randomBytes(4)
|
|
130
|
+
.toString("hex")}`;
|
|
131
|
+
const days = Number.isFinite(Number(ttlDays))
|
|
132
|
+
? Math.max(1, Number(ttlDays))
|
|
133
|
+
: DEFAULT_TTL_DAYS;
|
|
134
|
+
const createdAt = this._now();
|
|
135
|
+
const entry = {
|
|
136
|
+
id,
|
|
137
|
+
title: String(title || path.basename(abs)),
|
|
138
|
+
kind: normalizeKind(kind),
|
|
139
|
+
mime: mimeForExt(ext),
|
|
140
|
+
size: stat.size,
|
|
141
|
+
sha256,
|
|
142
|
+
sourcePath: abs,
|
|
143
|
+
file: `${id}${ext}`,
|
|
144
|
+
sessionId: sessionId ? String(sessionId) : null,
|
|
145
|
+
createdAt: new Date(createdAt).toISOString(),
|
|
146
|
+
expiresAt: new Date(createdAt + days * 24 * 60 * 60 * 1000).toISOString(),
|
|
147
|
+
};
|
|
148
|
+
this._ensureDirs();
|
|
149
|
+
fs.writeFileSync(path.join(this._filesDir(), entry.file), body);
|
|
150
|
+
fs.appendFileSync(this._indexFile(), JSON.stringify(entry) + "\n", "utf-8");
|
|
151
|
+
return entry;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** All (non-corrupt) metadata rows, oldest first. */
|
|
155
|
+
list({ sessionId } = {}) {
|
|
156
|
+
let raw;
|
|
157
|
+
try {
|
|
158
|
+
raw = fs.readFileSync(this._indexFile(), "utf-8");
|
|
159
|
+
} catch {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
const out = [];
|
|
163
|
+
for (const line of raw.split("\n")) {
|
|
164
|
+
const t = line.trim();
|
|
165
|
+
if (!t) continue;
|
|
166
|
+
try {
|
|
167
|
+
const e = JSON.parse(t);
|
|
168
|
+
if (e && typeof e === "object" && e.id) out.push(e);
|
|
169
|
+
} catch {
|
|
170
|
+
/* per-row tolerance — a corrupt line never poisons the store */
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return sessionId ? out.filter((e) => e.sessionId === sessionId) : out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** One entry by id (or null). */
|
|
177
|
+
get(id) {
|
|
178
|
+
return this.list().find((e) => e.id === id) || null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Absolute path of an artifact's stored copy (or null). */
|
|
182
|
+
storedPath(idOrEntry) {
|
|
183
|
+
const entry =
|
|
184
|
+
typeof idOrEntry === "object" && idOrEntry
|
|
185
|
+
? idOrEntry
|
|
186
|
+
: this.get(idOrEntry);
|
|
187
|
+
if (!entry) return null;
|
|
188
|
+
return path.join(this._filesDir(), entry.file);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Remove one artifact (file + index row). @returns {boolean} found */
|
|
192
|
+
remove(id) {
|
|
193
|
+
const entries = this.list();
|
|
194
|
+
const target = entries.find((e) => e.id === id);
|
|
195
|
+
if (!target) return false;
|
|
196
|
+
try {
|
|
197
|
+
fs.rmSync(this.storedPath(target), { force: true });
|
|
198
|
+
} catch {
|
|
199
|
+
/* best-effort — the index row is the source of truth */
|
|
200
|
+
}
|
|
201
|
+
this._rewrite(entries.filter((e) => e.id !== id));
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Remove expired artifacts (expiresAt in the past).
|
|
207
|
+
* @returns {{ removed: number }}
|
|
208
|
+
*/
|
|
209
|
+
cleanupExpired() {
|
|
210
|
+
const entries = this.list();
|
|
211
|
+
const now = this._now();
|
|
212
|
+
const keep = [];
|
|
213
|
+
let removed = 0;
|
|
214
|
+
for (const e of entries) {
|
|
215
|
+
const exp = Date.parse(e.expiresAt || "");
|
|
216
|
+
if (Number.isFinite(exp) && exp <= now) {
|
|
217
|
+
try {
|
|
218
|
+
fs.rmSync(this.storedPath(e), { force: true });
|
|
219
|
+
} catch {
|
|
220
|
+
/* best-effort */
|
|
221
|
+
}
|
|
222
|
+
removed += 1;
|
|
223
|
+
} else {
|
|
224
|
+
keep.push(e);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (removed > 0) this._rewrite(keep);
|
|
228
|
+
return { removed };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
_rewrite(entries) {
|
|
232
|
+
this._ensureDirs();
|
|
233
|
+
const text = entries.map((e) => JSON.stringify(e)).join("\n");
|
|
234
|
+
fs.writeFileSync(this._indexFile(), text ? text + "\n" : "", "utf-8");
|
|
235
|
+
}
|
|
236
|
+
}
|