@rubytech/create-maxy-code 0.1.39 → 0.1.41
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/dist/index.js +35 -101
- package/dist/uninstall.js +11 -102
- package/package.json +1 -1
- 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 +7 -3
- package/payload/platform/plugins/docs/references/platform.md +18 -4
- package/payload/platform/plugins/docs/references/plugins-guide.md +2 -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 +37 -1
- 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 +54 -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 +516 -0
- package/payload/platform/services/claude-session-manager/dist/fs-watcher.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/hooks-discovery.d.ts +7 -0
- package/payload/platform/services/claude-session-manager/dist/hooks-discovery.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/hooks-discovery.js +90 -0
- package/payload/platform/services/claude-session-manager/dist/hooks-discovery.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 +426 -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 +60 -13
- 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 +40 -45
- 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 +146 -162
- 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/platform/services/claude-session-manager/dist/system-prompt.d.ts +54 -0
- package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js +111 -0
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +4 -2
- package/payload/platform/templates/agents/admin/SOUL.md +8 -14
- 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/{chunk-NNHHGQJL.js → chunk-4P2MPERV.js} +6 -163
- package/payload/server/{chunk-3G6JMV7N.js → chunk-MEGIQYUD.js} +1 -1
- package/payload/server/{chunk-QC5LHYPH.js → chunk-XJVJ2B2Q.js} +1 -1
- package/payload/server/{cloudflare-task-tracker-NEB3X6O2.js → cloudflare-task-tracker-R6MMORFU.js} +2 -2
- package/payload/server/maxy-edge.js +2 -2
- package/payload/server/public/assets/{Checkbox-CmCjmfJy.js → Checkbox-blxv2XPY.js} +1 -1
- package/payload/server/public/assets/{admin-CtodW6As.js → admin-C3EvHpLN.js} +1 -1
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-CfOjbx0H.js → architectureDiagram-Q4EWVU46-bETsVzGS.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-D0kA2C9i.js → blockDiagram-DXYQGD6D-5y0GBnfQ.js} +1 -1
- package/payload/server/public/assets/{brand-6hKfBgOI.css → brand-DQlLKLDH.css} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-CDgXiXgj.js → c4Diagram-AHTNJAMY-C1znpVGu.js} +1 -1
- package/payload/server/public/assets/channel-BWqTaXzb.js +1 -0
- package/payload/server/public/assets/{chunk-336JU56O-BfBTcT5X.js → chunk-336JU56O-DlCVUGzQ.js} +2 -2
- package/payload/server/public/assets/{chunk-426QAEUC-BdSSZ1Kw.js → chunk-426QAEUC-bGYkmA4z.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-UvE1im9g.js → chunk-4TB4RGXK-6Sb0Undm.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-Dx4DslXz.js → chunk-5FUZZQ4R-CAT_Pq2T.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-CNHawhLJ.js → chunk-5PVQY5BW-j231ESse.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-Rmjv-1et.js → chunk-EDXVE4YY-BAlS4QL7.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-CL-LeH3r.js → chunk-ENJZ2VHE-D3Ku_BMP.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-B2MFOJDt.js → chunk-ICPOFSXX-AR86A5WB.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-BxcIcCt5.js → chunk-OYMX7WX6-DxGhapkW.js} +1 -1
- package/payload/server/public/assets/{chunk-U2HBQHQK-COzwOBrY.js → chunk-U2HBQHQK-DAlqsDo_.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-B0rjT5hL.js → chunk-X2U36JSP-lZdkkdyB.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-pwrIw0XH.js → chunk-YZCP3GAM-CCjtXOFD.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-BXpeAm0k.js → chunk-ZZ45TVLE-ComtdKny.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-B9hC6fOR.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-afl_ydIG.js +1 -0
- package/payload/server/public/assets/clone-Lj83OaZ3.js +1 -0
- package/payload/server/public/assets/{dagre-DjOJn05k.js → dagre-DX8QJeVt.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-PczvQiEh.js → dagre-KV5264BT-BcRbiZ1q.js} +1 -1
- package/payload/server/public/assets/data-CS_R5VWQ.js +1 -0
- package/payload/server/public/assets/{device-url-actions-C55ZS_Zg.js → device-url-actions-Cag9IJ7n.js} +1 -1
- package/payload/server/public/assets/{diagram-5BDNPKRD-D477xo5F.js → diagram-5BDNPKRD-Bw47VwHO.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-C-HAtbtT.js → diagram-G4DWMVQ6-B-aVfhiD.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-B9wus4H0.js → diagram-MMDJMWI5-DERz2dKo.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-UMZLloFr.js → diagram-TYMM5635-B7eEX9Ta.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-B8E52Mp4.js → erDiagram-SMLLAGMA-G_JJq3zB.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-CUel34tq.js → flowDiagram-DWJPFMVM-BeY9QaO0.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-BfwE-Xav.js → ganttDiagram-T4ZO3ILL-IXviGYnx.js} +1 -1
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-2d6tXRIM.js → gitGraphDiagram-UUTBAWPF-GlY6pZQ1.js} +1 -1
- package/payload/server/public/assets/graph-DtGkQBFr.js +1 -0
- package/payload/server/public/assets/graph-labels-Dwu1ON5v.js +1 -0
- package/payload/server/public/assets/{graphlib-CCo9-wNJ.js → graphlib-BWgAjZSb.js} +1 -1
- package/payload/server/public/assets/{infoDiagram-42DDH7IO-DB1SVWL6.js → infoDiagram-42DDH7IO-D4YZM0gD.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-CdeMUTYS.js → ishikawaDiagram-UXIWVN3A-fR71GDOp.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-D8x8TUoc.js → journeyDiagram-VCZTEJTY-C1TcY_6m.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-Cmkhd1mO.js → kanban-definition-6JOO6SKY-IPZKpnnG.js} +1 -1
- package/payload/server/public/assets/{line-CoHkpBv3.js → line-B2JtEL1B.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-C7R2PfZC.js → mermaid-parser.core-BKCLXqUh.js} +1 -1
- package/payload/server/public/assets/{mermaid.core-lQaq-kll.js → mermaid.core-CmNpCLB-.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-CsPq3pX1.js → mindmap-definition-QFDTVHPH-BS0E2FPD.js} +1 -1
- package/payload/server/public/assets/{page-DwEaAdVp.js → page-BdI3GCun.js} +1 -1
- package/payload/server/public/assets/{page-CBKzjUHO.js → page-zIjAkU2y.js} +1 -1
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-FZH9sTnP.js → pieDiagram-DEJITSTG-J61JDyk8.js} +1 -1
- package/payload/server/public/assets/{public-hGLzxhE_.js → public-C5vaQnC_.js} +4 -4
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-9poxfoqh.js → quadrantDiagram-34T5L4WZ-CLI03OtR.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-Ce7iOdT_.js → requirementDiagram-MS252O5E-C8MCt5NO.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-DINuXatI.js → sankeyDiagram-XADWPNL6-DQuugruj.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-CSMec3EK.js → sequenceDiagram-FGHM5R23-CoYitwhO.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-15DKzypj.js → stateDiagram-FHFEXIEX-G68cHHoI.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-Dtgpgr_D.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-BxIRc9Pu.js → timeline-definition-GMOUNBTQ-CelLeJVS.js} +1 -1
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-DdgRyU55.js → vennDiagram-DHZGUBPP-BVgFPkJq.js} +1 -1
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-CKvhU1Uj.js → wardleyDiagram-NUSXRM2D-CzD_8hl2.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-d0V1V4PD.js → xychartDiagram-5P7HB3ND-BuTxWdX5.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 +1058 -846
- package/payload/server/public/assets/channel-DA7MGMzQ.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-DqfmcWSr.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-BVOMSbZ_.js +0 -1
- package/payload/server/public/assets/clone-Byp7hagU.js +0 -1
- package/payload/server/public/assets/data-A7azIkZU.js +0 -1
- package/payload/server/public/assets/graph-Dx83EhtP.js +0 -1
- package/payload/server/public/assets/graph-labels-Bg2Bzelb.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-AcaK_QfD.js +0 -1
- /package/payload/server/public/assets/{brand-DXOx08r0.js → brand-D6mXeyxV.js} +0 -0
|
@@ -6,35 +6,27 @@
|
|
|
6
6
|
// POST /spawn { senderId, role, channel, tunnelUrl? } -> Session
|
|
7
7
|
// GET /list?senderId=... -> Session[]
|
|
8
8
|
// POST /resume { senderId, role, channel, claudeSessionId } -> Session
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
9
|
+
// POST /:id/stop -> 204 (kill PTY; idempotent on already-dead row)
|
|
10
|
+
// 404 if no row matches any of the 4 keys
|
|
11
|
+
// DELETE /:id -> 204 (rmSync JSONL + sidecar + subdir)
|
|
12
|
+
// 409 if PTY still alive
|
|
13
|
+
// 404 if no transcript on disk
|
|
14
|
+
// POST /:id/input { text }
|
|
15
|
+
// GET /:id/log -> { jsonlPath, sizeBytes, exists }
|
|
16
|
+
// GET /:id/log?follow=1 -> streamed file content (current + new)
|
|
17
|
+
// GET /:id/meta -> SessionMeta payload
|
|
18
|
+
// PATCH /:id { displayName } -> 204 (writes sidecar)
|
|
19
|
+
// POST /:id/archive { mode }
|
|
16
20
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
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.
|
|
24
|
-
//
|
|
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.
|
|
21
|
+
// Any of the four ids — intrinsic sessionId, bridgeSessionId, bridgeSuffix,
|
|
22
|
+
// or pid (numeric) — resolves to the same row via the fs-watcher index.
|
|
23
|
+
// Unknown ids return 404; nothing returns a silent 204 against an id the
|
|
24
|
+
// manager does not know.
|
|
33
25
|
import { Hono } from 'hono';
|
|
34
26
|
import { stream } from 'hono/streaming';
|
|
35
27
|
import { existsSync, statSync, createReadStream, watchFile, unwatchFile, rmSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
|
|
36
|
-
import { spawnClaudeSession,
|
|
37
|
-
import {
|
|
28
|
+
import { spawnClaudeSession, stopSession, PERMISSION_MODES } from './pty-spawner.js';
|
|
29
|
+
import { readJsonlSession, readSidecar, sidecarPathFor } from './jsonl-enumerator.js';
|
|
38
30
|
import { claudeStateRoot, projectSlugForCwd } from './jsonl-path.js';
|
|
39
31
|
import { basename, join } from 'node:path';
|
|
40
32
|
const ROLES = ['admin', 'public'];
|
|
@@ -43,12 +35,10 @@ function parseAboutOwner(raw) {
|
|
|
43
35
|
if (!raw || typeof raw !== 'object')
|
|
44
36
|
return undefined;
|
|
45
37
|
const r = raw;
|
|
46
|
-
if (r.ok === true && typeof r.body === 'string')
|
|
38
|
+
if (r.ok === true && typeof r.body === 'string')
|
|
47
39
|
return { ok: true, body: r.body };
|
|
48
|
-
|
|
49
|
-
if (r.ok === false && typeof r.reason === 'string' && r.reason.length > 0) {
|
|
40
|
+
if (r.ok === false && typeof r.reason === 'string' && r.reason.length > 0)
|
|
50
41
|
return { ok: false, reason: r.reason };
|
|
51
|
-
}
|
|
52
42
|
return undefined;
|
|
53
43
|
}
|
|
54
44
|
function parseDormantPlugins(raw) {
|
|
@@ -67,37 +57,94 @@ function parseDormantPlugins(raw) {
|
|
|
67
57
|
}
|
|
68
58
|
return out.length > 0 ? out : undefined;
|
|
69
59
|
}
|
|
70
|
-
|
|
71
|
-
|
|
60
|
+
function parseNameDescList(raw) {
|
|
61
|
+
if (!Array.isArray(raw))
|
|
62
|
+
return [];
|
|
63
|
+
const out = [];
|
|
64
|
+
for (const entry of raw) {
|
|
65
|
+
if (!entry || typeof entry !== 'object')
|
|
66
|
+
continue;
|
|
67
|
+
const e = entry;
|
|
68
|
+
if (typeof e.name !== 'string' || e.name.length === 0)
|
|
69
|
+
continue;
|
|
70
|
+
const item = { name: e.name };
|
|
71
|
+
if (typeof e.description === 'string' && e.description.length > 0)
|
|
72
|
+
item.description = e.description;
|
|
73
|
+
out.push(item);
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
function parseActivePlugins(raw) {
|
|
78
|
+
if (!Array.isArray(raw))
|
|
79
|
+
return undefined;
|
|
80
|
+
const out = [];
|
|
81
|
+
for (const entry of raw) {
|
|
82
|
+
if (!entry || typeof entry !== 'object')
|
|
83
|
+
continue;
|
|
84
|
+
const e = entry;
|
|
85
|
+
if (typeof e.name !== 'string' || e.name.length === 0)
|
|
86
|
+
continue;
|
|
87
|
+
if (typeof e.description !== 'string')
|
|
88
|
+
continue;
|
|
89
|
+
out.push({
|
|
90
|
+
name: e.name,
|
|
91
|
+
description: e.description,
|
|
92
|
+
tools: parseNameDescList(e.tools),
|
|
93
|
+
skills: parseNameDescList(e.skills),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return out.length > 0 ? out : undefined;
|
|
97
|
+
}
|
|
98
|
+
function parseSpecialistDomains(raw) {
|
|
99
|
+
if (!Array.isArray(raw))
|
|
100
|
+
return undefined;
|
|
101
|
+
const out = [];
|
|
102
|
+
for (const entry of raw) {
|
|
103
|
+
if (!entry || typeof entry !== 'object')
|
|
104
|
+
continue;
|
|
105
|
+
const e = entry;
|
|
106
|
+
if (typeof e.name !== 'string' || e.name.length === 0)
|
|
107
|
+
continue;
|
|
108
|
+
if (typeof e.description !== 'string')
|
|
109
|
+
continue;
|
|
110
|
+
out.push({ name: e.name, description: e.description, tools: parseNameDescList(e.tools) });
|
|
111
|
+
}
|
|
112
|
+
return out.length > 0 ? out : undefined;
|
|
113
|
+
}
|
|
114
|
+
// `<id>` may be: intrinsic UUID, bridgeSessionId ("session_<suffix>"),
|
|
115
|
+
// bridgeSuffix, or pid (numeric). All resolve to a sessionId via watcher.
|
|
72
116
|
const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
73
117
|
const LIST_TAIL_BYTES = 64 * 1024;
|
|
74
118
|
const DISPLAY_NAME_MAX = 256;
|
|
75
119
|
function timed(logger, method, path, status, ms) {
|
|
76
120
|
logger(`api method=${method} path=${path} outcome=${status} ms=${ms}`);
|
|
77
121
|
}
|
|
78
|
-
/**
|
|
79
|
-
*
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return
|
|
122
|
+
/** Resolve any of the 4 keys to the canonical (sessionId, row) pair.
|
|
123
|
+
* Tries: intrinsic sessionId, bridgeSessionId, bridgeSuffix, numeric pid.
|
|
124
|
+
* Returns undefined when nothing matches. */
|
|
125
|
+
function resolveRow(watcher, id) {
|
|
126
|
+
const direct = watcher.getBySessionId(id);
|
|
127
|
+
if (direct)
|
|
128
|
+
return direct;
|
|
129
|
+
const byBridge = watcher.getByBridgeSessionId(id);
|
|
130
|
+
if (byBridge)
|
|
131
|
+
return byBridge;
|
|
132
|
+
const bySuffix = watcher.getByBridgeSuffix(id);
|
|
133
|
+
if (bySuffix)
|
|
134
|
+
return bySuffix;
|
|
135
|
+
if (/^\d+$/.test(id)) {
|
|
136
|
+
const byPid = watcher.getByPid(Number(id));
|
|
137
|
+
if (byPid)
|
|
138
|
+
return byPid;
|
|
88
139
|
}
|
|
89
|
-
|
|
90
|
-
const path = join(claudeStateRoot(), 'projects', slug, `${sessionId}.jsonl`);
|
|
91
|
-
if (existsSync(path))
|
|
92
|
-
return { path, claudeSessionId: sessionId };
|
|
93
|
-
return null;
|
|
140
|
+
return undefined;
|
|
94
141
|
}
|
|
95
142
|
function attachPublicAudit(deps, session) {
|
|
96
143
|
if (session.role !== 'public')
|
|
97
144
|
return;
|
|
98
|
-
if (session.
|
|
145
|
+
if (session.jsonlPath === null)
|
|
99
146
|
return;
|
|
100
|
-
const claudeSessionId = session.
|
|
147
|
+
const claudeSessionId = session.sessionId;
|
|
101
148
|
deps.publicToolAudit.attach(claudeSessionId, session.jsonlPath);
|
|
102
149
|
if (session.exited) {
|
|
103
150
|
deps.publicToolAudit.detach(claudeSessionId);
|
|
@@ -109,24 +156,41 @@ function attachPublicAudit(deps, session) {
|
|
|
109
156
|
}
|
|
110
157
|
export function buildHttpApp(deps) {
|
|
111
158
|
const app = new Hono();
|
|
159
|
+
// Watcher subscriber: when JSONL CREATE fires for a live session,
|
|
160
|
+
// patch jsonlPath onto the StoredSession and attach the audit watcher.
|
|
161
|
+
deps.watcher.subscribe((event) => {
|
|
162
|
+
if (event.source !== 'jsonl')
|
|
163
|
+
return;
|
|
164
|
+
if (event.kind !== 'create' && event.kind !== 'modify')
|
|
165
|
+
return;
|
|
166
|
+
if (!event.row || !event.row.jsonlPath)
|
|
167
|
+
return;
|
|
168
|
+
const live = deps.store.get(event.sessionId);
|
|
169
|
+
if (!live)
|
|
170
|
+
return;
|
|
171
|
+
if (live.jsonlPath !== null)
|
|
172
|
+
return;
|
|
173
|
+
live.jsonlPath = event.row.jsonlPath;
|
|
174
|
+
deps.logger(`[fs-watcher] live-row-jsonl-attached sessionId=${event.sessionId} path=${event.row.jsonlPath}`);
|
|
175
|
+
attachPublicAudit(deps, live);
|
|
176
|
+
});
|
|
112
177
|
function liveDisplayName(s, sidecarName) {
|
|
113
178
|
if (sidecarName && sidecarName.trim())
|
|
114
179
|
return sidecarName.trim();
|
|
115
|
-
if (s.jsonlPath &&
|
|
116
|
-
const enriched = readJsonlSession(deps.spawnCwd, s.
|
|
180
|
+
if (s.jsonlPath && existsSync(s.jsonlPath)) {
|
|
181
|
+
const enriched = readJsonlSession(deps.spawnCwd, s.sessionId, LIST_TAIL_BYTES, deps.logger);
|
|
117
182
|
if (enriched?.aiTitle?.trim())
|
|
118
183
|
return enriched.aiTitle.trim();
|
|
119
184
|
}
|
|
120
|
-
|
|
121
|
-
// chars of the intrinsic id (which the operator may copy into resume).
|
|
122
|
-
return (s.claudeSessionId ?? s.bridgeSuffix).slice(0, 8);
|
|
185
|
+
return s.sessionId.slice(0, 8);
|
|
123
186
|
}
|
|
124
187
|
function toLivePayload(s) {
|
|
125
188
|
const sidecar = s.jsonlPath ? readSidecar(s.jsonlPath) : null;
|
|
126
189
|
const hasJsonl = s.jsonlPath !== null && existsSync(s.jsonlPath);
|
|
127
190
|
return {
|
|
128
|
-
sessionId: s.
|
|
129
|
-
claudeSessionId: s.
|
|
191
|
+
sessionId: s.sessionId,
|
|
192
|
+
claudeSessionId: s.sessionId,
|
|
193
|
+
bridgeSessionId: s.bridgeSessionId,
|
|
130
194
|
pid: s.pid,
|
|
131
195
|
url: s.url,
|
|
132
196
|
startedAt: s.startedAt,
|
|
@@ -144,6 +208,45 @@ export function buildHttpApp(deps) {
|
|
|
144
208
|
specialist: s.specialist,
|
|
145
209
|
};
|
|
146
210
|
}
|
|
211
|
+
function toArchivedPayload(row, senderId) {
|
|
212
|
+
let displayName = row.sessionId.slice(0, 8);
|
|
213
|
+
let lastMessageAt = null;
|
|
214
|
+
let turnCount = 0;
|
|
215
|
+
let capped = false;
|
|
216
|
+
let sizeBytes = null;
|
|
217
|
+
let startedAt = new Date(row.updatedAt || 0).toISOString();
|
|
218
|
+
if (row.jsonlPath && existsSync(row.jsonlPath)) {
|
|
219
|
+
const enriched = readJsonlSession(deps.spawnCwd, row.sessionId, LIST_TAIL_BYTES, deps.logger);
|
|
220
|
+
if (enriched) {
|
|
221
|
+
displayName = enriched.displayName;
|
|
222
|
+
lastMessageAt = enriched.lastMessageAt;
|
|
223
|
+
turnCount = enriched.turnCount;
|
|
224
|
+
capped = enriched.capped;
|
|
225
|
+
sizeBytes = enriched.sizeBytes;
|
|
226
|
+
startedAt = enriched.startedAt;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
sessionId: row.sessionId,
|
|
231
|
+
claudeSessionId: row.sessionId,
|
|
232
|
+
bridgeSessionId: row.bridgeSessionId,
|
|
233
|
+
pid: null,
|
|
234
|
+
url: null,
|
|
235
|
+
startedAt,
|
|
236
|
+
senderId,
|
|
237
|
+
role: null,
|
|
238
|
+
channel: null,
|
|
239
|
+
jsonlPath: row.jsonlPath,
|
|
240
|
+
status: 'ended',
|
|
241
|
+
displayName,
|
|
242
|
+
lastMessageAt,
|
|
243
|
+
turnCount,
|
|
244
|
+
capped,
|
|
245
|
+
sizeBytes,
|
|
246
|
+
hidden: false,
|
|
247
|
+
specialist: row.agent,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
147
250
|
app.post('/spawn', async (c) => {
|
|
148
251
|
const start = Date.now();
|
|
149
252
|
let body;
|
|
@@ -162,7 +265,6 @@ export function buildHttpApp(deps) {
|
|
|
162
265
|
const specialist = typeof body.specialist === 'string' && /^[A-Za-z0-9_-]{1,64}$/.test(body.specialist)
|
|
163
266
|
? body.specialist
|
|
164
267
|
: undefined;
|
|
165
|
-
const stdinPayload = typeof body.stdinPayload === 'string' ? body.stdinPayload : undefined;
|
|
166
268
|
const aboutOwnerShape = body.aboutOwner == null ? 'absent' : (typeof body.aboutOwner === 'object' ? 'object' : typeof body.aboutOwner);
|
|
167
269
|
const dormantPluginsShape = Array.isArray(body.dormantPlugins)
|
|
168
270
|
? `array(${body.dormantPlugins.length})`
|
|
@@ -194,16 +296,14 @@ export function buildHttpApp(deps) {
|
|
|
194
296
|
: undefined;
|
|
195
297
|
const aboutOwner = parseAboutOwner(body.aboutOwner);
|
|
196
298
|
const dormantPlugins = parseDormantPlugins(body.dormantPlugins);
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
// string → render; null → render `tunnel-url: none`; undefined → fall
|
|
200
|
-
// back to deps.host.tunnelUrl (whatever boot resolved).
|
|
299
|
+
const activePlugins = parseActivePlugins(body.activePlugins);
|
|
300
|
+
const specialistDomains = parseSpecialistDomains(body.specialistDomains);
|
|
201
301
|
const tunnelUrlOverride = typeof body.tunnelUrl === 'string'
|
|
202
302
|
? body.tunnelUrl
|
|
203
303
|
: body.tunnelUrl === null
|
|
204
304
|
? null
|
|
205
305
|
: undefined;
|
|
206
|
-
const result = await spawnClaudeSession({ ...deps, store: deps.store,
|
|
306
|
+
const result = await spawnClaudeSession({ ...deps, store: deps.store, watcher: deps.watcher, onSessionReady: (s) => attachPublicAudit(deps, s), tunnelUrlOverride }, {
|
|
207
307
|
senderId,
|
|
208
308
|
role: role,
|
|
209
309
|
channel: channel,
|
|
@@ -212,21 +312,151 @@ export function buildHttpApp(deps) {
|
|
|
212
312
|
attachmentDir,
|
|
213
313
|
aboutOwner,
|
|
214
314
|
dormantPlugins,
|
|
315
|
+
activePlugins,
|
|
316
|
+
specialistDomains,
|
|
215
317
|
hidden,
|
|
216
318
|
specialist,
|
|
217
|
-
stdinPayload,
|
|
218
319
|
});
|
|
219
320
|
if (!result.ok) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
321
|
+
if ('rejected' in result) {
|
|
322
|
+
timed(deps.logger, 'POST', '/spawn', 503, Date.now() - start);
|
|
323
|
+
if (result.rejected.kind === 'specialist-cap-reached') {
|
|
324
|
+
return c.json({ error: 'specialist-cap-reached', currentCount: result.rejected.currentCount, cap: result.rejected.cap }, 503);
|
|
325
|
+
}
|
|
326
|
+
return c.json({ error: 'operator-slots-reserved', total: result.rejected.total, reserve: result.rejected.reserve, totalCap: result.rejected.totalCap }, 503);
|
|
327
|
+
}
|
|
224
328
|
timed(deps.logger, 'POST', '/spawn', 500, Date.now() - start);
|
|
225
329
|
return c.json({ error: 'spawn-failed', reason: result.reason, stderrTail: result.stderrTail }, 500);
|
|
226
330
|
}
|
|
227
331
|
timed(deps.logger, 'POST', '/spawn', 200, Date.now() - start);
|
|
228
332
|
return c.json(toLivePayload(result.session), 200);
|
|
229
333
|
});
|
|
334
|
+
// SSE feed of row deltas. One channel per client; manager fans out from
|
|
335
|
+
// a single watcher subscription. On connect: replay current rows as
|
|
336
|
+
// `row-created`, then stream subsequent watcher events as
|
|
337
|
+
// `row-<created|updated|archived|removed>`.
|
|
338
|
+
//
|
|
339
|
+
// Payload shape per event (`data: <json>`):
|
|
340
|
+
// { sessionId, live, status, agentLabel, bridgeSessionId, cwd,
|
|
341
|
+
// firstPrompt, updatedAt }
|
|
342
|
+
//
|
|
343
|
+
// `live` mirrors the watcher's `state === 'live'`. `status` is the
|
|
344
|
+
// watcher's `idle | busy | gone`. `agentLabel` is the resolution
|
|
345
|
+
// `agent ?? cachedAgentSetting`; the UI maps null → "admin",
|
|
346
|
+
// non-null → the specialist name. `firstPrompt` is the resolved
|
|
347
|
+
// displayName (sidecar → ai-title → 8-char slice) so the row title is
|
|
348
|
+
// computed once on the server, not by every browser tab.
|
|
349
|
+
app.get('/events', (c) => {
|
|
350
|
+
const start = Date.now();
|
|
351
|
+
const clientIp = c.req.header('x-forwarded-for') ?? 'loopback';
|
|
352
|
+
const replayRows = deps.watcher.all();
|
|
353
|
+
deps.logger(`[admin-events] client-connected ip=${clientIp} seeded-rows=${replayRows.length}`);
|
|
354
|
+
function rowToPayload(row) {
|
|
355
|
+
const agentLabel = row.agent ?? row.cachedAgentSetting ?? null;
|
|
356
|
+
let firstPrompt = null;
|
|
357
|
+
if (row.jsonlPath && existsSync(row.jsonlPath)) {
|
|
358
|
+
const enriched = readJsonlSession(deps.spawnCwd, row.sessionId, LIST_TAIL_BYTES, deps.logger);
|
|
359
|
+
if (enriched)
|
|
360
|
+
firstPrompt = enriched.displayName;
|
|
361
|
+
}
|
|
362
|
+
if (!firstPrompt) {
|
|
363
|
+
const sidecar = row.jsonlPath ? readSidecar(row.jsonlPath) : null;
|
|
364
|
+
firstPrompt = sidecar?.displayName ?? row.sessionId.slice(0, 8);
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
sessionId: row.sessionId,
|
|
368
|
+
live: row.state === 'live',
|
|
369
|
+
status: row.status,
|
|
370
|
+
agentLabel,
|
|
371
|
+
bridgeSessionId: row.bridgeSessionId,
|
|
372
|
+
cwd: row.cwd,
|
|
373
|
+
firstPrompt,
|
|
374
|
+
updatedAt: row.updatedAt,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
timed(deps.logger, 'GET', '/events', 200, Date.now() - start);
|
|
378
|
+
return stream(c, async (out) => {
|
|
379
|
+
// SSE response headers — Hono's stream() helper sets text/event-stream
|
|
380
|
+
// automatically, but we explicitly disable proxy buffering so the
|
|
381
|
+
// first replay batch arrives before any keep-alive.
|
|
382
|
+
c.header('Content-Type', 'text/event-stream');
|
|
383
|
+
c.header('Cache-Control', 'no-cache, no-transform');
|
|
384
|
+
c.header('X-Accel-Buffering', 'no');
|
|
385
|
+
c.header('Connection', 'keep-alive');
|
|
386
|
+
const connectStart = Date.now();
|
|
387
|
+
let active = true;
|
|
388
|
+
let subscribersCount = 0;
|
|
389
|
+
// Replay phase. Emit a `row-created` for every row in the index so
|
|
390
|
+
// a freshly-connected tab catches up to the manager's view before
|
|
391
|
+
// any subsequent delta arrives.
|
|
392
|
+
for (const row of replayRows) {
|
|
393
|
+
if (!active)
|
|
394
|
+
break;
|
|
395
|
+
const payload = rowToPayload(row);
|
|
396
|
+
await out.write(`event: row-created\ndata: ${JSON.stringify(payload)}\n\n`);
|
|
397
|
+
}
|
|
398
|
+
// Delta phase. Subscribe to the watcher and map watcher events to
|
|
399
|
+
// the four SSE event kinds. Mapping:
|
|
400
|
+
// create → row-created
|
|
401
|
+
// modify → row-updated (state may have flipped — see archive)
|
|
402
|
+
// delete → row-removed when the row is fully gone, row-archived
|
|
403
|
+
// when the PID file went away but the JSONL survives.
|
|
404
|
+
const unsubscribe = deps.watcher.subscribe((event) => {
|
|
405
|
+
if (!active)
|
|
406
|
+
return;
|
|
407
|
+
// The watcher emits a synthetic `kind=modify source=jsonl` pair
|
|
408
|
+
// immediately after `kind=delete source=pid` when a row demotes
|
|
409
|
+
// from live → archived. Treat that pair as one `row-archived`
|
|
410
|
+
// event so the UI sees a single transition rather than a delete
|
|
411
|
+
// followed by an unrelated update.
|
|
412
|
+
if (event.kind === 'delete' && event.source === 'pid' && event.row) {
|
|
413
|
+
const payload = rowToPayload(event.row);
|
|
414
|
+
subscribersCount += 1;
|
|
415
|
+
deps.logger(`[admin-events] emit event=row-archived sessionId=${event.sessionId} subscribers=1`);
|
|
416
|
+
void out.write(`event: row-archived\ndata: ${JSON.stringify(payload)}\n\n`);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (event.kind === 'delete' && event.source === 'jsonl' && !event.row) {
|
|
420
|
+
subscribersCount += 1;
|
|
421
|
+
deps.logger(`[admin-events] emit event=row-removed sessionId=${event.sessionId} subscribers=1`);
|
|
422
|
+
void out.write(`event: row-removed\ndata: ${JSON.stringify({ sessionId: event.sessionId })}\n\n`);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
// Skip the synthetic `modify jsonl` half of the demote pair —
|
|
426
|
+
// it carries the same row state already emitted as row-archived.
|
|
427
|
+
if (event.kind === 'modify' && event.source === 'jsonl' && event.row && event.row.state === 'archived' && event.row.pidFilePath === null) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (!event.row)
|
|
431
|
+
return;
|
|
432
|
+
const sseEvent = event.kind === 'create' ? 'row-created' : 'row-updated';
|
|
433
|
+
const payload = rowToPayload(event.row);
|
|
434
|
+
subscribersCount += 1;
|
|
435
|
+
deps.logger(`[admin-events] emit event=${sseEvent} sessionId=${event.sessionId} subscribers=1`);
|
|
436
|
+
void out.write(`event: ${sseEvent}\ndata: ${JSON.stringify(payload)}\n\n`);
|
|
437
|
+
});
|
|
438
|
+
// Keep-alive comment every 25 s. Proxies aggressively close idle
|
|
439
|
+
// text/event-stream connections at 30–60 s; a comment line is
|
|
440
|
+
// ignored by EventSource consumers but resets the proxy clock.
|
|
441
|
+
const keepAlive = setInterval(() => {
|
|
442
|
+
if (!active)
|
|
443
|
+
return;
|
|
444
|
+
void out.write(`: keep-alive ${Date.now()}\n\n`);
|
|
445
|
+
}, 25_000);
|
|
446
|
+
out.onAbort(() => {
|
|
447
|
+
active = false;
|
|
448
|
+
clearInterval(keepAlive);
|
|
449
|
+
unsubscribe();
|
|
450
|
+
const durationMs = Date.now() - connectStart;
|
|
451
|
+
deps.logger(`[admin-events] client-disconnected ip=${clientIp} duration-ms=${durationMs} emits=${subscribersCount}`);
|
|
452
|
+
});
|
|
453
|
+
// Park the stream open until the client aborts. Without this the
|
|
454
|
+
// handler returns and Hono closes the response.
|
|
455
|
+
while (active) {
|
|
456
|
+
await new Promise((resolve) => setTimeout(resolve, 1_000));
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
});
|
|
230
460
|
app.get('/list', (c) => {
|
|
231
461
|
const start = Date.now();
|
|
232
462
|
const senderId = c.req.query('senderId') ?? '';
|
|
@@ -237,36 +467,22 @@ export function buildHttpApp(deps) {
|
|
|
237
467
|
}
|
|
238
468
|
const liveStored = deps.store.listBySender(senderId, { includeHidden });
|
|
239
469
|
const live = liveStored.map(toLivePayload);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
470
|
+
const liveSessionIds = new Set();
|
|
471
|
+
for (const s of liveStored)
|
|
472
|
+
liveSessionIds.add(s.sessionId);
|
|
473
|
+
// Archived rows come from the watcher's JSONL index (rows whose
|
|
474
|
+
// state === 'archived'). The watcher's boot-seed already paired
|
|
475
|
+
// PID files with JSONLs on disk, so archived rows here are exactly
|
|
476
|
+
// the sessions whose PTY has exited but transcript persists.
|
|
247
477
|
const dehydrated = [];
|
|
248
|
-
for (const
|
|
249
|
-
if (
|
|
478
|
+
for (const row of deps.watcher.all()) {
|
|
479
|
+
if (row.state !== 'archived')
|
|
250
480
|
continue;
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
startedAt: j.startedAt,
|
|
257
|
-
senderId,
|
|
258
|
-
role: null,
|
|
259
|
-
channel: null,
|
|
260
|
-
jsonlPath: j.jsonlPath,
|
|
261
|
-
status: 'ended',
|
|
262
|
-
displayName: j.displayName,
|
|
263
|
-
lastMessageAt: j.lastMessageAt,
|
|
264
|
-
turnCount: j.turnCount,
|
|
265
|
-
capped: j.capped,
|
|
266
|
-
sizeBytes: j.sizeBytes,
|
|
267
|
-
hidden: false,
|
|
268
|
-
specialist: null,
|
|
269
|
-
});
|
|
481
|
+
if (liveSessionIds.has(row.sessionId))
|
|
482
|
+
continue;
|
|
483
|
+
if (!row.jsonlPath)
|
|
484
|
+
continue;
|
|
485
|
+
dehydrated.push(toArchivedPayload(row, senderId));
|
|
270
486
|
}
|
|
271
487
|
deps.logger(`list-source live=${live.length} jsonl=${dehydrated.length}`);
|
|
272
488
|
const merged = [...live, ...dehydrated];
|
|
@@ -297,12 +513,14 @@ export function buildHttpApp(deps) {
|
|
|
297
513
|
: undefined;
|
|
298
514
|
const aboutOwner = parseAboutOwner(body.aboutOwner);
|
|
299
515
|
const dormantPlugins = parseDormantPlugins(body.dormantPlugins);
|
|
516
|
+
const activePlugins = parseActivePlugins(body.activePlugins);
|
|
517
|
+
const specialistDomains = parseSpecialistDomains(body.specialistDomains);
|
|
300
518
|
const tunnelUrlOverride = typeof body.tunnelUrl === 'string'
|
|
301
519
|
? body.tunnelUrl
|
|
302
520
|
: body.tunnelUrl === null
|
|
303
521
|
? null
|
|
304
522
|
: undefined;
|
|
305
|
-
const result = await spawnClaudeSession({ ...deps, store: deps.store,
|
|
523
|
+
const result = await spawnClaudeSession({ ...deps, store: deps.store, watcher: deps.watcher, onSessionReady: (s) => attachPublicAudit(deps, s), tunnelUrlOverride }, {
|
|
306
524
|
senderId,
|
|
307
525
|
role: role,
|
|
308
526
|
channel: channel,
|
|
@@ -310,24 +528,38 @@ export function buildHttpApp(deps) {
|
|
|
310
528
|
channels,
|
|
311
529
|
aboutOwner,
|
|
312
530
|
dormantPlugins,
|
|
531
|
+
activePlugins,
|
|
532
|
+
specialistDomains,
|
|
313
533
|
});
|
|
314
534
|
if (!result.ok) {
|
|
535
|
+
if ('rejected' in result) {
|
|
536
|
+
timed(deps.logger, 'POST', '/resume', 503, Date.now() - start);
|
|
537
|
+
if (result.rejected.kind === 'specialist-cap-reached') {
|
|
538
|
+
return c.json({ error: 'specialist-cap-reached', currentCount: result.rejected.currentCount, cap: result.rejected.cap }, 503);
|
|
539
|
+
}
|
|
540
|
+
return c.json({ error: 'operator-slots-reserved', total: result.rejected.total, reserve: result.rejected.reserve, totalCap: result.rejected.totalCap }, 503);
|
|
541
|
+
}
|
|
315
542
|
deps.logger(`resume-failed reason=${result.reason} resumeFrom=${claudeSessionId}`);
|
|
316
543
|
timed(deps.logger, 'POST', '/resume', 500, Date.now() - start);
|
|
317
544
|
return c.json({ error: 'resume-failed', reason: result.reason }, 500);
|
|
318
545
|
}
|
|
319
|
-
deps.logger(`resume pid=${result.session.pid}
|
|
546
|
+
deps.logger(`resume pid=${result.session.pid} sessionId=${result.session.sessionId} resumeFrom=${claudeSessionId} latency-ms=${Date.now() - start}`);
|
|
320
547
|
timed(deps.logger, 'POST', '/resume', 200, Date.now() - start);
|
|
321
548
|
return c.json(toLivePayload(result.session), 200);
|
|
322
549
|
});
|
|
323
550
|
app.get('/:sessionId/meta', (c) => {
|
|
324
551
|
const start = Date.now();
|
|
325
|
-
const
|
|
326
|
-
if (!SESSION_ID_PATTERN.test(
|
|
327
|
-
timed(deps.logger, 'GET', `/${
|
|
552
|
+
const id = c.req.param('sessionId');
|
|
553
|
+
if (!SESSION_ID_PATTERN.test(id)) {
|
|
554
|
+
timed(deps.logger, 'GET', `/${id}/meta`, 400, Date.now() - start);
|
|
328
555
|
return c.json({ error: 'invalid-session-id' }, 400);
|
|
329
556
|
}
|
|
330
|
-
const
|
|
557
|
+
const row = resolveRow(deps.watcher, id);
|
|
558
|
+
if (!row) {
|
|
559
|
+
timed(deps.logger, 'GET', `/${id}/meta`, 404, Date.now() - start);
|
|
560
|
+
return c.json({ error: 'session-not-found' }, 404);
|
|
561
|
+
}
|
|
562
|
+
const live = deps.store.get(row.sessionId);
|
|
331
563
|
let payload;
|
|
332
564
|
if (live) {
|
|
333
565
|
const base = toLivePayload(live);
|
|
@@ -335,8 +567,8 @@ export function buildHttpApp(deps) {
|
|
|
335
567
|
let turnCount = 0;
|
|
336
568
|
let capped = false;
|
|
337
569
|
let sizeBytes = base.sizeBytes;
|
|
338
|
-
if (live.
|
|
339
|
-
const enriched = readJsonlSession(deps.spawnCwd, live.
|
|
570
|
+
if (live.jsonlPath && existsSync(live.jsonlPath)) {
|
|
571
|
+
const enriched = readJsonlSession(deps.spawnCwd, live.sessionId, deps.jsonlReadMaxBytes, deps.logger);
|
|
340
572
|
if (enriched) {
|
|
341
573
|
lastMessageAt = enriched.lastMessageAt;
|
|
342
574
|
turnCount = enriched.turnCount;
|
|
@@ -344,50 +576,20 @@ export function buildHttpApp(deps) {
|
|
|
344
576
|
sizeBytes = enriched.sizeBytes;
|
|
345
577
|
}
|
|
346
578
|
}
|
|
347
|
-
payload = {
|
|
348
|
-
...base,
|
|
349
|
-
lastMessageAt,
|
|
350
|
-
turnCount,
|
|
351
|
-
capped,
|
|
352
|
-
sizeBytes,
|
|
353
|
-
};
|
|
579
|
+
payload = { ...base, lastMessageAt, turnCount, capped, sizeBytes };
|
|
354
580
|
}
|
|
355
581
|
else {
|
|
356
|
-
|
|
357
|
-
if (!enriched) {
|
|
358
|
-
timed(deps.logger, 'GET', `/${sessionId}/meta`, 404, Date.now() - start);
|
|
359
|
-
return c.json({ error: 'session-not-found' }, 404);
|
|
360
|
-
}
|
|
361
|
-
payload = {
|
|
362
|
-
sessionId: enriched.claudeSessionId,
|
|
363
|
-
claudeSessionId: enriched.claudeSessionId,
|
|
364
|
-
pid: null,
|
|
365
|
-
url: null,
|
|
366
|
-
startedAt: enriched.startedAt,
|
|
367
|
-
senderId: '',
|
|
368
|
-
role: null,
|
|
369
|
-
channel: null,
|
|
370
|
-
jsonlPath: enriched.jsonlPath,
|
|
371
|
-
status: 'ended',
|
|
372
|
-
displayName: enriched.displayName,
|
|
373
|
-
lastMessageAt: enriched.lastMessageAt,
|
|
374
|
-
turnCount: enriched.turnCount,
|
|
375
|
-
capped: enriched.capped,
|
|
376
|
-
sizeBytes: enriched.sizeBytes,
|
|
377
|
-
hidden: false,
|
|
378
|
-
specialist: null,
|
|
379
|
-
};
|
|
380
|
-
deps.logger(`meta-jsonl-tail-bytes sessionId=${sessionId.slice(0, 8)} read=${Math.min(enriched.sizeBytes, deps.jsonlReadMaxBytes)} capped=${enriched.capped}`);
|
|
582
|
+
payload = toArchivedPayload(row, '');
|
|
381
583
|
}
|
|
382
|
-
deps.logger(`meta sessionId=${sessionId.slice(0, 8)} fields=${Object.keys(payload).length} ms=${Date.now() - start}`);
|
|
383
|
-
timed(deps.logger, 'GET', `/${
|
|
584
|
+
deps.logger(`meta sessionId=${row.sessionId.slice(0, 8)} fields=${Object.keys(payload).length} ms=${Date.now() - start}`);
|
|
585
|
+
timed(deps.logger, 'GET', `/${id}/meta`, 200, Date.now() - start);
|
|
384
586
|
return c.json(payload, 200);
|
|
385
587
|
});
|
|
386
588
|
app.patch('/:sessionId', async (c) => {
|
|
387
589
|
const start = Date.now();
|
|
388
|
-
const
|
|
389
|
-
if (!SESSION_ID_PATTERN.test(
|
|
390
|
-
timed(deps.logger, 'PATCH', `/${
|
|
590
|
+
const id = c.req.param('sessionId');
|
|
591
|
+
if (!SESSION_ID_PATTERN.test(id)) {
|
|
592
|
+
timed(deps.logger, 'PATCH', `/${id}`, 400, Date.now() - start);
|
|
391
593
|
return c.json({ error: 'invalid-session-id' }, 400);
|
|
392
594
|
}
|
|
393
595
|
let body;
|
|
@@ -395,38 +597,38 @@ export function buildHttpApp(deps) {
|
|
|
395
597
|
body = await c.req.json();
|
|
396
598
|
}
|
|
397
599
|
catch {
|
|
398
|
-
timed(deps.logger, 'PATCH', `/${
|
|
600
|
+
timed(deps.logger, 'PATCH', `/${id}`, 400, Date.now() - start);
|
|
399
601
|
return c.json({ error: 'body-not-json' }, 400);
|
|
400
602
|
}
|
|
401
603
|
if (typeof body.displayName !== 'string' || body.displayName.length === 0 || body.displayName.length > DISPLAY_NAME_MAX) {
|
|
402
|
-
timed(deps.logger, 'PATCH', `/${
|
|
604
|
+
timed(deps.logger, 'PATCH', `/${id}`, 400, Date.now() - start);
|
|
403
605
|
return c.json({ error: 'invalid-displayName', detail: { maxLen: DISPLAY_NAME_MAX } }, 400);
|
|
404
606
|
}
|
|
405
|
-
const
|
|
406
|
-
if (!
|
|
407
|
-
timed(deps.logger, 'PATCH', `/${
|
|
607
|
+
const row = resolveRow(deps.watcher, id);
|
|
608
|
+
if (!row || !row.jsonlPath) {
|
|
609
|
+
timed(deps.logger, 'PATCH', `/${id}`, 404, Date.now() - start);
|
|
408
610
|
return c.json({ error: 'session-not-found' }, 404);
|
|
409
611
|
}
|
|
410
|
-
const sidecarPath = sidecarPathFor(
|
|
411
|
-
const prior = readSidecar(
|
|
612
|
+
const sidecarPath = sidecarPathFor(row.jsonlPath);
|
|
613
|
+
const prior = readSidecar(row.jsonlPath)?.displayName ?? null;
|
|
412
614
|
try {
|
|
413
615
|
writeFileSync(sidecarPath, JSON.stringify({ displayName: body.displayName }, null, 2) + '\n', 'utf8');
|
|
414
616
|
}
|
|
415
617
|
catch (err) {
|
|
416
618
|
const msg = err instanceof Error ? err.message : String(err);
|
|
417
|
-
deps.logger(`rename-failed sessionId=${sessionId.slice(0, 8)} message=${msg}`);
|
|
418
|
-
timed(deps.logger, 'PATCH', `/${
|
|
619
|
+
deps.logger(`rename-failed sessionId=${row.sessionId.slice(0, 8)} message=${msg}`);
|
|
620
|
+
timed(deps.logger, 'PATCH', `/${id}`, 500, Date.now() - start);
|
|
419
621
|
return c.json({ error: 'sidecar-write-failed', detail: msg }, 500);
|
|
420
622
|
}
|
|
421
|
-
deps.logger(`rename sessionId=${sessionId.slice(0, 8)} from=${prior ?? ''} to=${body.displayName}`);
|
|
422
|
-
timed(deps.logger, 'PATCH', `/${
|
|
623
|
+
deps.logger(`rename sessionId=${row.sessionId.slice(0, 8)} from=${prior ?? ''} to=${body.displayName}`);
|
|
624
|
+
timed(deps.logger, 'PATCH', `/${id}`, 200, Date.now() - start);
|
|
423
625
|
return c.json({ ok: true, displayName: body.displayName }, 200);
|
|
424
626
|
});
|
|
425
627
|
app.post('/:sessionId/archive', async (c) => {
|
|
426
628
|
const start = Date.now();
|
|
427
|
-
const
|
|
428
|
-
if (!SESSION_ID_PATTERN.test(
|
|
429
|
-
timed(deps.logger, 'POST', `/${
|
|
629
|
+
const id = c.req.param('sessionId');
|
|
630
|
+
if (!SESSION_ID_PATTERN.test(id)) {
|
|
631
|
+
timed(deps.logger, 'POST', `/${id}/archive`, 400, Date.now() - start);
|
|
430
632
|
return c.json({ error: 'invalid-session-id' }, 400);
|
|
431
633
|
}
|
|
432
634
|
let body;
|
|
@@ -434,30 +636,35 @@ export function buildHttpApp(deps) {
|
|
|
434
636
|
body = await c.req.json();
|
|
435
637
|
}
|
|
436
638
|
catch {
|
|
437
|
-
timed(deps.logger, 'POST', `/${
|
|
639
|
+
timed(deps.logger, 'POST', `/${id}/archive`, 400, Date.now() - start);
|
|
438
640
|
return c.json({ error: 'body-not-json' }, 400);
|
|
439
641
|
}
|
|
440
642
|
const mode = body.mode;
|
|
441
643
|
if (mode !== 'archive' && mode !== 'unarchive') {
|
|
442
|
-
timed(deps.logger, 'POST', `/${
|
|
644
|
+
timed(deps.logger, 'POST', `/${id}/archive`, 400, Date.now() - start);
|
|
443
645
|
return c.json({ error: 'invalid-mode', detail: { expected: ['archive', 'unarchive'] } }, 400);
|
|
444
646
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
timed(deps.logger, 'POST', `/${
|
|
647
|
+
const row = resolveRow(deps.watcher, id);
|
|
648
|
+
if (!row) {
|
|
649
|
+
timed(deps.logger, 'POST', `/${id}/archive`, 404, Date.now() - start);
|
|
650
|
+
return c.json({ error: 'session-not-found' }, 404);
|
|
651
|
+
}
|
|
652
|
+
if (deps.store.get(row.sessionId)) {
|
|
653
|
+
deps.logger(`archive-rejected sessionId=${row.sessionId.slice(0, 8)} reason=pty-still-alive`);
|
|
654
|
+
timed(deps.logger, 'POST', `/${id}/archive`, 409, Date.now() - start);
|
|
448
655
|
return c.json({ error: 'pty-still-alive', detail: 'end the session before archiving' }, 409);
|
|
449
656
|
}
|
|
450
657
|
const slug = projectSlugForCwd(deps.spawnCwd);
|
|
451
658
|
const slugDir = join(claudeStateRoot(), 'projects', slug);
|
|
452
659
|
const archiveDir = join(slugDir, 'archive');
|
|
453
|
-
const fileName = `${sessionId}.jsonl`;
|
|
454
|
-
const sidecarName = `${sessionId}.meta.json`;
|
|
660
|
+
const fileName = `${row.sessionId}.jsonl`;
|
|
661
|
+
const sidecarName = `${row.sessionId}.meta.json`;
|
|
455
662
|
const fromJsonl = mode === 'archive' ? join(slugDir, fileName) : join(archiveDir, fileName);
|
|
456
663
|
const toJsonl = mode === 'archive' ? join(archiveDir, fileName) : join(slugDir, fileName);
|
|
457
664
|
const fromSidecar = mode === 'archive' ? join(slugDir, sidecarName) : join(archiveDir, sidecarName);
|
|
458
665
|
const toSidecar = mode === 'archive' ? join(archiveDir, sidecarName) : join(slugDir, sidecarName);
|
|
459
666
|
if (!existsSync(fromJsonl)) {
|
|
460
|
-
timed(deps.logger, 'POST', `/${
|
|
667
|
+
timed(deps.logger, 'POST', `/${id}/archive`, 404, Date.now() - start);
|
|
461
668
|
return c.json({ error: 'session-not-found' }, 404);
|
|
462
669
|
}
|
|
463
670
|
try {
|
|
@@ -469,49 +676,63 @@ export function buildHttpApp(deps) {
|
|
|
469
676
|
}
|
|
470
677
|
catch (err) {
|
|
471
678
|
const msg = err instanceof Error ? err.message : String(err);
|
|
472
|
-
deps.logger(`archive-failed sessionId=${sessionId.slice(0, 8)} mode=${mode} message=${msg}`);
|
|
473
|
-
timed(deps.logger, 'POST', `/${
|
|
679
|
+
deps.logger(`archive-failed sessionId=${row.sessionId.slice(0, 8)} mode=${mode} message=${msg}`);
|
|
680
|
+
timed(deps.logger, 'POST', `/${id}/archive`, 500, Date.now() - start);
|
|
474
681
|
return c.json({ error: 'archive-failed', detail: msg }, 500);
|
|
475
682
|
}
|
|
476
|
-
deps.logger(`archive sessionId=${sessionId.slice(0, 8)} from=${basename(fromJsonl)} to=${join(mode === 'archive' ? 'archive' : '.', basename(toJsonl))}`);
|
|
477
|
-
timed(deps.logger, 'POST', `/${
|
|
683
|
+
deps.logger(`archive sessionId=${row.sessionId.slice(0, 8)} from=${basename(fromJsonl)} to=${join(mode === 'archive' ? 'archive' : '.', basename(toJsonl))}`);
|
|
684
|
+
timed(deps.logger, 'POST', `/${id}/archive`, 200, Date.now() - start);
|
|
478
685
|
return c.json({ ok: true, mode }, 200);
|
|
479
686
|
});
|
|
480
|
-
|
|
687
|
+
// SIGTERM-only stop. Idempotent on already-dead PTYs. JSONL untouched.
|
|
688
|
+
app.post('/:sessionId/stop', async (c) => {
|
|
481
689
|
const start = Date.now();
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
timed(deps.logger, 'DELETE', `/${sessionId}`, 400, Date.now() - start);
|
|
690
|
+
const id = c.req.param('sessionId');
|
|
691
|
+
if (!SESSION_ID_PATTERN.test(id) && !/^\d+$/.test(id)) {
|
|
692
|
+
timed(deps.logger, 'POST', `/${id}/stop`, 400, Date.now() - start);
|
|
486
693
|
return c.json({ error: 'invalid-session-id' }, 400);
|
|
487
694
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
timed(deps.logger, '
|
|
491
|
-
return c.
|
|
695
|
+
const row = resolveRow(deps.watcher, id);
|
|
696
|
+
if (!row && !deps.store.get(id)) {
|
|
697
|
+
timed(deps.logger, 'POST', `/${id}/stop`, 404, Date.now() - start);
|
|
698
|
+
return c.json({ error: 'session-not-found' }, 404);
|
|
699
|
+
}
|
|
700
|
+
const sessionId = row?.sessionId ?? id;
|
|
701
|
+
await stopSession({ store: deps.store, killGraceMs: deps.killGraceMs, logger: deps.logger }, sessionId);
|
|
702
|
+
timed(deps.logger, 'POST', `/${sessionId}/stop`, 204, Date.now() - start);
|
|
703
|
+
return c.body(null, 204);
|
|
704
|
+
});
|
|
705
|
+
// Hard-remove transcript. 409 if PTY is still alive (stop first).
|
|
706
|
+
app.delete('/:sessionId', async (c) => {
|
|
707
|
+
const start = Date.now();
|
|
708
|
+
const id = c.req.param('sessionId');
|
|
709
|
+
if (!SESSION_ID_PATTERN.test(id) && !/^\d+$/.test(id)) {
|
|
710
|
+
timed(deps.logger, 'DELETE', `/${id}`, 400, Date.now() - start);
|
|
711
|
+
return c.json({ error: 'invalid-session-id' }, 400);
|
|
492
712
|
}
|
|
713
|
+
const row = resolveRow(deps.watcher, id);
|
|
714
|
+
const sessionId = row?.sessionId ?? id;
|
|
493
715
|
if (deps.store.get(sessionId)) {
|
|
494
|
-
deps.logger(`
|
|
495
|
-
timed(deps.logger, 'DELETE', `/${
|
|
496
|
-
return c.json({ error: 'pty-still-alive', detail: '
|
|
716
|
+
deps.logger(`delete-rejected sessionId=${sessionId.slice(0, 8)} reason=pty-still-alive`);
|
|
717
|
+
timed(deps.logger, 'DELETE', `/${id}`, 409, Date.now() - start);
|
|
718
|
+
return c.json({ error: 'pty-still-alive', detail: 'stop the session before deleting' }, 409);
|
|
497
719
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
timed(deps.logger, 'DELETE', `/${sessionId}?purge=1`, 404, Date.now() - start);
|
|
720
|
+
if (!row || !row.jsonlPath) {
|
|
721
|
+
timed(deps.logger, 'DELETE', `/${id}`, 404, Date.now() - start);
|
|
501
722
|
return c.json({ error: 'session-not-found' }, 404);
|
|
502
723
|
}
|
|
503
|
-
const sidecarPath = sidecarPathFor(
|
|
724
|
+
const sidecarPath = sidecarPathFor(row.jsonlPath);
|
|
504
725
|
const subdir = join(claudeStateRoot(), 'projects', projectSlugForCwd(deps.spawnCwd), sessionId);
|
|
505
726
|
let bytes = 0;
|
|
506
727
|
try {
|
|
507
|
-
bytes = existsSync(
|
|
728
|
+
bytes = existsSync(row.jsonlPath) ? statSync(row.jsonlPath).size : 0;
|
|
508
729
|
}
|
|
509
730
|
catch {
|
|
510
|
-
//
|
|
731
|
+
// size is informational
|
|
511
732
|
}
|
|
512
733
|
try {
|
|
513
|
-
if (existsSync(
|
|
514
|
-
rmSync(
|
|
734
|
+
if (existsSync(row.jsonlPath))
|
|
735
|
+
rmSync(row.jsonlPath);
|
|
515
736
|
if (existsSync(sidecarPath))
|
|
516
737
|
rmSync(sidecarPath);
|
|
517
738
|
if (existsSync(subdir))
|
|
@@ -519,24 +740,26 @@ export function buildHttpApp(deps) {
|
|
|
519
740
|
}
|
|
520
741
|
catch (err) {
|
|
521
742
|
const msg = err instanceof Error ? err.message : String(err);
|
|
522
|
-
deps.logger(`
|
|
523
|
-
timed(deps.logger, 'DELETE', `/${
|
|
524
|
-
return c.json({ error: '
|
|
743
|
+
deps.logger(`delete-failed sessionId=${sessionId.slice(0, 8)} message=${msg}`);
|
|
744
|
+
timed(deps.logger, 'DELETE', `/${id}`, 500, Date.now() - start);
|
|
745
|
+
return c.json({ error: 'delete-failed', detail: msg }, 500);
|
|
525
746
|
}
|
|
526
747
|
deps.logger(`purge sessionId=${sessionId.slice(0, 8)} bytes=${bytes}`);
|
|
527
|
-
timed(deps.logger, 'DELETE', `/${
|
|
748
|
+
timed(deps.logger, 'DELETE', `/${id}`, 204, Date.now() - start);
|
|
528
749
|
return c.body(null, 204);
|
|
529
750
|
});
|
|
530
751
|
app.post('/:sessionId/input', async (c) => {
|
|
531
752
|
const start = Date.now();
|
|
532
|
-
const
|
|
753
|
+
const id = c.req.param('sessionId');
|
|
754
|
+
const row = resolveRow(deps.watcher, id);
|
|
755
|
+
const sessionId = row?.sessionId ?? id;
|
|
533
756
|
const s = deps.store.get(sessionId);
|
|
534
757
|
if (!s) {
|
|
535
|
-
timed(deps.logger, 'POST', `/${
|
|
758
|
+
timed(deps.logger, 'POST', `/${id}/input`, 404, Date.now() - start);
|
|
536
759
|
return c.json({ error: 'session-not-found' }, 404);
|
|
537
760
|
}
|
|
538
761
|
if (s.exited) {
|
|
539
|
-
timed(deps.logger, 'POST', `/${
|
|
762
|
+
timed(deps.logger, 'POST', `/${id}/input`, 410, Date.now() - start);
|
|
540
763
|
return c.json({ error: 'session-exited' }, 410);
|
|
541
764
|
}
|
|
542
765
|
let body;
|
|
@@ -544,11 +767,11 @@ export function buildHttpApp(deps) {
|
|
|
544
767
|
body = await c.req.json();
|
|
545
768
|
}
|
|
546
769
|
catch {
|
|
547
|
-
timed(deps.logger, 'POST', `/${
|
|
770
|
+
timed(deps.logger, 'POST', `/${id}/input`, 400, Date.now() - start);
|
|
548
771
|
return c.json({ error: 'body-not-json' }, 400);
|
|
549
772
|
}
|
|
550
773
|
if (typeof body.text !== 'string') {
|
|
551
|
-
timed(deps.logger, 'POST', `/${
|
|
774
|
+
timed(deps.logger, 'POST', `/${id}/input`, 400, Date.now() - start);
|
|
552
775
|
return c.json({ error: 'text-required' }, 400);
|
|
553
776
|
}
|
|
554
777
|
const bytes = Buffer.byteLength(body.text, 'utf8');
|
|
@@ -558,26 +781,25 @@ export function buildHttpApp(deps) {
|
|
|
558
781
|
catch (err) {
|
|
559
782
|
const msg = err instanceof Error ? err.message : String(err);
|
|
560
783
|
deps.logger(`input-failed pid=${s.pid} sessionId=${sessionId} message=${msg}`);
|
|
561
|
-
timed(deps.logger, 'POST', `/${
|
|
784
|
+
timed(deps.logger, 'POST', `/${id}/input`, 500, Date.now() - start);
|
|
562
785
|
return c.json({ error: 'pty-write-failed', detail: msg }, 500);
|
|
563
786
|
}
|
|
564
787
|
deps.logger(`input pid=${s.pid} sessionId=${sessionId} bytes=${bytes} channel=${s.channel}`);
|
|
565
|
-
timed(deps.logger, 'POST', `/${
|
|
788
|
+
timed(deps.logger, 'POST', `/${id}/input`, 200, Date.now() - start);
|
|
566
789
|
return c.json({ ok: true, bytes }, 200);
|
|
567
790
|
});
|
|
568
791
|
app.get('/:sessionId/log', (c) => {
|
|
569
792
|
const start = Date.now();
|
|
570
|
-
const
|
|
793
|
+
const id = c.req.param('sessionId');
|
|
794
|
+
const row = resolveRow(deps.watcher, id);
|
|
795
|
+
const sessionId = row?.sessionId ?? id;
|
|
571
796
|
const s = deps.store.get(sessionId);
|
|
572
797
|
if (!s) {
|
|
573
|
-
timed(deps.logger, 'GET', `/${
|
|
798
|
+
timed(deps.logger, 'GET', `/${id}/log`, 404, Date.now() - start);
|
|
574
799
|
return c.json({ error: 'session-not-found' }, 404);
|
|
575
800
|
}
|
|
576
801
|
if (s.jsonlPath === null) {
|
|
577
|
-
|
|
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);
|
|
802
|
+
timed(deps.logger, 'GET', `/${id}/log`, 202, Date.now() - start);
|
|
581
803
|
return c.json({ jsonlPath: null, exists: false, sizeBytes: 0, pending: true }, 202);
|
|
582
804
|
}
|
|
583
805
|
const jsonlPath = s.jsonlPath;
|
|
@@ -585,10 +807,10 @@ export function buildHttpApp(deps) {
|
|
|
585
807
|
if (!follow) {
|
|
586
808
|
const exists = existsSync(jsonlPath);
|
|
587
809
|
const sizeBytes = exists ? statSync(jsonlPath).size : 0;
|
|
588
|
-
timed(deps.logger, 'GET', `/${
|
|
810
|
+
timed(deps.logger, 'GET', `/${id}/log`, 200, Date.now() - start);
|
|
589
811
|
return c.json({ jsonlPath, exists, sizeBytes }, 200);
|
|
590
812
|
}
|
|
591
|
-
timed(deps.logger, 'GET', `/${
|
|
813
|
+
timed(deps.logger, 'GET', `/${id}/log?follow=1`, 200, Date.now() - start);
|
|
592
814
|
return stream(c, async (out) => {
|
|
593
815
|
let position = 0;
|
|
594
816
|
if (existsSync(jsonlPath)) {
|