@rubytech/create-maxy-code 0.1.35 → 0.1.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +12 -0
- package/payload/platform/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +109 -0
- package/payload/platform/plugins/admin/hooks/turn-completed-graph-write.sh +114 -0
- package/payload/platform/plugins/admin/skills/unzip-attachment/SKILL.md +1 -1
- package/payload/platform/plugins/contacts/PLUGIN.md +11 -0
- package/payload/platform/plugins/docs/references/internals.md +10 -0
- package/payload/platform/plugins/docs/references/platform.md +11 -4
- package/payload/platform/plugins/email/PLUGIN.md +11 -0
- package/payload/platform/plugins/linkedin-import/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/linkedin-import/PLUGIN.md +5 -5
- package/payload/platform/plugins/linkedin-import/skills/linkedin-import/SKILL.md +3 -1
- package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/connections.md +2 -2
- package/payload/platform/plugins/memory/PLUGIN.md +11 -0
- package/payload/platform/plugins/memory/skills/conversation-archive/SKILL.md +2 -0
- package/payload/platform/plugins/memory/skills/conversation-archive-enrich/SKILL.md +2 -0
- package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +6 -4
- package/payload/platform/plugins/outlook/PLUGIN.md +11 -0
- package/payload/platform/plugins/replicate/PLUGIN.md +11 -0
- package/payload/platform/plugins/scheduling/PLUGIN.md +11 -0
- package/payload/platform/plugins/tasks/PLUGIN.md +11 -0
- package/payload/platform/plugins/telegram/PLUGIN.md +11 -0
- package/payload/platform/plugins/waitlist/PLUGIN.md +11 -0
- package/payload/platform/plugins/whatsapp/PLUGIN.md +11 -0
- package/payload/platform/plugins/workflows/PLUGIN.md +11 -0
- package/payload/platform/scripts/seed-neo4j.sh +14 -0
- package/payload/platform/scripts/verify-skill-tool-surface.sh +5 -3
- package/payload/platform/services/claude-session-manager/dist/config.d.ts +0 -5
- package/payload/platform/services/claude-session-manager/dist/config.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/config.js +16 -6
- package/payload/platform/services/claude-session-manager/dist/config.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +0 -5
- 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 +109 -139
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +63 -4
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +47 -61
- 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 +191 -131
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-store.d.ts +43 -37
- 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 +72 -61
- package/payload/platform/services/claude-session-manager/dist/session-store.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/tool-surface.d.ts +34 -4
- package/payload/platform/services/claude-session-manager/dist/tool-surface.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/tool-surface.js +125 -27
- package/payload/platform/services/claude-session-manager/dist/tool-surface.js.map +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +9 -18
- package/payload/platform/templates/specialists/agents/database-operator.md +27 -70
- package/payload/platform/templates/specialists/agents/librarian.md +55 -0
- package/payload/server/public/assets/{Checkbox-8mOR8-gV.js → Checkbox-DAki3vDW.js} +1 -1
- package/payload/server/public/assets/{admin-CK4V-B9A.js → admin-CyAZtsYZ.js} +1 -1
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-2DgPtOot.js → architectureDiagram-Q4EWVU46-C-hla3Ip.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-BilycXVZ.js → blockDiagram-DXYQGD6D-CbzjtNs3.js} +1 -1
- package/payload/server/public/assets/{brand-1_DraUIv.css → brand-Cun0PBrA.css} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-CGiIFO5U.js → c4Diagram-AHTNJAMY-B87UblA0.js} +1 -1
- package/payload/server/public/assets/channel-CTNo72W7.js +1 -0
- package/payload/server/public/assets/{chunk-336JU56O-DxTg1h50.js → chunk-336JU56O-2a2pVlSg.js} +2 -2
- package/payload/server/public/assets/{chunk-426QAEUC-DkGPjs0P.js → chunk-426QAEUC-j9NOD8_a.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-D2JaxD6r.js → chunk-4TB4RGXK-Cs872xAq.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-Do3pbAGo.js → chunk-5FUZZQ4R-BnDFbZ_I.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-np5moZSl.js → chunk-5PVQY5BW-Bu5B3Hyf.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-BUFLWwRP.js → chunk-EDXVE4YY-B96Fw4FF.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-BE7Gt41s.js → chunk-ENJZ2VHE-CcGrGQim.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-gOBkROMP.js → chunk-ICPOFSXX-ZRvcI4Ai.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-DR-BkNZa.js → chunk-OYMX7WX6-B6RI9Fbf.js} +1 -1
- package/payload/server/public/assets/{chunk-U2HBQHQK-qLp-uSVE.js → chunk-U2HBQHQK-BYGHVYyo.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-DQiKti0J.js → chunk-X2U36JSP-rwNs-gTO.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-Cd0JhpTo.js → chunk-YZCP3GAM-jPttJRap.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-8u01H3Hy.js → chunk-ZZ45TVLE-DLykWOT5.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-BaV5nDmT.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DOKlCKjY.js +1 -0
- package/payload/server/public/assets/clone-sQSwlSln.js +1 -0
- package/payload/server/public/assets/{dagre-KV5264BT-BBF_zxbi.js → dagre-KV5264BT-C6wSzHX2.js} +1 -1
- package/payload/server/public/assets/{dagre-BfVsOYOv.js → dagre-jnQDnF-5.js} +1 -1
- package/payload/server/public/assets/data-DbkuXcPb.js +1 -0
- package/payload/server/public/assets/{device-url-actions-CJBD3ecO.js → device-url-actions-y1tXNDZx.js} +1 -1
- package/payload/server/public/assets/{diagram-5BDNPKRD-BFHk2a7t.js → diagram-5BDNPKRD-N3X-n90h.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-Be3_qYl2.js → diagram-G4DWMVQ6-vfmbzdPd.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-B3r4XVcu.js → diagram-MMDJMWI5-BETmPx_w.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-BW5EIT4D.js → diagram-TYMM5635-BSUGM4wG.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-DTu4_1Kb.js → erDiagram-SMLLAGMA-CWZ9OwMb.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-CadrjLcA.js → flowDiagram-DWJPFMVM-BpQOQS1a.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-Cn0npD0L.js → ganttDiagram-T4ZO3ILL-Dc59igCS.js} +1 -1
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-CK3gG5Qm.js → gitGraphDiagram-UUTBAWPF-D6lXg3zl.js} +1 -1
- package/payload/server/public/assets/graph-CC5_6-dg.js +1 -0
- package/payload/server/public/assets/{graph-labels-CPzF6Gbk.js → graph-labels-DFyNH0jb.js} +1 -1
- package/payload/server/public/assets/{graphlib-DFIOO59N.js → graphlib-yG7kH8Fr.js} +1 -1
- package/payload/server/public/assets/{infoDiagram-42DDH7IO-BohUsXnN.js → infoDiagram-42DDH7IO-TS9Pv8Os.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-tjmA4BU8.js → ishikawaDiagram-UXIWVN3A-BJiJJTq6.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-DzBlFESM.js → journeyDiagram-VCZTEJTY-C7PW4fmw.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-BTw9BxK8.js → kanban-definition-6JOO6SKY-BEk21ciD.js} +1 -1
- package/payload/server/public/assets/{line-BYPL6wAj.js → line-DusyVWDE.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-Dmu4fWCS.js → mermaid-parser.core-DeeojQSb.js} +1 -1
- package/payload/server/public/assets/{mermaid.core-Z9vvieC2.js → mermaid.core-p65Lo7Sj.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-BccAnfJ4.js → mindmap-definition-QFDTVHPH-B0jxFxdS.js} +1 -1
- package/payload/server/public/assets/{page-CBTlFm3b.js → page-BeYGnDzI.js} +1 -1
- package/payload/server/public/assets/{page-CDrsNrDO.js → page-x5khmfRx.js} +1 -1
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-CZwxH1nJ.js → pieDiagram-DEJITSTG-DJ5GKYt6.js} +1 -1
- package/payload/server/public/assets/{public-MKWxjwiF.js → public-cnoUs7vq.js} +3 -3
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-BWT4DItX.js → quadrantDiagram-34T5L4WZ-BV7R-kwV.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-CHYkWUEy.js → requirementDiagram-MS252O5E-DRiLzlGN.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-km7p45Ng.js → sankeyDiagram-XADWPNL6-C5-6_cCI.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-BT1C0Dw-.js → sequenceDiagram-FGHM5R23-5FXm_VYM.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-hibC19DY.js → stateDiagram-FHFEXIEX-FCQTaGqV.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-BdpPhdqg.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-Cnm86NfE.js → timeline-definition-GMOUNBTQ-tvtq62ef.js} +1 -1
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-ByjQPLzS.js → vennDiagram-DHZGUBPP-Dk-k7CE4.js} +1 -1
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-CMVrWw7l.js → wardleyDiagram-NUSXRM2D-CZSrqniY.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-Mpc1u3FV.js → xychartDiagram-5P7HB3ND-CpGLpk7Q.js} +1 -1
- package/payload/server/public/data.html +5 -5
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +8 -8
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +128 -106
- package/payload/server/public/assets/channel-DYpIZW3Q.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-nkfy9gg7.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-B-pMmeCC.js +0 -1
- package/payload/server/public/assets/clone-jiTzIER6.js +0 -1
- package/payload/server/public/assets/data-BIOaDoqe.js +0 -1
- package/payload/server/public/assets/graph-ClfU7t1k.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-MuJZxjmU.js +0 -1
- /package/payload/server/public/assets/{brand-CENN2zdq.js → brand-lj7EhWRg.js} +0 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// here via fetch.
|
|
4
4
|
//
|
|
5
5
|
// Routes:
|
|
6
|
-
// POST /spawn { senderId, role, channel } -> Session
|
|
6
|
+
// POST /spawn { senderId, role, channel, tunnelUrl? } -> Session
|
|
7
7
|
// GET /list?senderId=... -> Session[]
|
|
8
8
|
// POST /resume { senderId, role, channel, claudeSessionId } -> Session
|
|
9
9
|
// DELETE /:sessionId -> 204
|
|
@@ -14,15 +14,22 @@
|
|
|
14
14
|
// GET /:sessionId/meta -> SessionMeta payload
|
|
15
15
|
// PATCH /:sessionId { displayName } -> 204 (writes sidecar)
|
|
16
16
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// the
|
|
17
|
+
// `/spawn` returns 200 the instant Claude Code emits its `/remote-control`
|
|
18
|
+
// URL — the operator gets a usable session inside ~2 s even when they never
|
|
19
|
+
// type. The wire shape is `{sessionId: <bridge-suffix>, claudeSessionId: null,
|
|
20
|
+
// jsonlPath: null, ...LivePayload}`. The jsonl-observer fires on the first
|
|
21
|
+
// operator turn (claude does not write the JSONL until then, verified Pi
|
|
22
|
+
// 2.1.143 and Mac 2.1.128) and promotes the row to the intrinsic
|
|
23
|
+
// claudeSessionId in the background — no second client call needed.
|
|
22
24
|
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
25
|
+
// Every GET route resolves either the bridge suffix or the intrinsic id to
|
|
26
|
+
// the same row, so a UI that mounted on the bridge suffix at t=0 keeps
|
|
27
|
+
// routing correctly after promotion without flipping the row's visible
|
|
28
|
+
// position. The intrinsic id is the right field to use for routing once
|
|
29
|
+
// known (UI reads `claudeSessionId ?? sessionId`).
|
|
30
|
+
//
|
|
31
|
+
// sidecar `<id>.meta.json` next to the JSONL holds operator-owned fields
|
|
32
|
+
// (displayName). PATCH writes it; /meta reads it via the enumerator helpers.
|
|
26
33
|
import { Hono } from 'hono';
|
|
27
34
|
import { stream } from 'hono/streaming';
|
|
28
35
|
import { existsSync, statSync, createReadStream, watchFile, unwatchFile, rmSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
|
|
@@ -32,10 +39,6 @@ import { claudeStateRoot, projectSlugForCwd } from './jsonl-path.js';
|
|
|
32
39
|
import { basename, join } from 'node:path';
|
|
33
40
|
const ROLES = ['admin', 'public'];
|
|
34
41
|
const CHANNELS = ['browser', 'whatsapp', 'telegram', 'webchat', 'email'];
|
|
35
|
-
/** Parse the optional `aboutOwner` field from a /spawn or /resume POST body.
|
|
36
|
-
* Accepts either `{ ok:true, body:string }` or `{ ok:false, reason:string }`.
|
|
37
|
-
* Returns undefined for missing / malformed shapes so a stale UI never blocks
|
|
38
|
-
* the spawn — the composer simply omits the `<about-owner>` sentinel. */
|
|
39
42
|
function parseAboutOwner(raw) {
|
|
40
43
|
if (!raw || typeof raw !== 'object')
|
|
41
44
|
return undefined;
|
|
@@ -48,9 +51,6 @@ function parseAboutOwner(raw) {
|
|
|
48
51
|
}
|
|
49
52
|
return undefined;
|
|
50
53
|
}
|
|
51
|
-
/** Parse the optional `dormantPlugins` array. Each entry must be
|
|
52
|
-
* `{ name:string, description:string }`. Malformed entries are dropped
|
|
53
|
-
* silently — the composer suppresses the sentinel if the array ends up empty. */
|
|
54
54
|
function parseDormantPlugins(raw) {
|
|
55
55
|
if (!Array.isArray(raw))
|
|
56
56
|
return undefined;
|
|
@@ -67,40 +67,36 @@ function parseDormantPlugins(raw) {
|
|
|
67
67
|
}
|
|
68
68
|
return out.length > 0 ? out : undefined;
|
|
69
69
|
}
|
|
70
|
-
// Path-traversal guard.
|
|
71
|
-
//
|
|
72
|
-
// Claude Code wrote as the JSONL basename (`session_<id>` or a bare UUID,
|
|
73
|
-
// per claude's writer). Reject anything with `/`, `..`, or out-of-range
|
|
74
|
-
// chars before touching the filesystem.
|
|
70
|
+
// Path-traversal guard. Bridge suffixes and intrinsic JSONL basenames both
|
|
71
|
+
// match this character class.
|
|
75
72
|
const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
76
73
|
const LIST_TAIL_BYTES = 64 * 1024;
|
|
77
74
|
const DISPLAY_NAME_MAX = 256;
|
|
78
75
|
function timed(logger, method, path, status, ms) {
|
|
79
76
|
logger(`api method=${method} path=${path} outcome=${status} ms=${ms}`);
|
|
80
77
|
}
|
|
81
|
-
/** Locate the JSONL path for any sessionId
|
|
82
|
-
*
|
|
83
|
-
* is one map probe plus a filesystem fallback for ended rows. */
|
|
78
|
+
/** Locate the JSONL path for any sessionId. For alive rows, returns null
|
|
79
|
+
* when the row has not yet promoted (no JSONL on disk yet). */
|
|
84
80
|
function resolveJsonlPath(deps, sessionId) {
|
|
85
81
|
const live = deps.store.get(sessionId);
|
|
86
|
-
if (live) {
|
|
82
|
+
if (live && live.claudeSessionId !== null && live.jsonlPath !== null) {
|
|
87
83
|
return { path: live.jsonlPath, claudeSessionId: live.claudeSessionId };
|
|
88
84
|
}
|
|
85
|
+
if (live && live.claudeSessionId === null) {
|
|
86
|
+
// Row is alive but pre-promotion: no JSONL exists yet.
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
89
|
const slug = projectSlugForCwd(deps.spawnCwd);
|
|
90
90
|
const path = join(claudeStateRoot(), 'projects', slug, `${sessionId}.jsonl`);
|
|
91
91
|
if (existsSync(path))
|
|
92
92
|
return { path, claudeSessionId: sessionId };
|
|
93
93
|
return null;
|
|
94
94
|
}
|
|
95
|
-
/** attach the audit watcher for a public session's JSONL.
|
|
96
|
-
* the attach is gated on `onJsonlObserved`, which fires only
|
|
97
|
-
* after the observer has bound the JSONL, so jsonlPath and claudeSessionId
|
|
98
|
-
* are always set by the time this runs. Race-safe: if the PTY exited
|
|
99
|
-
* between observer fire and this call, detach immediately so the watcher
|
|
100
|
-
* does not leak. */
|
|
101
95
|
function attachPublicAudit(deps, session) {
|
|
102
96
|
if (session.role !== 'public')
|
|
103
97
|
return;
|
|
98
|
+
if (session.claudeSessionId === null || session.jsonlPath === null)
|
|
99
|
+
return;
|
|
104
100
|
const claudeSessionId = session.claudeSessionId;
|
|
105
101
|
deps.publicToolAudit.attach(claudeSessionId, session.jsonlPath);
|
|
106
102
|
if (session.exited) {
|
|
@@ -113,27 +109,24 @@ function attachPublicAudit(deps, session) {
|
|
|
113
109
|
}
|
|
114
110
|
export function buildHttpApp(deps) {
|
|
115
111
|
const app = new Hono();
|
|
116
|
-
// display-name resolution for alive rows shares the same
|
|
117
|
-
// three-tier chain as ended rows: sidecar → ai-title → 8-char slice.
|
|
118
|
-
// The ai-title tier comes from the JSONL tail; we read it via the
|
|
119
|
-
// enumerator's cached parse so /list polls remain cheap once warm.
|
|
120
|
-
// every live row in the store carries the intrinsic
|
|
121
|
-
// `claudeSessionId` (the spawn promise blocks on observation, so a row
|
|
122
|
-
// never appears in the store without it).
|
|
123
112
|
function liveDisplayName(s, sidecarName) {
|
|
124
113
|
if (sidecarName && sidecarName.trim())
|
|
125
114
|
return sidecarName.trim();
|
|
126
|
-
if (existsSync(s.jsonlPath)) {
|
|
115
|
+
if (s.jsonlPath && s.claudeSessionId && existsSync(s.jsonlPath)) {
|
|
127
116
|
const enriched = readJsonlSession(deps.spawnCwd, s.claudeSessionId, LIST_TAIL_BYTES, deps.logger);
|
|
128
117
|
if (enriched?.aiTitle?.trim())
|
|
129
118
|
return enriched.aiTitle.trim();
|
|
130
119
|
}
|
|
131
|
-
|
|
120
|
+
// pre-promotion → 8 chars of the bridge suffix; post-promotion → 8
|
|
121
|
+
// chars of the intrinsic id (which the operator may copy into resume).
|
|
122
|
+
return (s.claudeSessionId ?? s.bridgeSuffix).slice(0, 8);
|
|
132
123
|
}
|
|
133
124
|
function toLivePayload(s) {
|
|
134
|
-
const sidecar = readSidecar(s.jsonlPath);
|
|
125
|
+
const sidecar = s.jsonlPath ? readSidecar(s.jsonlPath) : null;
|
|
126
|
+
const hasJsonl = s.jsonlPath !== null && existsSync(s.jsonlPath);
|
|
135
127
|
return {
|
|
136
|
-
sessionId: s.
|
|
128
|
+
sessionId: s.bridgeSuffix,
|
|
129
|
+
claudeSessionId: s.claudeSessionId,
|
|
137
130
|
pid: s.pid,
|
|
138
131
|
url: s.url,
|
|
139
132
|
startedAt: s.startedAt,
|
|
@@ -143,12 +136,12 @@ export function buildHttpApp(deps) {
|
|
|
143
136
|
jsonlPath: s.jsonlPath,
|
|
144
137
|
status: 'alive',
|
|
145
138
|
displayName: liveDisplayName(s, sidecar?.displayName),
|
|
146
|
-
// Pane-grade fields for alive rows are filled lazily on /meta to keep
|
|
147
|
-
// /list polls cheap; /list payload carries null/0 placeholders.
|
|
148
139
|
lastMessageAt: null,
|
|
149
140
|
turnCount: 0,
|
|
150
141
|
capped: false,
|
|
151
|
-
sizeBytes:
|
|
142
|
+
sizeBytes: hasJsonl && s.jsonlPath ? statSync(s.jsonlPath).size : null,
|
|
143
|
+
hidden: s.hidden,
|
|
144
|
+
specialist: s.specialist,
|
|
152
145
|
};
|
|
153
146
|
}
|
|
154
147
|
app.post('/spawn', async (c) => {
|
|
@@ -165,21 +158,20 @@ export function buildHttpApp(deps) {
|
|
|
165
158
|
const senderId = typeof body.senderId === 'string' ? body.senderId : '';
|
|
166
159
|
const role = typeof body.role === 'string' ? body.role : '';
|
|
167
160
|
const channel = typeof body.channel === 'string' ? body.channel : '';
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
161
|
+
const hidden = body.hidden === true;
|
|
162
|
+
const specialist = typeof body.specialist === 'string' && /^[A-Za-z0-9_-]{1,64}$/.test(body.specialist)
|
|
163
|
+
? body.specialist
|
|
164
|
+
: undefined;
|
|
165
|
+
const stdinPayload = typeof body.stdinPayload === 'string' ? body.stdinPayload : undefined;
|
|
171
166
|
const aboutOwnerShape = body.aboutOwner == null ? 'absent' : (typeof body.aboutOwner === 'object' ? 'object' : typeof body.aboutOwner);
|
|
172
167
|
const dormantPluginsShape = Array.isArray(body.dormantPlugins)
|
|
173
168
|
? `array(${body.dormantPlugins.length})`
|
|
174
169
|
: body.dormantPlugins == null ? 'absent' : typeof body.dormantPlugins;
|
|
175
|
-
deps.logger(`spawn-request senderId=${senderId || 'missing'} role=${role || 'missing'} channel=${channel || 'missing'} permissionMode=${typeof body.permissionMode === 'string' ? body.permissionMode : 'default'} channels=${Array.isArray(body.channels) ? body.channels.length : 0} attachmentDir=${typeof body.attachmentDir === 'string' ? 'set' : 'absent'} aboutOwner=${aboutOwnerShape} dormantPlugins=${dormantPluginsShape}`);
|
|
170
|
+
deps.logger(`spawn-request senderId=${senderId || 'missing'} role=${role || 'missing'} channel=${channel || 'missing'} permissionMode=${typeof body.permissionMode === 'string' ? body.permissionMode : 'default'} channels=${Array.isArray(body.channels) ? body.channels.length : 0} attachmentDir=${typeof body.attachmentDir === 'string' ? 'set' : 'absent'} aboutOwner=${aboutOwnerShape} dormantPlugins=${dormantPluginsShape} tunnelUrl=${typeof body.tunnelUrl === 'string' ? 'set' : (body.tunnelUrl === null ? 'null' : 'absent')}`);
|
|
176
171
|
if (!senderId || !ROLES.includes(role) || !CHANNELS.includes(channel)) {
|
|
177
172
|
timed(deps.logger, 'POST', '/spawn', 400, Date.now() - start);
|
|
178
173
|
return c.json({ error: 'invalid-arguments', detail: { senderId: !!senderId, role, channel } }, 400);
|
|
179
174
|
}
|
|
180
|
-
// gate `role=public` spawns on the in-memory token buckets.
|
|
181
|
-
// Admin spawns are unthrottled. Rejection carries the bucket that tripped
|
|
182
|
-
// so the caller can distinguish channel-level abuse from cross-channel.
|
|
183
175
|
if (role === 'public') {
|
|
184
176
|
const decision = deps.publicSpawnLimiter.admit(senderId, channel);
|
|
185
177
|
if (!decision.admitted) {
|
|
@@ -191,29 +183,27 @@ export function buildHttpApp(deps) {
|
|
|
191
183
|
}, 429);
|
|
192
184
|
}
|
|
193
185
|
}
|
|
194
|
-
// `channels` is an optional string[]; non-string entries are
|
|
195
|
-
// dropped silently. Empty / absent leaves the spawn argv unchanged.
|
|
196
186
|
const channels = Array.isArray(body.channels)
|
|
197
187
|
? body.channels.filter((c) => typeof c === 'string' && c.length > 0)
|
|
198
188
|
: undefined;
|
|
199
|
-
// optional permission-mode passthrough. Unknown values are
|
|
200
|
-
// dropped silently (spawn proceeds in `default` Ask-permissions mode)
|
|
201
|
-
// so a stale UI never blocks a spawn over a typo'd mode string.
|
|
202
189
|
const permissionMode = typeof body.permissionMode === 'string' && PERMISSION_MODES.includes(body.permissionMode)
|
|
203
190
|
? body.permissionMode
|
|
204
191
|
: undefined;
|
|
205
|
-
// optional public-chat attachment dir. Absolute path required;
|
|
206
|
-
// non-absolute or non-string values are dropped silently so a malformed
|
|
207
|
-
// bridge payload never blocks a spawn.
|
|
208
192
|
const attachmentDir = typeof body.attachmentDir === 'string' && body.attachmentDir.startsWith('/')
|
|
209
193
|
? body.attachmentDir
|
|
210
194
|
: undefined;
|
|
211
|
-
// Pre-resolved owner profile + dormant-plugins set, computed UI-side by
|
|
212
|
-
// the caller that holds Neo4j credentials. Missing / malformed shapes
|
|
213
|
-
// suppress the corresponding sentinel rather than blocking the spawn.
|
|
214
195
|
const aboutOwner = parseAboutOwner(body.aboutOwner);
|
|
215
196
|
const dormantPlugins = parseDormantPlugins(body.dormantPlugins);
|
|
216
|
-
|
|
197
|
+
// tunnelUrl override resolved upstream by the UI process from the
|
|
198
|
+
// brand's tunnel.state on disk (the manager has no Neo4j credentials).
|
|
199
|
+
// string → render; null → render `tunnel-url: none`; undefined → fall
|
|
200
|
+
// back to deps.host.tunnelUrl (whatever boot resolved).
|
|
201
|
+
const tunnelUrlOverride = typeof body.tunnelUrl === 'string'
|
|
202
|
+
? body.tunnelUrl
|
|
203
|
+
: body.tunnelUrl === null
|
|
204
|
+
? null
|
|
205
|
+
: undefined;
|
|
206
|
+
const result = await spawnClaudeSession({ ...deps, store: deps.store, onJsonlObserved: (s) => attachPublicAudit(deps, s), tunnelUrlOverride }, {
|
|
217
207
|
senderId,
|
|
218
208
|
role: role,
|
|
219
209
|
channel: channel,
|
|
@@ -222,46 +212,45 @@ export function buildHttpApp(deps) {
|
|
|
222
212
|
attachmentDir,
|
|
223
213
|
aboutOwner,
|
|
224
214
|
dormantPlugins,
|
|
215
|
+
hidden,
|
|
216
|
+
specialist,
|
|
217
|
+
stdinPayload,
|
|
225
218
|
});
|
|
226
219
|
if (!result.ok) {
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
timed(deps.logger, 'POST', '/spawn', status, Date.now() - start);
|
|
235
|
-
return c.json({ error: 'spawn-failed', reason: result.reason, stderrTail: result.stderrTail }, status);
|
|
236
|
-
}
|
|
237
|
-
// public-tool audit attaches inside the
|
|
238
|
-
// `onJsonlObserved` callback once the observer has matched Claude
|
|
239
|
-
// Code's JSONL on disk. — by the time the spawn promise
|
|
240
|
-
// resolves, that callback has fired and the audit is attached.
|
|
220
|
+
// Every remaining spawn-failure surface is 500: which-claude-not-found,
|
|
221
|
+
// pty-spawn-failed, url-capture-timeout, bridge-suffix-unparseable,
|
|
222
|
+
// host-context-unresolved, identity-unresolved. The observe-timeout 504
|
|
223
|
+
// is gone — /spawn never blocks on observation.
|
|
224
|
+
timed(deps.logger, 'POST', '/spawn', 500, Date.now() - start);
|
|
225
|
+
return c.json({ error: 'spawn-failed', reason: result.reason, stderrTail: result.stderrTail }, 500);
|
|
226
|
+
}
|
|
241
227
|
timed(deps.logger, 'POST', '/spawn', 200, Date.now() - start);
|
|
242
228
|
return c.json(toLivePayload(result.session), 200);
|
|
243
229
|
});
|
|
244
230
|
app.get('/list', (c) => {
|
|
245
231
|
const start = Date.now();
|
|
246
232
|
const senderId = c.req.query('senderId') ?? '';
|
|
233
|
+
const includeHidden = c.req.query('includeHidden') === '1';
|
|
247
234
|
if (!senderId) {
|
|
248
235
|
timed(deps.logger, 'GET', '/list', 400, Date.now() - start);
|
|
249
236
|
return c.json({ error: 'senderId-required' }, 400);
|
|
250
237
|
}
|
|
251
|
-
|
|
252
|
-
// (Claude Code's intrinsic claudeSessionId), so dedup is a simple
|
|
253
|
-
// set-membership check. Live entries take precedence on a collision
|
|
254
|
-
// (the PTY is still attached; the dehydrated row would be a stale
|
|
255
|
-
// snapshot of the same JSONL).
|
|
256
|
-
const liveStored = deps.store.listBySender(senderId);
|
|
238
|
+
const liveStored = deps.store.listBySender(senderId, { includeHidden });
|
|
257
239
|
const live = liveStored.map(toLivePayload);
|
|
258
|
-
|
|
240
|
+
// Live rows dedup against dehydrated by intrinsic id when promoted;
|
|
241
|
+
// un-promoted rows have no intrinsic id and cannot collide with disk.
|
|
242
|
+
const liveIntrinsicIds = new Set();
|
|
243
|
+
for (const s of liveStored) {
|
|
244
|
+
if (s.claudeSessionId !== null)
|
|
245
|
+
liveIntrinsicIds.add(s.claudeSessionId);
|
|
246
|
+
}
|
|
259
247
|
const dehydrated = [];
|
|
260
248
|
for (const j of listJsonlSessions(deps.spawnCwd, LIST_TAIL_BYTES, deps.logger)) {
|
|
261
|
-
if (
|
|
249
|
+
if (liveIntrinsicIds.has(j.claudeSessionId))
|
|
262
250
|
continue;
|
|
263
251
|
dehydrated.push({
|
|
264
252
|
sessionId: j.claudeSessionId,
|
|
253
|
+
claudeSessionId: j.claudeSessionId,
|
|
265
254
|
pid: null,
|
|
266
255
|
url: null,
|
|
267
256
|
startedAt: j.startedAt,
|
|
@@ -275,6 +264,8 @@ export function buildHttpApp(deps) {
|
|
|
275
264
|
turnCount: j.turnCount,
|
|
276
265
|
capped: j.capped,
|
|
277
266
|
sizeBytes: j.sizeBytes,
|
|
267
|
+
hidden: false,
|
|
268
|
+
specialist: null,
|
|
278
269
|
});
|
|
279
270
|
}
|
|
280
271
|
deps.logger(`list-source live=${live.length} jsonl=${dehydrated.length}`);
|
|
@@ -301,15 +292,17 @@ export function buildHttpApp(deps) {
|
|
|
301
292
|
timed(deps.logger, 'POST', '/resume', 400, Date.now() - start);
|
|
302
293
|
return c.json({ error: 'invalid-arguments' }, 400);
|
|
303
294
|
}
|
|
304
|
-
// same `channels` passthrough as /spawn so resumed sessions
|
|
305
|
-
// re-attach to the brand's channel plugins. Absent/empty preserves
|
|
306
|
-
// today's argv shape.
|
|
307
295
|
const channels = Array.isArray(body.channels)
|
|
308
296
|
? body.channels.filter((c) => typeof c === 'string' && c.length > 0)
|
|
309
297
|
: undefined;
|
|
310
298
|
const aboutOwner = parseAboutOwner(body.aboutOwner);
|
|
311
299
|
const dormantPlugins = parseDormantPlugins(body.dormantPlugins);
|
|
312
|
-
const
|
|
300
|
+
const tunnelUrlOverride = typeof body.tunnelUrl === 'string'
|
|
301
|
+
? body.tunnelUrl
|
|
302
|
+
: body.tunnelUrl === null
|
|
303
|
+
? null
|
|
304
|
+
: undefined;
|
|
305
|
+
const result = await spawnClaudeSession({ ...deps, store: deps.store, onJsonlObserved: (s) => attachPublicAudit(deps, s), tunnelUrlOverride }, {
|
|
313
306
|
senderId,
|
|
314
307
|
role: role,
|
|
315
308
|
channel: channel,
|
|
@@ -320,18 +313,13 @@ export function buildHttpApp(deps) {
|
|
|
320
313
|
});
|
|
321
314
|
if (!result.ok) {
|
|
322
315
|
deps.logger(`resume-failed reason=${result.reason} resumeFrom=${claudeSessionId}`);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
return c.json({ error: 'resume-failed', reason: result.reason }, status);
|
|
316
|
+
timed(deps.logger, 'POST', '/resume', 500, Date.now() - start);
|
|
317
|
+
return c.json({ error: 'resume-failed', reason: result.reason }, 500);
|
|
326
318
|
}
|
|
327
319
|
deps.logger(`resume pid=${result.session.pid} sessionUrl=${result.session.url} resumeFrom=${claudeSessionId} latency-ms=${Date.now() - start}`);
|
|
328
|
-
// audit watcher attaches via onJsonlObserved.
|
|
329
320
|
timed(deps.logger, 'POST', '/resume', 200, Date.now() - start);
|
|
330
321
|
return c.json(toLivePayload(result.session), 200);
|
|
331
322
|
});
|
|
332
|
-
// sessionId metadata for the pane. Merges live store, dehydrated
|
|
333
|
-
// enumeration, and sidecar into one payload. Alive rows get a fresh tail
|
|
334
|
-
// parse with the full jsonlReadMaxBytes; ended rows likewise (cached).
|
|
335
323
|
app.get('/:sessionId/meta', (c) => {
|
|
336
324
|
const start = Date.now();
|
|
337
325
|
const sessionId = c.req.param('sessionId');
|
|
@@ -343,15 +331,12 @@ export function buildHttpApp(deps) {
|
|
|
343
331
|
let payload;
|
|
344
332
|
if (live) {
|
|
345
333
|
const base = toLivePayload(live);
|
|
346
|
-
// claudeSessionId is the row's only id; the manager-mint
|
|
347
|
-
// is gone.
|
|
348
|
-
const claudeSessionId = live.claudeSessionId;
|
|
349
334
|
let lastMessageAt = null;
|
|
350
335
|
let turnCount = 0;
|
|
351
336
|
let capped = false;
|
|
352
337
|
let sizeBytes = base.sizeBytes;
|
|
353
|
-
if (existsSync(live.jsonlPath)) {
|
|
354
|
-
const enriched = readJsonlSession(deps.spawnCwd, claudeSessionId, deps.jsonlReadMaxBytes, deps.logger);
|
|
338
|
+
if (live.claudeSessionId && live.jsonlPath && existsSync(live.jsonlPath)) {
|
|
339
|
+
const enriched = readJsonlSession(deps.spawnCwd, live.claudeSessionId, deps.jsonlReadMaxBytes, deps.logger);
|
|
355
340
|
if (enriched) {
|
|
356
341
|
lastMessageAt = enriched.lastMessageAt;
|
|
357
342
|
turnCount = enriched.turnCount;
|
|
@@ -365,7 +350,6 @@ export function buildHttpApp(deps) {
|
|
|
365
350
|
turnCount,
|
|
366
351
|
capped,
|
|
367
352
|
sizeBytes,
|
|
368
|
-
claudeSessionId,
|
|
369
353
|
};
|
|
370
354
|
}
|
|
371
355
|
else {
|
|
@@ -376,6 +360,7 @@ export function buildHttpApp(deps) {
|
|
|
376
360
|
}
|
|
377
361
|
payload = {
|
|
378
362
|
sessionId: enriched.claudeSessionId,
|
|
363
|
+
claudeSessionId: enriched.claudeSessionId,
|
|
379
364
|
pid: null,
|
|
380
365
|
url: null,
|
|
381
366
|
startedAt: enriched.startedAt,
|
|
@@ -389,7 +374,8 @@ export function buildHttpApp(deps) {
|
|
|
389
374
|
turnCount: enriched.turnCount,
|
|
390
375
|
capped: enriched.capped,
|
|
391
376
|
sizeBytes: enriched.sizeBytes,
|
|
392
|
-
|
|
377
|
+
hidden: false,
|
|
378
|
+
specialist: null,
|
|
393
379
|
};
|
|
394
380
|
deps.logger(`meta-jsonl-tail-bytes sessionId=${sessionId.slice(0, 8)} read=${Math.min(enriched.sizeBytes, deps.jsonlReadMaxBytes)} capped=${enriched.capped}`);
|
|
395
381
|
}
|
|
@@ -397,8 +383,6 @@ export function buildHttpApp(deps) {
|
|
|
397
383
|
timed(deps.logger, 'GET', `/${sessionId}/meta`, 200, Date.now() - start);
|
|
398
384
|
return c.json(payload, 200);
|
|
399
385
|
});
|
|
400
|
-
// operator-owned displayName. Writes sidecar JSON next to the
|
|
401
|
-
// JSONL. Sidecar never touches the claude-owned transcript.
|
|
402
386
|
app.patch('/:sessionId', async (c) => {
|
|
403
387
|
const start = Date.now();
|
|
404
388
|
const sessionId = c.req.param('sessionId');
|
|
@@ -438,12 +422,6 @@ export function buildHttpApp(deps) {
|
|
|
438
422
|
timed(deps.logger, 'PATCH', `/${sessionId}`, 200, Date.now() - start);
|
|
439
423
|
return c.json({ ok: true, displayName: body.displayName }, 200);
|
|
440
424
|
});
|
|
441
|
-
// archive / unarchive. Moves <id>.jsonl + sidecar between the
|
|
442
|
-
// project dir and a sibling `archive/` subdir at
|
|
443
|
-
// ${claudeStateRoot()}/projects/<slug>/archive/. Archived JSONLs no longer
|
|
444
|
-
// appear in /list because the enumerator only reads files at the top of the
|
|
445
|
-
// slug dir, not subdirs. Refuses with 409 pty-still-alive when any live PTY
|
|
446
|
-
// maps to this claudeSessionId — same guard as the purge route.
|
|
447
425
|
app.post('/:sessionId/archive', async (c) => {
|
|
448
426
|
const start = Date.now();
|
|
449
427
|
const sessionId = c.req.param('sessionId');
|
|
@@ -464,9 +442,6 @@ export function buildHttpApp(deps) {
|
|
|
464
442
|
timed(deps.logger, 'POST', `/${sessionId}/archive`, 400, Date.now() - start);
|
|
465
443
|
return c.json({ error: 'invalid-mode', detail: { expected: ['archive', 'unarchive'] } }, 400);
|
|
466
444
|
}
|
|
467
|
-
// live and dehydrated rows share the intrinsic id, so a
|
|
468
|
-
// single store probe answers "is the PTY still alive?". The compensating
|
|
469
|
-
// findLiveByClaudeId lookup is gone.
|
|
470
445
|
if (deps.store.get(sessionId)) {
|
|
471
446
|
deps.logger(`archive-rejected sessionId=${sessionId.slice(0, 8)} reason=pty-still-alive`);
|
|
472
447
|
timed(deps.logger, 'POST', `/${sessionId}/archive`, 409, Date.now() - start);
|
|
@@ -502,9 +477,6 @@ export function buildHttpApp(deps) {
|
|
|
502
477
|
timed(deps.logger, 'POST', `/${sessionId}/archive`, 200, Date.now() - start);
|
|
503
478
|
return c.json({ ok: true, mode }, 200);
|
|
504
479
|
});
|
|
505
|
-
// DELETE branches on `?purge=1`. Bare DELETE is the existing
|
|
506
|
-
// "end session" path (kills the PTY, leaves the JSONL). Purge hard-removes
|
|
507
|
-
// the JSONL + sidecar + any `<sessionId>/` subdir claude wrote.
|
|
508
480
|
app.delete('/:sessionId', async (c) => {
|
|
509
481
|
const start = Date.now();
|
|
510
482
|
const sessionId = c.req.param('sessionId');
|
|
@@ -518,8 +490,6 @@ export function buildHttpApp(deps) {
|
|
|
518
490
|
timed(deps.logger, 'DELETE', `/${sessionId}`, 204, Date.now() - start);
|
|
519
491
|
return c.body(null, 204);
|
|
520
492
|
}
|
|
521
|
-
// Purge path. — live rows are keyed on the intrinsic id, so a
|
|
522
|
-
// single store probe is the alive-PTY check.
|
|
523
493
|
if (deps.store.get(sessionId)) {
|
|
524
494
|
deps.logger(`purge-rejected sessionId=${sessionId.slice(0, 8)} reason=pty-still-alive`);
|
|
525
495
|
timed(deps.logger, 'DELETE', `/${sessionId}?purge=1`, 409, Date.now() - start);
|
|
@@ -603,26 +573,27 @@ export function buildHttpApp(deps) {
|
|
|
603
573
|
timed(deps.logger, 'GET', `/${sessionId}/log`, 404, Date.now() - start);
|
|
604
574
|
return c.json({ error: 'session-not-found' }, 404);
|
|
605
575
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
576
|
+
if (s.jsonlPath === null) {
|
|
577
|
+
// alive but pre-promotion: the first operator turn has not landed,
|
|
578
|
+
// so claude has not written a JSONL yet. Loud-fail with 202 so the
|
|
579
|
+
// UI can re-poll once `claudeSessionId` flips on a subsequent /list.
|
|
580
|
+
timed(deps.logger, 'GET', `/${sessionId}/log`, 202, Date.now() - start);
|
|
581
|
+
return c.json({ jsonlPath: null, exists: false, sizeBytes: 0, pending: true }, 202);
|
|
582
|
+
}
|
|
583
|
+
const jsonlPath = s.jsonlPath;
|
|
609
584
|
const follow = c.req.query('follow') === '1';
|
|
610
585
|
if (!follow) {
|
|
611
|
-
const exists = existsSync(
|
|
612
|
-
const sizeBytes = exists ? statSync(
|
|
586
|
+
const exists = existsSync(jsonlPath);
|
|
587
|
+
const sizeBytes = exists ? statSync(jsonlPath).size : 0;
|
|
613
588
|
timed(deps.logger, 'GET', `/${sessionId}/log`, 200, Date.now() - start);
|
|
614
|
-
return c.json({ jsonlPath
|
|
589
|
+
return c.json({ jsonlPath, exists, sizeBytes }, 200);
|
|
615
590
|
}
|
|
616
|
-
// Streaming follow. Stream the current file content, then watch for
|
|
617
|
-
// appends and push new bytes as they arrive. On client disconnect
|
|
618
|
-
// (controller.cancel via Hono's onAbort) unwatch.
|
|
619
|
-
const path = s.jsonlPath;
|
|
620
591
|
timed(deps.logger, 'GET', `/${sessionId}/log?follow=1`, 200, Date.now() - start);
|
|
621
592
|
return stream(c, async (out) => {
|
|
622
593
|
let position = 0;
|
|
623
|
-
if (existsSync(
|
|
624
|
-
const initialSize = statSync(
|
|
625
|
-
const head = createReadStream(
|
|
594
|
+
if (existsSync(jsonlPath)) {
|
|
595
|
+
const initialSize = statSync(jsonlPath).size;
|
|
596
|
+
const head = createReadStream(jsonlPath, { start: 0, end: Math.max(0, initialSize - 1) });
|
|
626
597
|
for await (const chunk of head) {
|
|
627
598
|
await out.write(chunk);
|
|
628
599
|
}
|
|
@@ -631,25 +602,24 @@ export function buildHttpApp(deps) {
|
|
|
631
602
|
let active = true;
|
|
632
603
|
out.onAbort(() => {
|
|
633
604
|
active = false;
|
|
634
|
-
unwatchFile(
|
|
605
|
+
unwatchFile(jsonlPath);
|
|
635
606
|
});
|
|
636
607
|
const listener = () => {
|
|
637
608
|
if (!active)
|
|
638
609
|
return;
|
|
639
|
-
if (!existsSync(
|
|
610
|
+
if (!existsSync(jsonlPath))
|
|
640
611
|
return;
|
|
641
|
-
const newSize = statSync(
|
|
612
|
+
const newSize = statSync(jsonlPath).size;
|
|
642
613
|
if (newSize <= position)
|
|
643
614
|
return;
|
|
644
|
-
const tail = createReadStream(
|
|
615
|
+
const tail = createReadStream(jsonlPath, { start: position, end: newSize - 1 });
|
|
645
616
|
position = newSize;
|
|
646
617
|
tail.on('data', (chunk) => {
|
|
647
618
|
if (active)
|
|
648
619
|
void out.write(chunk);
|
|
649
620
|
});
|
|
650
621
|
};
|
|
651
|
-
watchFile(
|
|
652
|
-
// Keep the stream open until the client aborts.
|
|
622
|
+
watchFile(jsonlPath, { interval: 250 }, listener);
|
|
653
623
|
while (active) {
|
|
654
624
|
await new Promise((resolve) => setTimeout(resolve, 1_000));
|
|
655
625
|
}
|