@rubytech/create-maxy-code 0.1.40 → 0.1.43
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 +2 -2
- package/payload/platform/lib/mcp-stderr-tee/package.json +7 -0
- package/payload/platform/plugins/admin/hooks/turn-completed-graph-write.sh +142 -52
- package/payload/platform/plugins/docs/references/deployment.md +6 -0
- package/payload/platform/plugins/docs/references/platform.md +18 -4
- package/payload/platform/plugins/docs/references/plugins-guide.md +2 -0
- package/payload/platform/scripts/check-no-esm-require.mjs +132 -0
- package/payload/platform/scripts/smoke-boot-services.sh +175 -0
- package/payload/platform/services/claude-session-manager/dist/config.d.ts +17 -0
- package/payload/platform/services/claude-session-manager/dist/config.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/config.js +31 -0
- package/payload/platform/services/claude-session-manager/dist/config.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts +63 -0
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.js +514 -0
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +3 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +364 -204
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +38 -11
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-path.d.ts +2 -0
- package/payload/platform/services/claude-session-manager/dist/jsonl-path.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-path.js +11 -6
- package/payload/platform/services/claude-session-manager/dist/jsonl-path.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +37 -44
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +136 -159
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/reaper.d.ts +24 -0
- package/payload/platform/services/claude-session-manager/dist/reaper.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/reaper.js +114 -0
- package/payload/platform/services/claude-session-manager/dist/reaper.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/session-store.d.ts +16 -41
- package/payload/platform/services/claude-session-manager/dist/session-store.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-store.js +29 -68
- package/payload/platform/services/claude-session-manager/dist/session-store.js.map +1 -1
- package/payload/premium-plugins/real-agent/lib/mcp-stderr-tee/dist/index.d.ts +51 -0
- package/payload/premium-plugins/real-agent/lib/mcp-stderr-tee/dist/index.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/lib/mcp-stderr-tee/dist/index.js +196 -0
- package/payload/premium-plugins/real-agent/lib/mcp-stderr-tee/dist/index.js.map +1 -0
- package/payload/premium-plugins/real-agent/lib/mcp-stderr-tee/package.json +7 -0
- package/payload/server/public/assets/{admin-DO9W2D9g.js → admin-CzJDs5FQ.js} +24 -24
- package/payload/server/public/assets/{arc-DMDAZHAN.js → arc-aUiRP9AS.js} +1 -1
- package/payload/server/public/assets/architecture-YZFGNWBL--v-pJPNp.js +1 -0
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-BbLtk_k6.js → architectureDiagram-Q4EWVU46-DJvcnhtT.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-B9IgcgeJ.js → blockDiagram-DXYQGD6D-_vt7n9ql.js} +1 -1
- package/payload/server/public/assets/{brand-DQrOw38P.css → brand-DNSV7-vn.css} +1 -1
- package/payload/server/public/assets/{brand-Bky-hMVw.js → brand-FZ59T8pI.js} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-BrvPS09f.js → c4Diagram-AHTNJAMY-CdEo9QqN.js} +1 -1
- package/payload/server/public/assets/channel-DzmCBQ_9.js +1 -0
- package/payload/server/public/assets/{chunk-2KRD3SAO-Di4bO8ir.js → chunk-2KRD3SAO-BK3470lx.js} +1 -1
- package/payload/server/public/assets/chunk-336JU56O-JmuLOuir.js +2 -0
- package/payload/server/public/assets/chunk-426QAEUC-10f8pXpz.js +1 -0
- package/payload/server/public/assets/{chunk-4BX2VUAB-DyEhFk-Z.js → chunk-4BX2VUAB-BOvVdJLf.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-D7_xc8xf.js → chunk-4TB4RGXK-DajMd0ug.js} +1 -1
- package/payload/server/public/assets/{chunk-55IACEB6-BRJOZLpU.js → chunk-55IACEB6-BwZyF7vR.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-CdCSpb8j.js → chunk-5FUZZQ4R-B4A-XUHI.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-BVgnD9QR.js → chunk-5PVQY5BW-BNM3rT14.js} +1 -1
- package/payload/server/public/assets/{chunk-67CJDMHE-BG6-9r6c.js → chunk-67CJDMHE-D5bhMrtY.js} +1 -1
- package/payload/server/public/assets/{chunk-7N4EOEYR-BvMbitX7.js → chunk-7N4EOEYR-Si7Lgrwc.js} +1 -1
- package/payload/server/public/assets/{chunk-AA7GKIK3-CE8mGBD5.js → chunk-AA7GKIK3-DMuHtDqO.js} +1 -1
- package/payload/server/public/assets/{chunk-BSJP7CBP-DP7LTBll.js → chunk-BSJP7CBP-L79XKVcb.js} +1 -1
- package/payload/server/public/assets/{chunk-CIAEETIT-VfnIdN-h.js → chunk-CIAEETIT-C0O7Upmg.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-CYrT1Tom.js → chunk-EDXVE4YY-atMwO81K.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-B70d0jN7.js → chunk-ENJZ2VHE-BUbZv2Nz.js} +1 -1
- package/payload/server/public/assets/{chunk-FMBD7UC4-w-yBZsN2.js → chunk-FMBD7UC4-C_E43NFJ.js} +1 -1
- package/payload/server/public/assets/{chunk-FOC6F5B3-f9cFywhd.js → chunk-FOC6F5B3-D9lWWHAu.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-BcLouRbd.js → chunk-ICPOFSXX-BMF-eWHb.js} +2 -2
- package/payload/server/public/assets/{chunk-K5T4RW27-B_ZUrFUq.js → chunk-K5T4RW27-DuhsNH4c.js} +1 -1
- package/payload/server/public/assets/{chunk-KGLVRYIC-CcWTvRlI.js → chunk-KGLVRYIC-B4-A1Abi.js} +1 -1
- package/payload/server/public/assets/{chunk-LIHQZDEY-D-5-peQw.js → chunk-LIHQZDEY-BxqgHRgT.js} +1 -1
- package/payload/server/public/assets/{chunk-ORNJ4GCN-B4Z5L25I.js → chunk-ORNJ4GCN-DEYQ5WaJ.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-C2ja2fPj.js → chunk-OYMX7WX6-BEMDmPTI.js} +1 -1
- package/payload/server/public/assets/chunk-QZHKN3VN-Bd-GrQM6.js +1 -0
- package/payload/server/public/assets/{chunk-U2HBQHQK-CCjoTpL6.js → chunk-U2HBQHQK-DOM7Y4f1.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-QIVkp-XR.js → chunk-X2U36JSP-C_uWyW6Y.js} +1 -1
- package/payload/server/public/assets/{chunk-XPW4576I-m1Y_r88I.js → chunk-XPW4576I-CBfZXZDB.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-qD9rebhx.js → chunk-YZCP3GAM-DQbgl47k.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-C0J_f05H.js → chunk-ZZ45TVLE-C44DEAgq.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-DVoOmecn.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-B0r3hpf9.js +1 -0
- package/payload/server/public/assets/clone-Dde30M8J.js +1 -0
- package/payload/server/public/assets/{cose-bilkent-S5V4N54A-BhtgY3T7.js → cose-bilkent-S5V4N54A-Br2gjtEO.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-C8lVFO5U.js → dagre-KV5264BT-CE3meBlZ.js} +1 -1
- package/payload/server/public/assets/{dagre-B3cA7jId.js → dagre-lNEvWLDX.js} +1 -1
- package/payload/server/public/assets/data-DW6QAnMD.js +1 -0
- package/payload/server/public/assets/{device-url-actions-qu2-XiHc.js → device-url-actions-a6Bd-MRk.js} +1 -1
- package/payload/server/public/assets/{diagram-5BDNPKRD-BYkcTelK.js → diagram-5BDNPKRD-BqR72jWu.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-B6f_WR4j.js → diagram-G4DWMVQ6-DOjamNAP.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-ALaKcN6w.js → diagram-MMDJMWI5-DAUB-aPu.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-CkVg3crF.js → diagram-TYMM5635-DpToW00F.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-DszlTGIQ.js → erDiagram-SMLLAGMA-C-p2ze2T.js} +1 -1
- package/payload/server/public/assets/{flatten-BsWEYbBB.js → flatten-Bo6YRmWl.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-C9-FqSlk.js → flowDiagram-DWJPFMVM-B9SS6a8F.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-faJU-5Zy.js → ganttDiagram-T4ZO3ILL-DnXZX0Gp.js} +1 -1
- package/payload/server/public/assets/gitGraph-7Q5UKJZL-CI0s_tqn.js +1 -0
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-Cck8_tiG.js → gitGraphDiagram-UUTBAWPF-CtQ0FzPg.js} +1 -1
- package/payload/server/public/assets/graph-D13IJB3w.js +1 -0
- package/payload/server/public/assets/graph-labels-CI9WmEiE.js +1 -0
- package/payload/server/public/assets/{graphlib-CYRxeBXj.js → graphlib-nUaDKzkp.js} +1 -1
- package/payload/server/public/assets/info-OMHHGYJF-g3gYW7Qm.js +1 -0
- package/payload/server/public/assets/infoDiagram-42DDH7IO-c2wWbEaX.js +2 -0
- package/payload/server/public/assets/{isEmpty-BWl67LAZ.js → isEmpty-D6Kr-M1M.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-nSIaVXdw.js → ishikawaDiagram-UXIWVN3A-BBOgnjrU.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-DuKq7d3g.js → journeyDiagram-VCZTEJTY-DzIkOZor.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-Bt5xqHfY.js → kanban-definition-6JOO6SKY-NoAdbB_E.js} +1 -1
- package/payload/server/public/assets/{line-BgV8qCP_.js → line-BRK1p00q.js} +1 -1
- package/payload/server/public/assets/{linear-0O14Y6uf.js → linear-DOh_6k2k.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-DQzl__pY.js → mermaid-parser.core-Bm0OvYTL.js} +2 -2
- package/payload/server/public/assets/{mermaid.core-BAPEOjwh.js → mermaid.core-CTznq93v.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-CmMBrI79.js → mindmap-definition-QFDTVHPH-BOQ1enWi.js} +1 -1
- package/payload/server/public/assets/{ordinal-krseTxxN.js → ordinal-BDi6f4xk.js} +1 -1
- package/payload/server/public/assets/packet-4T2RLAQJ-CT0TB9HI.js +1 -0
- package/payload/server/public/assets/{page-BoDdtGsH.js → page-BLNJpxTT.js} +2 -2
- package/payload/server/public/assets/{page-D_NCNi5j.js → page-Dn3gASQM.js} +1 -1
- package/payload/server/public/assets/pie-ZZUOXDRM-CXLe7TFF.js +1 -0
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-BhsR9k6j.js → pieDiagram-DEJITSTG-2S-XHkal.js} +1 -1
- package/payload/server/public/assets/{public-DCcGKIMW.js → public-D0z7BALL.js} +5 -5
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-DSKV6d_A.js → quadrantDiagram-34T5L4WZ-CncZqXXr.js} +1 -1
- package/payload/server/public/assets/radar-PYXPWWZC-DnPLBl-D.js +1 -0
- package/payload/server/public/assets/{reduce-tk-xY6Fv.js → reduce-CGi9ik8i.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-BccM48AI.js → requirementDiagram-MS252O5E-CPeWs9fs.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-C0pxiQfn.js → sankeyDiagram-XADWPNL6-A4_669ey.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-BVsUIJsW.js → sequenceDiagram-FGHM5R23-DD-qEurY.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-lqYQZ-F1.js → stateDiagram-FHFEXIEX-CdO6Mw_T.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-D28bJZnf.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-D-LxYOHq.js → timeline-definition-GMOUNBTQ-JH65fN1V.js} +1 -1
- package/payload/server/public/assets/treeView-SZITEDCU-C3cb7Xwe.js +1 -0
- package/payload/server/public/assets/treemap-W4RFUUIX-Dc7G3Bgm.js +1 -0
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-BNHzvNba.js → vennDiagram-DHZGUBPP-CJLoNrBL.js} +1 -1
- package/payload/server/public/assets/wardley-RL74JXVD-DtgibWAt.js +1 -0
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-BfXR9l5s.js → wardleyDiagram-NUSXRM2D-BgM9eEf7.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-CL7mVEbT.js → xychartDiagram-5P7HB3ND-DCmesmc9.js} +1 -1
- package/payload/server/public/data.html +5 -5
- package/payload/server/public/graph.html +5 -6
- package/payload/server/public/index.html +7 -8
- package/payload/server/public/public.html +4 -5
- package/payload/server/server.js +94 -4
- package/payload/server/public/assets/Checkbox-6lLDKzMP.js +0 -1
- package/payload/server/public/assets/architecture-YZFGNWBL-COhEvUpo.js +0 -1
- package/payload/server/public/assets/channel-Bmc1qlCD.js +0 -1
- package/payload/server/public/assets/chunk-336JU56O-DXOr0ZnC.js +0 -2
- package/payload/server/public/assets/chunk-426QAEUC-BFW4zwrV.js +0 -1
- package/payload/server/public/assets/chunk-QZHKN3VN-CWh_0JsP.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-RVZorqSk.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BEZem5bl.js +0 -1
- package/payload/server/public/assets/clone-B6eXIhiR.js +0 -1
- package/payload/server/public/assets/data-BaSmBQ7U.js +0 -1
- package/payload/server/public/assets/gitGraph-7Q5UKJZL-DeTNsAO0.js +0 -1
- package/payload/server/public/assets/graph-labels-COhLbJD0.js +0 -1
- package/payload/server/public/assets/graph-qg6fkRyC.js +0 -1
- package/payload/server/public/assets/info-OMHHGYJF-DJJ9GlS6.js +0 -1
- package/payload/server/public/assets/infoDiagram-42DDH7IO-CLjEjhJX.js +0 -2
- package/payload/server/public/assets/packet-4T2RLAQJ-CGbvGkvF.js +0 -1
- package/payload/server/public/assets/pie-ZZUOXDRM-BZy8rjFn.js +0 -1
- package/payload/server/public/assets/radar-PYXPWWZC-DcfWIVXr.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-D2eHThhC.js +0 -1
- package/payload/server/public/assets/treeView-SZITEDCU-3WugwVdj.js +0 -1
- package/payload/server/public/assets/treemap-W4RFUUIX-Cf5mDLlu.js +0 -1
- package/payload/server/public/assets/wardley-RL74JXVD-Bv4md4b3.js +0 -1
- /package/payload/server/public/assets/{_baseFor-BHtDrjIo.js → _baseFor-Cam2PbSt.js} +0 -0
- /package/payload/server/public/assets/{array-DetWRiSa.js → array-DYRGGQae.js} +0 -0
- /package/payload/server/public/assets/{cytoscape.esm-C9yNhe1u.js → cytoscape.esm-nWsJMTNI.js} +0 -0
- /package/payload/server/public/assets/{defaultLocale-_WRwicXn.js → defaultLocale-Du1XY3Dp.js} +0 -0
- /package/payload/server/public/assets/{dist-Bd4S37oi.js → dist-BzAsli7o.js} +0 -0
- /package/payload/server/public/assets/{init-sTEcj9YX.js → init-B5BXBRcm.js} +0 -0
- /package/payload/server/public/assets/{katex-s61Rgv6l.js → katex-HOUACuRw.js} +0 -0
- /package/payload/server/public/assets/{path-B0Ik7Tu9.js → path-CNO468J-.js} +0 -0
- /package/payload/server/public/assets/{rough.esm-DKRO8IF-.js → rough.esm-DRO6hWPh.js} +0 -0
- /package/payload/server/public/assets/{src-B6XdH6xq.js → src-CWiyyVfn.js} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rubytech/create-maxy-code",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.43",
|
|
4
4
|
"description": "Install Maxy — AI for Productive People",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-maxy-code": "./dist/index.js"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"build": "tsc",
|
|
14
14
|
"bundle": "node scripts/bundle.js",
|
|
15
15
|
"test": "npm run build && node --test 'dist/__tests__/*.test.js'",
|
|
16
|
-
"prepublishOnly": "bash ../../platform/scripts/verify-skill-tool-surface.sh && node ../../platform/scripts/check-no-task-id-leaks.mjs && node ../../platform/scripts/check-skill-load-coverage.mjs && node ../../platform/ui/scripts/check-route-wiring.mjs && node ../../platform/ui/scripts/check-edge-admin-routes.mjs && npm run build && node --test 'dist/__tests__/*.test.js' && chmod +x dist/index.js && npm run bundle && node ../../platform/ui/scripts/check-bundle-node-imports.mjs --dir=./payload/server/public/assets"
|
|
16
|
+
"prepublishOnly": "bash ../../platform/scripts/verify-skill-tool-surface.sh && node ../../platform/scripts/check-no-task-id-leaks.mjs && node ../../platform/scripts/check-no-esm-require.mjs && node ../../platform/scripts/check-skill-load-coverage.mjs && node ../../platform/ui/scripts/check-route-wiring.mjs && node ../../platform/ui/scripts/check-edge-admin-routes.mjs && npm run build && node --test 'dist/__tests__/*.test.js' && chmod +x dist/index.js && npm run bundle && bash ../../platform/scripts/smoke-boot-services.sh && node ../../platform/ui/scripts/check-bundle-node-imports.mjs --dir=./payload/server/public/assets"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"dist",
|
|
@@ -1,37 +1,71 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Stop hook — fires on every completed admin-agent turn and dispatches one
|
|
3
|
-
# background PTY against `specialists:database-operator
|
|
4
|
-
#
|
|
5
|
-
#
|
|
3
|
+
# background PTY against `specialists:database-operator`. The hidden PTY is
|
|
4
|
+
# the only writer to the Neo4j graph; the admin agent stays focused on the
|
|
5
|
+
# operator's request.
|
|
6
|
+
#
|
|
7
|
+
# Pipeline (Task 094 redesign):
|
|
8
|
+
# 1. /spawn (no stdinPayload) — manager awaits its watcher's PID-file
|
|
9
|
+
# event before returning. Response carries the recorder's intrinsic
|
|
10
|
+
# sessionId.
|
|
11
|
+
# 2. POST /<recorder-sessionId>/input — body is the database-operator
|
|
12
|
+
# prompt followed by the filtered JSONL tail of the just-completed
|
|
13
|
+
# turn (user / assistant / tool-call records only; queue-operation,
|
|
14
|
+
# auto_mode, deferred_tools_delta, hook_*, last-prompt, skill_listing,
|
|
15
|
+
# mcp_instructions_delta, permission-mode are dropped).
|
|
16
|
+
# 3. When the recorder's Stop hook fires, this hook runs again INSIDE
|
|
17
|
+
# the recorder PTY; the MAXY_HIDDEN_SPAWN gate exits early so no
|
|
18
|
+
# recursion. The recorder's PTY exits naturally; its Stop is then
|
|
19
|
+
# driven by POST /<sessionId>/stop from the same hook invocation
|
|
20
|
+
# that posted /input.
|
|
6
21
|
#
|
|
7
22
|
# Gating:
|
|
8
|
-
# - MAXY_SESSION_ROLE must equal "admin"
|
|
9
|
-
# - MAXY_HIDDEN_SPAWN must equal "0" (skip recursive fires
|
|
10
|
-
# hidden database-operator PTY itself; without this gate every
|
|
11
|
-
# recorder turn would spawn another recorder forever)
|
|
23
|
+
# - MAXY_SESSION_ROLE must equal "admin"
|
|
24
|
+
# - MAXY_HIDDEN_SPAWN must equal "0" (skip recursive fires)
|
|
12
25
|
#
|
|
13
26
|
# Input: Claude Code's Stop hook stdin shape
|
|
14
27
|
# { "session_id": "<intrinsic>", "transcript_path": "<jsonl path>", ... }
|
|
15
28
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# Failure modes are silent (exit 0) — this hook is observability + write
|
|
20
|
-
# orchestration; a failure here must never break the operator's session.
|
|
29
|
+
# Failure modes are silent (exit 0) — a failure here must never break the
|
|
30
|
+
# operator's session.
|
|
21
31
|
|
|
22
32
|
set -uo pipefail
|
|
23
33
|
|
|
24
34
|
START_MS=$(python3 -c 'import time; print(int(time.time()*1000))')
|
|
25
35
|
|
|
26
|
-
# Gate 1: only fire on admin foreground PTYs.
|
|
27
36
|
if [ "${MAXY_SESSION_ROLE:-}" != "admin" ]; then
|
|
28
37
|
exit 0
|
|
29
38
|
fi
|
|
30
39
|
if [ "${MAXY_HIDDEN_SPAWN:-0}" = "1" ]; then
|
|
40
|
+
# Recorder PTY's own Stop hook. Read its session_id and ask the manager
|
|
41
|
+
# to SIGTERM the recorder so it does not idle in the active recents list
|
|
42
|
+
# after its single turn completes. Keep the JSONL — it's the audit trail.
|
|
43
|
+
if [ -t 0 ]; then
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
REC_INPUT=$(cat)
|
|
47
|
+
if [ -z "$REC_INPUT" ]; then
|
|
48
|
+
exit 0
|
|
49
|
+
fi
|
|
50
|
+
REC_SESSION_ID=$(printf '%s' "$REC_INPUT" | python3 -c '
|
|
51
|
+
import sys, json
|
|
52
|
+
try:
|
|
53
|
+
d = json.load(sys.stdin)
|
|
54
|
+
print(d.get("session_id", "") or "")
|
|
55
|
+
except Exception:
|
|
56
|
+
print("")
|
|
57
|
+
' 2>/dev/null)
|
|
58
|
+
if [ -z "$REC_SESSION_ID" ]; then
|
|
59
|
+
exit 0
|
|
60
|
+
fi
|
|
61
|
+
REC_PORT="${CLAUDE_SESSION_MANAGER_PORT:-19400}"
|
|
62
|
+
curl -sS -X POST \
|
|
63
|
+
--max-time 5 \
|
|
64
|
+
"http://127.0.0.1:${REC_PORT}/${REC_SESSION_ID}/stop" >/dev/null 2>&1
|
|
65
|
+
echo "[recorder] self-stop sessionId=${REC_SESSION_ID:0:8}" >&2
|
|
31
66
|
exit 0
|
|
32
67
|
fi
|
|
33
68
|
|
|
34
|
-
# Gate 2: stdin must be a JSON envelope. Fail-open on parse failure.
|
|
35
69
|
if [ -t 0 ]; then
|
|
36
70
|
exit 0
|
|
37
71
|
fi
|
|
@@ -40,8 +74,6 @@ if [ -z "$INPUT" ]; then
|
|
|
40
74
|
exit 0
|
|
41
75
|
fi
|
|
42
76
|
|
|
43
|
-
# Extract session_id and transcript_path via python3 (grep on JSON is
|
|
44
|
-
# fragile when values carry escaped quotes).
|
|
45
77
|
PARSED=$(printf '%s' "$INPUT" | python3 -c '
|
|
46
78
|
import sys, json
|
|
47
79
|
try:
|
|
@@ -52,63 +84,121 @@ try:
|
|
|
52
84
|
except Exception:
|
|
53
85
|
print("\t")
|
|
54
86
|
' 2>/dev/null)
|
|
55
|
-
|
|
87
|
+
ADMIN_SESSION_ID="${PARSED%% *}"
|
|
56
88
|
TRANSCRIPT_PATH="${PARSED#* }"
|
|
57
89
|
|
|
58
|
-
if [ -z "$
|
|
90
|
+
if [ -z "$ADMIN_SESSION_ID" ] || [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
|
|
59
91
|
exit 0
|
|
60
92
|
fi
|
|
61
93
|
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
94
|
+
# Filter the JSONL tail down to records the database-operator cares about:
|
|
95
|
+
# user prompts, assistant messages, and tool calls. Drop ephemeral
|
|
96
|
+
# admin-state records that bloat the prompt without graph value.
|
|
97
|
+
FILTERED_TAIL=$(python3 - "$TRANSCRIPT_PATH" <<'PY'
|
|
98
|
+
import sys, json
|
|
99
|
+
DROP = {
|
|
100
|
+
'queue-operation', 'auto_mode', 'deferred_tools_delta',
|
|
101
|
+
'last-prompt', 'skill_listing', 'mcp_instructions_delta',
|
|
102
|
+
'permission-mode',
|
|
103
|
+
}
|
|
104
|
+
def keep(rec):
|
|
105
|
+
t = rec.get('type') if isinstance(rec, dict) else None
|
|
106
|
+
if not isinstance(t, str): return False
|
|
107
|
+
if t in DROP: return False
|
|
108
|
+
if t.startswith('hook_'): return False
|
|
109
|
+
if t in ('user', 'assistant', 'message'): return True
|
|
110
|
+
if 'tool' in t.lower(): return True
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
path = sys.argv[1]
|
|
114
|
+
# Read whole file; admin transcripts are bounded by Claude Code's own
|
|
115
|
+
# compaction so the read cost is reasonable. Take the last 200 records
|
|
116
|
+
# from the kept set so a long transcript doesn't blow the prompt budget.
|
|
117
|
+
out = []
|
|
118
|
+
try:
|
|
119
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
120
|
+
for line in f:
|
|
121
|
+
line = line.strip()
|
|
122
|
+
if not line: continue
|
|
123
|
+
try: rec = json.loads(line)
|
|
124
|
+
except Exception: continue
|
|
125
|
+
if keep(rec): out.append(rec)
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
out = out[-200:]
|
|
129
|
+
print(json.dumps(out, separators=(',', ':')))
|
|
130
|
+
PY
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
FILTERED_COUNT=$(printf '%s' "$FILTERED_TAIL" | python3 -c 'import sys,json; d=json.load(sys.stdin); print(len(d) if isinstance(d,list) else 0)' 2>/dev/null || echo 0)
|
|
68
134
|
|
|
69
|
-
# Manager port — environment override or compile-time default. The
|
|
70
|
-
# installer's systemd unit stamps CLAUDE_SESSION_MANAGER_PORT so this
|
|
71
|
-
# resolves to the brand's loopback port at runtime.
|
|
72
135
|
MANAGER_PORT="${CLAUDE_SESSION_MANAGER_PORT:-19400}"
|
|
136
|
+
MANAGER_BASE="http://127.0.0.1:${MANAGER_PORT}"
|
|
73
137
|
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
# (admin-agent JSONL is more authoritative than any post-hoc summary the
|
|
77
|
-
# hook could synthesise).
|
|
138
|
+
# Spawn the recorder PTY. Manager blocks on its watcher's PID-file event
|
|
139
|
+
# and returns the recorder's intrinsic sessionId in the response body.
|
|
78
140
|
SPAWN_BODY=$(python3 -c '
|
|
79
|
-
import
|
|
80
|
-
sid = sys.argv[1]
|
|
81
|
-
tail = sys.argv[2]
|
|
141
|
+
import json, sys
|
|
82
142
|
body = {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
"stdinPayload": json.dumps({
|
|
89
|
-
"conversationId": sid,
|
|
90
|
-
"occurredAt": __import__("datetime").datetime.utcnow().isoformat() + "Z",
|
|
91
|
-
"transcriptTail": tail,
|
|
92
|
-
}),
|
|
143
|
+
"senderId": "turn-recorder",
|
|
144
|
+
"role": "admin",
|
|
145
|
+
"channel": "browser",
|
|
146
|
+
"hidden": True,
|
|
147
|
+
"specialist": "database-operator",
|
|
93
148
|
}
|
|
94
149
|
print(json.dumps(body))
|
|
95
|
-
'
|
|
150
|
+
')
|
|
96
151
|
|
|
97
|
-
|
|
152
|
+
SPAWN_RES=$(curl -sS -X POST \
|
|
153
|
+
-H 'Content-Type: application/json' \
|
|
154
|
+
--max-time 10 \
|
|
155
|
+
--data "$SPAWN_BODY" \
|
|
156
|
+
"${MANAGER_BASE}/spawn" 2>/dev/null)
|
|
157
|
+
|
|
158
|
+
RECORDER_SESSION_ID=$(printf '%s' "$SPAWN_RES" | python3 -c '
|
|
159
|
+
import sys, json
|
|
160
|
+
try:
|
|
161
|
+
d = json.load(sys.stdin)
|
|
162
|
+
print(d.get("sessionId", "") or "")
|
|
163
|
+
except Exception:
|
|
164
|
+
print("")
|
|
165
|
+
' 2>/dev/null)
|
|
166
|
+
|
|
167
|
+
if [ -z "$RECORDER_SESSION_ID" ]; then
|
|
168
|
+
echo "turn-completed-graph-write spawn-failed conversationId=${ADMIN_SESSION_ID:0:8}" >&2
|
|
98
169
|
exit 0
|
|
99
170
|
fi
|
|
100
171
|
|
|
101
|
-
#
|
|
102
|
-
|
|
103
|
-
|
|
172
|
+
# Compose the input message: prompt prefix + filtered transcript JSON.
|
|
173
|
+
INPUT_BODY=$(python3 -c '
|
|
174
|
+
import sys, json
|
|
175
|
+
sid = sys.argv[1]
|
|
176
|
+
tail_json = sys.argv[2]
|
|
177
|
+
prompt = (
|
|
178
|
+
"You are the database-operator specialist. Record the following completed turn "
|
|
179
|
+
"into the Neo4j graph. The transcript is a JSON array of filtered records "
|
|
180
|
+
"from the admin agent (user prompts, assistant responses, tool calls). "
|
|
181
|
+
f"Conversation id: {sid}.\n\nTranscript:\n{tail_json}\n"
|
|
182
|
+
)
|
|
183
|
+
print(json.dumps({"text": prompt}))
|
|
184
|
+
' "$ADMIN_SESSION_ID" "$FILTERED_TAIL" 2>/dev/null)
|
|
185
|
+
|
|
186
|
+
if [ -z "$INPUT_BODY" ]; then
|
|
187
|
+
exit 0
|
|
188
|
+
fi
|
|
189
|
+
|
|
190
|
+
INPUT_BYTES=$(printf '%s' "$INPUT_BODY" | wc -c | tr -d ' ')
|
|
191
|
+
|
|
104
192
|
curl -sS -X POST \
|
|
105
193
|
-H 'Content-Type: application/json' \
|
|
106
194
|
--max-time 5 \
|
|
107
|
-
--data "$
|
|
108
|
-
"
|
|
195
|
+
--data "$INPUT_BODY" \
|
|
196
|
+
"${MANAGER_BASE}/${RECORDER_SESSION_ID}/input" >/dev/null 2>&1
|
|
197
|
+
|
|
198
|
+
echo "[recorder] input-posted sessionId=${RECORDER_SESSION_ID:0:8} bytes=${INPUT_BYTES} filtered-from-records=${FILTERED_COUNT}" >&2
|
|
109
199
|
|
|
110
200
|
END_MS=$(python3 -c 'import time; print(int(time.time()*1000))')
|
|
111
201
|
ELAPSED=$((END_MS - START_MS))
|
|
112
|
-
echo "turn-completed-graph-write fired conversationId=${
|
|
202
|
+
echo "turn-completed-graph-write fired conversationId=${ADMIN_SESSION_ID:0:8} recorderSessionId=${RECORDER_SESSION_ID:0:8} filtered=${FILTERED_COUNT} ms=${ELAPSED}" >&2
|
|
113
203
|
|
|
114
204
|
exit 0
|
|
@@ -87,6 +87,12 @@ The manager also runs a boot-time self-test that renders a fixture compose call
|
|
|
87
87
|
|
|
88
88
|
LOUD-FAIL output is `startup-self-test system-prompt-sentinels=fail missing=<tag>` followed by an immediate process exit. Catches IDENTITY-promise-vs-emitter drift at boot rather than at the first real spawn.
|
|
89
89
|
|
|
90
|
+
### Pre-publish boot smoke
|
|
91
|
+
|
|
92
|
+
`platform/scripts/smoke-boot-services.sh` runs inside `prepublishOnly` of the installer (`packages/create-maxy-code/`). For every service in its `SERVICES` list it builds a synthetic install dir (`brand.json`, empty `plugins/`, a single `account.json` with `agents/admin/{IDENTITY,SOUL}.md`), spawns `node dist/index.js`, waits up to 5 s for the canonical `[claude-session-manager] boot port=…` line, SIGTERMs, and fails publish if any `^\[.*\] fatal ` line appears in stdout/stderr. Catches the class of regressions tsc and vitest miss because they evaluate TS source rather than the compiled ESM module (e.g. a stray `require()` in an ESM-typed package: tsc allows it because `@types/node` declares `require` globally, and Vite's vitest loader shims it, but native Node ESM throws `ReferenceError: require is not defined` at boot).
|
|
93
|
+
|
|
94
|
+
Companion lint: `platform/scripts/check-no-esm-require.mjs` rejects `require(` calls in any `.ts/.tsx/.js` file inside a package with `"type": "module"`, also wired into `prepublishOnly`. Allowlist lives at the top of the script.
|
|
95
|
+
|
|
90
96
|
## Service Management
|
|
91
97
|
|
|
92
98
|
{{productName}} runs via systemd and starts automatically on boot. You don't need to start it manually. To check if it's running, ask {{productName}} "Check system status."
|
|
@@ -70,15 +70,17 @@ There is no dashboard, no settings panel, no menus. Everything is done through c
|
|
|
70
70
|
|
|
71
71
|
The chat input auto-grows as you type — it expands to fit your message and shrinks back when you delete text. You can also drag the resize handle above the input to set a custom height.
|
|
72
72
|
|
|
73
|
-
The admin interface is a three-pane layout: a sidebar on the left with navigation (Sessions, People, Agents, Projects, Tasks, Artefacts) and your recent conversations; the chat in the middle; and an artefact pane on the right that opens when you select a document, click a project, or open Browser, Data, or Graph from the menu, holding the surface side-by-side with the conversation so the chat stays live while you work in it. At the very top of the sidebar — above the nav rows — a borderless row holds two controls: a "+ New session" button on the left that spawns a fresh Claude Code session, and a Mode trigger on the right showing the current permission mode (Ask, Accept edits, Plan, or Auto). The sidebar's vertical order is: new-session strip first, then the nav (Sessions, People, Agents, Projects, Tasks, Artefacts), then the sessions list, then the footer. Both controls render as plain text-plus-icon affordances with no surrounding rectangle. The "+ New session" button is a text-width hit target — its clickable area is exactly the icon plus label, not the whole row — and shows no hover fill; the only hover feedback is the pointer cursor. The Mode trigger is pushed flush to the right edge of the row. Clicking the Mode trigger opens a popover downward from the row whose header reads "Mode" and lists the four permission modes with the current selection check-marked. The sidebar's nav rows swap the list view in place: Sessions shows recent conversations, Projects shows your active work projects, and Artefacts lists every KnowledgeDocument plus this account's agent templates (your admin agent's IDENTITY, SOUL, and KNOWLEDGE files plus one entry per enabled specialist). Each recent session row carries a
|
|
73
|
+
The admin interface is a three-pane layout: a sidebar on the left with navigation (Sessions, People, Agents, Projects, Tasks, Artefacts) and your recent conversations; the chat in the middle; and an artefact pane on the right that opens when you select a document, click a project, or open Browser, Data, or Graph from the menu, holding the surface side-by-side with the conversation so the chat stays live while you work in it. At the very top of the sidebar — above the nav rows — a borderless row holds two controls: a "+ New session" button on the left that spawns a fresh Claude Code session, and a Mode trigger on the right showing the current permission mode (Ask, Accept edits, Plan, or Auto). The sidebar's vertical order is: new-session strip first, then the nav (Sessions, People, Agents, Projects, Tasks, Artefacts), then the sessions list, then the footer. Both controls render as plain text-plus-icon affordances with no surrounding rectangle. The "+ New session" button is a text-width hit target — its clickable area is exactly the icon plus label, not the whole row — and shows no hover fill; the only hover feedback is the pointer cursor. The Mode trigger is pushed flush to the right edge of the row. Clicking the Mode trigger opens a popover downward from the row whose header reads "Mode" and lists the four permission modes with the current selection check-marked. The sidebar's nav rows swap the list view in place: Sessions shows recent conversations, Projects shows your active work projects, and Artefacts lists every KnowledgeDocument plus this account's agent templates (your admin agent's IDENTITY, SOUL, and KNOWLEDGE files plus one entry per enabled specialist). Each recent session row carries a three-state indicator: three pulsing dots when the session is busy (currently processing a turn), a solid sage dot when it is idle (live PTY waiting for input), and a hollow ring when it is archived (PTY exited, JSONL on disk for audit). The list itself splits into three views via a segmented control above the rows: **Active** shows every live session, **Archived** shows every JSONL on disk whose PTY has exited, and **All** shows both. The view choice persists across reloads. An "Include subagents" toggle inside the Active view surfaces specialist spawns (the database-operator recorder, premium-plugin agents, anything spawned with a `--agent` flag) which are hidden by default so the list reflects what you started directly. Each row also carries a small uppercase badge — `admin` for operator-driven sessions, the specialist name (for example `db-op`) for background work — so the source of any row is unambiguous at a glance. The People, Agents, and Tasks rows are graph shortcuts: clicking each opens the artefact-pane Graph filtered to every Person, every public Agent, or every Task in your account respectively, with no side-list, because the graph itself is the result. Public agents become first-class graph entities the moment you create them, with edges to their IDENTITY/SOUL/KNOWLEDGE files, edges to every knowledge document they have access to, and edges from every conversation they have handled, so a single Agents click reveals the whole shape of who knows what and who has been talking to whom. Click an artefact row to open the document. KnowledgeDocuments and your admin agent's templates are editable: type in the document and changes save automatically; specialist agent templates are read-only because they ship with Maxy and your edits would be overwritten on the next install. PDF artefacts render inline so you can read them without leaving the pane. If your browser doesn't have a built-in PDF viewer, a Download button appears instead. Artefacts that have no readable file backing them (orphan rows, files removed from disk, unsupported content types) show a one-line banner explaining the skip instead of opening to a blank pane. Click a project row to open the Graph view focused on that project's neighbourhood; clicking a second project swaps the focus rather than stacking on top. The chat and artefact divider is drag-resizable: drag the line between the columns to make either side wider; double-click it to reset to half of the available width (viewport minus sidebar), clamped to the chat and artefact min-width floors. Your chosen width is remembered across reloads. On wider screens (>1280px) all three panes are visible. The sidebar narrows at 1280px, the artefact pane hides at 1080px (Browser, Data, and Graph then open as full-window pages instead), and the sidebar collapses to a 56px icon rail at 820px. On every viewport the chat header reads left to right as a triptych: a dedicated sidebar toggle (the panel-right icon, which swaps to panel-right-open when the sidebar is showing), the brand mark next to the title in the centre, and the menu burger on the right. This header toggle is the sole sidebar-toggle button; the sidebar itself no longer carries a duplicate. Tap the sidebar toggle to show or hide the sidebar: on phones (<720px) it slides the drawer in or out, on wider screens it collapses or expands the sidebar column. The brand mark in the centre is decorative; clicks go through the dedicated toggle so the affordance is unambiguous. The drawer animation only fires on tap (220ms slide in or out); resizing your window across the 720px boundary snaps the layout without animation, so you never see a half-open flash. At ≤640px the session metadata pane stacks each label above its value instead of the desktop two-column grid, and the row of action buttons (Open in new tab / Open JSONL / Rename / Pin / Archive / End or Purge) collapses behind a single Actions trigger that opens a popover upward from the foot of the pane. Breakpoint summary: >1280px = full sidebar + chat + artefact pane (drag-resizable divider); 1280px→1080px = sidebar narrows; 1080px→820px = artefact pane hides (Browser/Data/Graph open as full-window pages instead); 820px→720px = sidebar collapses to 56px icon rail; ≤720px = sidebar becomes off-canvas drawer (vertical stack of nav, recents list, foot, the same shape as the desktop sidebar, just on top of the chat instead of beside it).
|
|
74
74
|
|
|
75
75
|
Page titles are brand-aware: the browser tab shows your product name (e.g. `Real Agent` instead of `Maxy`) on every shell — chat, graph, and data — so a non-default brand never leaks the default name in tab strips or browser history.
|
|
76
76
|
|
|
77
|
-
**Session lifecycle and reconcile model.** The sidebar Sessions list
|
|
77
|
+
**Session lifecycle and reconcile model.** The sidebar Sessions list is driven by a single Server-Sent Events feed at `/api/admin/claude-sessions/events`. The session manager watches the two directories Claude Code writes (`${CLAUDE_CONFIG_DIR}/sessions/<pid>.json` for live state, `${CLAUDE_CONFIG_DIR}/projects/<slug>/<sid>.jsonl` for transcripts) and emits `row-created`, `row-updated`, `row-archived`, or `row-removed` deltas to every connected browser tab. On connect the manager replays the current row index so a freshly-opened tab catches up without polling, then streams deltas as files change on disk. Two open tabs see the same list within ~300ms of any spawn, status flip, or exit; no refresh button required for state to be current. The legacy `/list` fetch and `useAdminSessions` hook stay mounted to serve the ConversationsModal and the post-action reconcile path in `session-actions`, but the sidebar's visible rows come from the row store, not from `/list`. Each EventSource open emits `[admin-events] client-connected ip=<…> seeded-rows=<n>` server-side and `[admin-ui] session-row-store connected events-received=<n>` in the browser console; transport drops log `[admin-ui] session-row-store reconnect attempt=<n>` until the EventSource reattaches.
|
|
78
78
|
|
|
79
|
-
The trade-off is
|
|
79
|
+
The trade-off is a longer-lived connection per tab: the manager's per-process subscriber count rises with open tabs, and the SSE channel must survive proxy idle timeouts. The manager emits a 25-second keep-alive comment line on every connection (ignored by EventSource consumers, refreshes the proxy clock) and the browser-side store force-closes-and-reconnects on transport errors with exponential backoff capped at 30s.
|
|
80
80
|
|
|
81
|
-
**Spawn lifecycle:
|
|
81
|
+
**Spawn lifecycle: PID-file driven.** Clicking "+ New session" spawns the PTY and waits for Claude Code's PID file to appear at `${CLAUDE_CONFIG_DIR}/sessions/<pid>.json`. The PID file lands at process init (for `entrypoint: cli` spawns) and carries the intrinsic `sessionId`, `bridgeSessionId`, `agent`, and `status` directly. The manager's filesystem watcher reports the create event; the spawn response includes the canonical `sessionId` from that file. URL capture still runs in parallel to populate the operator-facing iframe URL, but it no longer gates readiness. The JSONL transcript is written on the first operator turn (true on 2.1.143 and 2.1.128); the watcher fires a separate event for that, and `/list`, `/meta`, `/log` resolve any of four ids — `sessionId`, `bridgeSessionId`, `bridgeSuffix`, or numeric `pid` — to the same row.
|
|
82
|
+
|
|
83
|
+
**Stop vs. delete.** `POST /<id>/stop` sends SIGTERM, leaves the JSONL on disk for audit, and is idempotent against an already-dead row. `DELETE /<id>` removes the JSONL + sidecar + per-session subdir and returns 409 if the PTY is still alive (stop first). Any unknown id returns 404; nothing returns a silent 204 against an id the manager does not know.
|
|
82
84
|
|
|
83
85
|
The metadata pane subscribes to the same /list projection. When an operator clicks End on an alive row, the DELETE returns 200 and the post-mutation refetch decides what happens next: a session that wrote a JSONL surfaces as a dehydrated `status: 'ended'` row (the pane swaps `End session` for `Purge JSONL` plus `Resume`), and a session that never wrote a JSONL (`Turns: 0`) leaves the list entirely (the pane shows a `Session ended without a transcript. Close this pane.` banner with a Close button and no destructive action). The manager's `/list` and `/meta` are the only authorities on post-End state; the client does not pre-empt either response with an optimistic mutation.
|
|
84
86
|
|
|
@@ -122,3 +124,15 @@ When your public agent sends a message to someone — via email, WhatsApp, Teleg
|
|
|
122
124
|
- **SMS:** a brief suffix is appended when the message is AI-composed
|
|
123
125
|
|
|
124
126
|
Messages you write yourself (e.g. typing directly in WhatsApp) are not marked — the disclosure applies only to content composed by the AI agent.
|
|
127
|
+
|
|
128
|
+
## Session Slot Safeguards
|
|
129
|
+
|
|
130
|
+
{{productName}} runs each chat — yours and every visitor's — as a separate `claude` process on your Pi. Three safeguards keep these processes from piling up:
|
|
131
|
+
|
|
132
|
+
- **Specialist cap.** Background specialists (`database-operator`, `content-producer`, etc.) are limited to three running at once. If you ask for a fourth while three are still working, the oldest idle one is shut down first. If all three are actively running, the request is rejected with `specialist-cap-reached`.
|
|
133
|
+
- **Operator reserve.** Two slots are always held back for *you* — your own chats and one-off tasks. Specialist work that would consume the last reserved slot is rejected with `operator-slots-reserved`. Your interactive chats are never blocked.
|
|
134
|
+
- **Idle reaper.** Every 30 seconds the platform looks for specialist processes that started, then went silent without producing any output. After two minutes of silence the platform shuts them down.
|
|
135
|
+
|
|
136
|
+
All three are tunable via env vars (`CLAUDE_SESSION_MANAGER_SPECIALIST_CAP`, `CLAUDE_SESSION_MANAGER_OPERATOR_RESERVE`, `CLAUDE_SESSION_MANAGER_TOTAL_PTY_CAP`, `CLAUDE_SESSION_MANAGER_RECORDER_IDLE_TTL_MS`); developer details in `.docs/platform.md` § "Claude Session Manager — PTY Slot Safeguards".
|
|
137
|
+
|
|
138
|
+
If you suspect background processes are piling up, run `grep '\[reaper\]' ~/.{brand}/logs/server.log | tail -50` — each tick logs how many rows it scanned and reaped.
|
|
@@ -141,6 +141,8 @@ initStderrTee("your-plugin-name");
|
|
|
141
141
|
|
|
142
142
|
After this, every `console.error("[your-tool]...")` from any tool in the plugin appears as `[<iso-ts>] [mcp:your-plugin-name] [your-tool]...` in the per-conversation stream log `claude-agent-stream-{conversationId}.log`, alongside the usual agent events. The raw per-server file `mcp-your-plugin-name-stderr-{date}.log` is still produced for deep-dive grep.
|
|
143
143
|
|
|
144
|
+
**Premium plugins.** Source lives at `premium-plugins/<bundle>/plugins/<name>/mcp/src/` — deeper than platform plugins, so the source-relative import to `platform/lib/mcp-stderr-tee/dist/index.js` uses more `../` segments. The bundler rewrites the compiled output to the canonical `../../../../lib/mcp-stderr-tee/dist/index.js` at staging time and ships `platform/lib/mcp-stderr-tee/{dist,package.json}` into `premium-plugins/<bundle>/lib/mcp-stderr-tee/` so the import resolves at deployed depth. The bundler fails loudly if `platform/lib/mcp-stderr-tee/package.json` is missing (it must pin `type` so install-location parent walks cannot mis-classify the dist file) or if any lib referenced by a rewritten import has no source dist.
|
|
145
|
+
|
|
144
146
|
**How the tee decides which file to write to:** the platform sets `STREAM_LOG_PATH` as an environment variable on every MCP server spawn, pointing to the conversation-scoped stream log. The MCP server does not know about conversations — it just trusts `STREAM_LOG_PATH`. Multiple concurrent conversations produce multiple concurrent MCP server processes, each teeing to its own file; no cross-conversation leakage.
|
|
145
147
|
|
|
146
148
|
**`STREAM_LOG_PATH` reaches every Claude Code child.** The platform now sets `STREAM_LOG_PATH` on the parent `claude` spawn env itself (not only on MCP server envs), so the bundled Bun runtime inherits it and every Bash-tool subprocess the CLI spawns sees it too. Opt-in shell scripts — currently `setup-tunnel.sh`, `reset-tunnel.sh`, and `list-cf-domains.sh` under `platform/plugins/cloudflare/scripts/` — read the variable, guard against a missing value with a loud exit, and tee subprocess output line-by-line into the same per-conversation file. Each spawn writes one `[spawn-env] STREAM_LOG_PATH=set pid=… conversationId=… site=…` line so the env-propagation is auditable per session. The chat UI tails the same file for lines matching `^\[([^\]]+)\] \[([a-z][a-z0-9-]*)((?::[a-z0-9:_-]+)?)\] ` — any lowercase scope shape participates on first write (earlier platform fixes generalised from the pre-592 enum `setup-tunnel|reset-tunnel`) — and emits them as `script_stream` SSE events; see `.docs/web-chat.md` for the contract. Inner-layer helpers that a.sh wrapper spawns (e.g. `list-cf-domains.ts` via `node --experimental-strip-types`) must write phase lines directly to `STREAM_LOG_PATH` rather than relying on stderr propagation; the build-gate `platform/ui/scripts/check-stream-log-contract.mjs` enforces this and is the definitive reference for the three allowed patterns (tee-wrapped, direct-write, or explicit stderr-only marker).
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Build-time gate: reject `require(` calls in TS/JS files that belong to a
|
|
3
|
+
// package whose package.json declares `"type": "module"`. Such calls compile
|
|
4
|
+
// fine under `tsc` (because `@types/node` declares `require` globally) and
|
|
5
|
+
// pass vitest (Vite shims `require` in its loader), but throw
|
|
6
|
+
// `ReferenceError: require is not defined` at runtime in native ESM.
|
|
7
|
+
//
|
|
8
|
+
// Live repro: Task 094 introduced `const { mkdirSync } = require('node:fs')`
|
|
9
|
+
// in claude-session-manager/src/fs-watcher.ts. tsc + vitest were green; the
|
|
10
|
+
// installed service crash-looped on boot at every admin request. See Task 099.
|
|
11
|
+
//
|
|
12
|
+
// Scope: walks every directory under REPO_ROOT containing a package.json with
|
|
13
|
+
// `"type": "module"`, and scans .ts/.tsx/.js (NOT .cjs / .mjs / node_modules /
|
|
14
|
+
// dist / payload / build) for `require(`. Comments are skipped. An explicit
|
|
15
|
+
// allowlist names files where `require` is intentional (e.g. vitest tests that
|
|
16
|
+
// load CJS-only modules via `vi.hoisted`).
|
|
17
|
+
|
|
18
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
|
19
|
+
import { dirname, join, relative, sep } from 'node:path'
|
|
20
|
+
import { fileURLToPath } from 'node:url'
|
|
21
|
+
|
|
22
|
+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
|
|
23
|
+
const REPO_ROOT = join(SCRIPT_DIR, '..', '..')
|
|
24
|
+
|
|
25
|
+
const SCANNABLE_EXTS = new Set(['.ts', '.tsx', '.js'])
|
|
26
|
+
const SKIP_DIR_NAMES = new Set([
|
|
27
|
+
'node_modules', 'dist', 'build', '.next', 'payload', '.git', 'coverage',
|
|
28
|
+
// Worktree checkouts under .claude/worktrees/** are siblings of the live tree
|
|
29
|
+
// and their own gate runs scan them; the top-level gate must not double-scan.
|
|
30
|
+
'worktrees',
|
|
31
|
+
])
|
|
32
|
+
|
|
33
|
+
// Files where `require()` is intentional. Each entry is a path relative to
|
|
34
|
+
// REPO_ROOT. Add with a one-line comment explaining why the call is required
|
|
35
|
+
// in an ESM package; if the reason no longer applies, delete the entry.
|
|
36
|
+
const ALLOWLIST = new Set([
|
|
37
|
+
// vitest hoists this fixture BEFORE the test module's own ESM imports are
|
|
38
|
+
// resolved; `vi.hoisted` runs in CJS context where `require` is the only
|
|
39
|
+
// available loader. The module under test is pure ESM.
|
|
40
|
+
'platform/ui/server/routes/admin/__tests__/files-delete-cascade.test.ts',
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
const REQUIRE_CALL_RE = /\brequire\s*\(/
|
|
44
|
+
|
|
45
|
+
/** Walk REPO_ROOT and collect every directory containing a package.json that
|
|
46
|
+
* declares `"type": "module"`. The walk skips SKIP_DIR_NAMES (node_modules,
|
|
47
|
+
* dist, etc.) to keep the cost bounded. */
|
|
48
|
+
function findEsmPackageRoots(root) {
|
|
49
|
+
const roots = []
|
|
50
|
+
function walk(dir) {
|
|
51
|
+
let entries
|
|
52
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
53
|
+
const hasPkg = entries.some((e) => e.isFile() && e.name === 'package.json')
|
|
54
|
+
if (hasPkg) {
|
|
55
|
+
const pkgPath = join(dir, 'package.json')
|
|
56
|
+
try {
|
|
57
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
58
|
+
if (pkg.type === 'module') roots.push(dir)
|
|
59
|
+
} catch {
|
|
60
|
+
// malformed package.json — not this gate's job
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
for (const e of entries) {
|
|
64
|
+
if (!e.isDirectory()) continue
|
|
65
|
+
if (SKIP_DIR_NAMES.has(e.name)) continue
|
|
66
|
+
walk(join(dir, e.name))
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
walk(root)
|
|
70
|
+
return roots
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function collectFiles(root) {
|
|
74
|
+
const out = []
|
|
75
|
+
function walk(dir) {
|
|
76
|
+
let entries
|
|
77
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
78
|
+
for (const e of entries) {
|
|
79
|
+
const full = join(dir, e.name)
|
|
80
|
+
if (e.isDirectory()) {
|
|
81
|
+
if (SKIP_DIR_NAMES.has(e.name)) continue
|
|
82
|
+
walk(full)
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
if (!e.isFile()) continue
|
|
86
|
+
const dot = e.name.lastIndexOf('.')
|
|
87
|
+
if (dot < 0) continue
|
|
88
|
+
const ext = e.name.slice(dot)
|
|
89
|
+
if (!SCANNABLE_EXTS.has(ext)) continue
|
|
90
|
+
out.push(full)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
walk(root)
|
|
94
|
+
return out
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isCommentLine(line) {
|
|
98
|
+
const t = line.trimStart()
|
|
99
|
+
return t.startsWith('//') || t.startsWith('*') || t.startsWith('/*')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const esmRoots = findEsmPackageRoots(REPO_ROOT)
|
|
103
|
+
const violations = []
|
|
104
|
+
|
|
105
|
+
for (const root of esmRoots) {
|
|
106
|
+
for (const file of collectFiles(root)) {
|
|
107
|
+
const rel = relative(REPO_ROOT, file).split(sep).join('/')
|
|
108
|
+
if (ALLOWLIST.has(rel)) continue
|
|
109
|
+
const lines = readFileSync(file, 'utf-8').split('\n')
|
|
110
|
+
for (let i = 0; i < lines.length; i++) {
|
|
111
|
+
const line = lines[i]
|
|
112
|
+
if (!REQUIRE_CALL_RE.test(line)) continue
|
|
113
|
+
if (isCommentLine(line)) continue
|
|
114
|
+
violations.push({ file: rel, lineNumber: i + 1, line: line.trim() })
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (violations.length > 0) {
|
|
120
|
+
console.error(`[check-no-esm-require] ${violations.length} require() call(s) in ESM package(s):`)
|
|
121
|
+
for (const v of violations) {
|
|
122
|
+
console.error(` ${v.file}:${v.lineNumber} ${v.line}`)
|
|
123
|
+
}
|
|
124
|
+
console.error('')
|
|
125
|
+
console.error('require() throws ReferenceError at runtime in native ESM.')
|
|
126
|
+
console.error('Replace with a top-of-file `import` statement, or add an explicit')
|
|
127
|
+
console.error('ALLOWLIST entry in platform/scripts/check-no-esm-require.mjs with a')
|
|
128
|
+
console.error('one-line comment explaining why the call is required.')
|
|
129
|
+
process.exit(1)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log(`[check-no-esm-require] scanned ${esmRoots.length} ESM package(s); 0 violations`)
|