@arcreflex/agent-transcripts 0.1.5 → 0.1.9
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/.github/workflows/publish.yml +5 -0
- package/CLAUDE.md +4 -0
- package/README.md +70 -17
- package/bun.lock +89 -0
- package/package.json +3 -2
- package/src/adapters/claude-code.ts +300 -33
- package/src/cache.ts +129 -0
- package/src/cli.ts +95 -68
- package/src/convert.ts +82 -42
- package/src/parse.ts +7 -101
- package/src/render-html.ts +1096 -0
- package/src/render-index.ts +611 -0
- package/src/render.ts +7 -194
- package/src/serve.ts +308 -0
- package/src/sync.ts +211 -98
- package/src/title.ts +172 -0
- package/src/types.ts +18 -2
- package/src/utils/html.ts +12 -0
- package/src/utils/naming.ts +30 -143
- package/src/utils/openrouter.ts +116 -0
- package/src/utils/provenance.ts +167 -69
- package/src/utils/tree.ts +116 -0
- package/test/fixtures/claude/non-message-parents.input.jsonl +9 -0
- package/test/fixtures/claude/non-message-parents.output.md +30 -0
- package/test/snapshots.test.ts +39 -33
package/src/render.ts
CHANGED
|
@@ -1,46 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Render
|
|
2
|
+
* Render: intermediate transcript format → markdown
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { basename, dirname, join } from "path";
|
|
6
|
-
import { mkdir } from "fs/promises";
|
|
7
5
|
import type { Transcript, Message, ToolCall } from "./types.ts";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Read transcript from file or stdin.
|
|
17
|
-
*/
|
|
18
|
-
async function readTranscript(
|
|
19
|
-
input: string,
|
|
20
|
-
): Promise<{ transcript: Transcript; path: string }> {
|
|
21
|
-
let content: string;
|
|
22
|
-
let path: string;
|
|
23
|
-
|
|
24
|
-
if (input !== "-") {
|
|
25
|
-
content = await Bun.file(input).text();
|
|
26
|
-
path = input;
|
|
27
|
-
} else {
|
|
28
|
-
const chunks: string[] = [];
|
|
29
|
-
const reader = Bun.stdin.stream().getReader();
|
|
30
|
-
|
|
31
|
-
while (true) {
|
|
32
|
-
const { done, value } = await reader.read();
|
|
33
|
-
if (done) break;
|
|
34
|
-
chunks.push(new TextDecoder().decode(value));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
content = chunks.join("");
|
|
38
|
-
path = "<stdin>";
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const transcript = JSON.parse(content) as Transcript;
|
|
42
|
-
return { transcript, path };
|
|
43
|
-
}
|
|
6
|
+
import {
|
|
7
|
+
buildTree,
|
|
8
|
+
findLatestLeaf,
|
|
9
|
+
tracePath,
|
|
10
|
+
getFirstLine,
|
|
11
|
+
} from "./utils/tree.ts";
|
|
44
12
|
|
|
45
13
|
/**
|
|
46
14
|
* Format a single tool call.
|
|
@@ -103,116 +71,6 @@ ${msg.thinking}
|
|
|
103
71
|
}
|
|
104
72
|
}
|
|
105
73
|
|
|
106
|
-
/**
|
|
107
|
-
* Get first line of message content for branch reference.
|
|
108
|
-
*/
|
|
109
|
-
function getFirstLine(msg: Message): string {
|
|
110
|
-
let text: string;
|
|
111
|
-
switch (msg.type) {
|
|
112
|
-
case "user":
|
|
113
|
-
case "assistant":
|
|
114
|
-
case "system":
|
|
115
|
-
case "error":
|
|
116
|
-
text = msg.content;
|
|
117
|
-
break;
|
|
118
|
-
case "tool_calls":
|
|
119
|
-
text = msg.calls.map((c) => c.name).join(", ");
|
|
120
|
-
break;
|
|
121
|
-
default:
|
|
122
|
-
text = "";
|
|
123
|
-
}
|
|
124
|
-
const firstLine = text.split("\n")[0].trim();
|
|
125
|
-
const maxLen = 60;
|
|
126
|
-
return firstLine.length > maxLen
|
|
127
|
-
? firstLine.slice(0, maxLen) + "..."
|
|
128
|
-
: firstLine;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Build tree structure from messages.
|
|
133
|
-
* Returns maps for navigation and the messages grouped by sourceRef.
|
|
134
|
-
*/
|
|
135
|
-
function buildTree(messages: Message[]): {
|
|
136
|
-
bySourceRef: Map<string, Message[]>;
|
|
137
|
-
children: Map<string, Set<string>>;
|
|
138
|
-
parents: Map<string, string>;
|
|
139
|
-
roots: string[];
|
|
140
|
-
} {
|
|
141
|
-
// Group messages by sourceRef
|
|
142
|
-
const bySourceRef = new Map<string, Message[]>();
|
|
143
|
-
for (const msg of messages) {
|
|
144
|
-
const existing = bySourceRef.get(msg.sourceRef) || [];
|
|
145
|
-
existing.push(msg);
|
|
146
|
-
bySourceRef.set(msg.sourceRef, existing);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Build parent → children map (at sourceRef level)
|
|
150
|
-
const children = new Map<string, Set<string>>();
|
|
151
|
-
const parents = new Map<string, string>();
|
|
152
|
-
|
|
153
|
-
for (const msg of messages) {
|
|
154
|
-
if (msg.parentMessageRef && bySourceRef.has(msg.parentMessageRef)) {
|
|
155
|
-
parents.set(msg.sourceRef, msg.parentMessageRef);
|
|
156
|
-
const existing = children.get(msg.parentMessageRef) || new Set();
|
|
157
|
-
existing.add(msg.sourceRef);
|
|
158
|
-
children.set(msg.parentMessageRef, existing);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Find roots (no parent in our set)
|
|
163
|
-
const roots: string[] = [];
|
|
164
|
-
for (const sourceRef of bySourceRef.keys()) {
|
|
165
|
-
if (!parents.has(sourceRef)) {
|
|
166
|
-
roots.push(sourceRef);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return { bySourceRef, children, parents, roots };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Find the latest leaf in the tree (for primary branch).
|
|
175
|
-
*/
|
|
176
|
-
function findLatestLeaf(
|
|
177
|
-
bySourceRef: Map<string, Message[]>,
|
|
178
|
-
children: Map<string, Set<string>>,
|
|
179
|
-
): string | undefined {
|
|
180
|
-
let latestLeaf: string | undefined;
|
|
181
|
-
let latestTime = 0;
|
|
182
|
-
|
|
183
|
-
for (const sourceRef of bySourceRef.keys()) {
|
|
184
|
-
const childSet = children.get(sourceRef);
|
|
185
|
-
if (!childSet || childSet.size === 0) {
|
|
186
|
-
// It's a leaf
|
|
187
|
-
const msgs = bySourceRef.get(sourceRef);
|
|
188
|
-
if (msgs && msgs.length > 0) {
|
|
189
|
-
const time = new Date(msgs[0].timestamp).getTime();
|
|
190
|
-
if (time > latestTime) {
|
|
191
|
-
latestTime = time;
|
|
192
|
-
latestLeaf = sourceRef;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return latestLeaf;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Trace path from root to target.
|
|
203
|
-
*/
|
|
204
|
-
function tracePath(target: string, parents: Map<string, string>): string[] {
|
|
205
|
-
const path: string[] = [];
|
|
206
|
-
let current: string | undefined = target;
|
|
207
|
-
|
|
208
|
-
while (current) {
|
|
209
|
-
path.unshift(current);
|
|
210
|
-
current = parents.get(current);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return path;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
74
|
export interface RenderTranscriptOptions {
|
|
217
75
|
head?: string; // render branch ending at this message ID
|
|
218
76
|
sourcePath?: string; // absolute source path for front matter provenance
|
|
@@ -333,48 +191,3 @@ export function renderTranscript(
|
|
|
333
191
|
|
|
334
192
|
return lines.join("\n");
|
|
335
193
|
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Determine output path for markdown.
|
|
339
|
-
*/
|
|
340
|
-
function getOutputPath(inputPath: string, outputOption?: string): string {
|
|
341
|
-
if (outputOption) {
|
|
342
|
-
// If it has an extension, use as-is
|
|
343
|
-
if (outputOption.match(/\.\w+$/)) {
|
|
344
|
-
return outputOption;
|
|
345
|
-
}
|
|
346
|
-
// Treat as directory
|
|
347
|
-
const base =
|
|
348
|
-
inputPath === "<stdin>"
|
|
349
|
-
? "transcript"
|
|
350
|
-
: basename(inputPath).replace(/\.json$/, "");
|
|
351
|
-
return join(outputOption, `${base}.md`);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Default: same name in cwd
|
|
355
|
-
const base =
|
|
356
|
-
inputPath === "<stdin>"
|
|
357
|
-
? "transcript"
|
|
358
|
-
: basename(inputPath).replace(/\.json$/, "");
|
|
359
|
-
return join(process.cwd(), `${base}.md`);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Render intermediate JSON to markdown.
|
|
364
|
-
*/
|
|
365
|
-
export async function render(options: RenderOptions): Promise<void> {
|
|
366
|
-
const { transcript, path: inputPath } = await readTranscript(options.input);
|
|
367
|
-
|
|
368
|
-
const markdown = renderTranscript(transcript, options.head);
|
|
369
|
-
|
|
370
|
-
if (options.output) {
|
|
371
|
-
const outputPath = getOutputPath(inputPath, options.output);
|
|
372
|
-
// Ensure directory exists
|
|
373
|
-
await mkdir(dirname(outputPath), { recursive: true });
|
|
374
|
-
await Bun.write(outputPath, markdown);
|
|
375
|
-
console.error(`Wrote: ${outputPath}`);
|
|
376
|
-
} else {
|
|
377
|
-
// Default: print to stdout
|
|
378
|
-
console.log(markdown);
|
|
379
|
-
}
|
|
380
|
-
}
|
package/src/serve.ts
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serve command: dynamic HTTP server for transcripts.
|
|
3
|
+
*
|
|
4
|
+
* Serves transcripts directly from source files, using the cache
|
|
5
|
+
* for rendered HTML and titles. No output directory needed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getAdapters } from "./adapters/index.ts";
|
|
9
|
+
import type { Adapter, DiscoveredSession, Transcript } from "./types.ts";
|
|
10
|
+
import { renderTranscriptHtml } from "./render-html.ts";
|
|
11
|
+
import { renderIndexFromSessions, type SessionEntry } from "./render-index.ts";
|
|
12
|
+
import { generateOutputName } from "./utils/naming.ts";
|
|
13
|
+
import { extractFirstUserMessage } from "./utils/provenance.ts";
|
|
14
|
+
import {
|
|
15
|
+
computeContentHash,
|
|
16
|
+
loadCache,
|
|
17
|
+
saveCache,
|
|
18
|
+
getCachedSegments,
|
|
19
|
+
type CacheEntry,
|
|
20
|
+
} from "./cache.ts";
|
|
21
|
+
|
|
22
|
+
export interface ServeOptions {
|
|
23
|
+
source: string;
|
|
24
|
+
port?: number;
|
|
25
|
+
quiet?: boolean;
|
|
26
|
+
noCache?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface SessionInfo {
|
|
30
|
+
source: DiscoveredSession;
|
|
31
|
+
adapter: Adapter;
|
|
32
|
+
baseName: string; // e.g., "2024-01-15-1423-sessionid"
|
|
33
|
+
segmentIndex: number; // 0-indexed segment within multi-transcript source
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Discover sessions and build URL mapping.
|
|
38
|
+
* Returns map of baseName → session info.
|
|
39
|
+
*/
|
|
40
|
+
async function discoverSessions(
|
|
41
|
+
sourceDir: string,
|
|
42
|
+
): Promise<Map<string, SessionInfo[]>> {
|
|
43
|
+
const sessions = new Map<string, SessionInfo[]>();
|
|
44
|
+
|
|
45
|
+
for (const adapter of getAdapters()) {
|
|
46
|
+
const discovered = await adapter.discover(sourceDir);
|
|
47
|
+
for (const source of discovered) {
|
|
48
|
+
// Parse to get transcript info for naming
|
|
49
|
+
// We need to parse once to determine the output name
|
|
50
|
+
const content = await Bun.file(source.path).text();
|
|
51
|
+
const transcripts = adapter.parse(content, source.path);
|
|
52
|
+
|
|
53
|
+
for (let i = 0; i < transcripts.length; i++) {
|
|
54
|
+
const transcript = transcripts[i];
|
|
55
|
+
const baseName = generateOutputName(transcript, source.path);
|
|
56
|
+
const suffix = transcripts.length > 1 ? `_${i + 1}` : "";
|
|
57
|
+
const fullName = `${baseName}${suffix}`;
|
|
58
|
+
|
|
59
|
+
const info: SessionInfo = {
|
|
60
|
+
source,
|
|
61
|
+
adapter,
|
|
62
|
+
baseName: fullName,
|
|
63
|
+
segmentIndex: i,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Store by baseName for lookup
|
|
67
|
+
const existing = sessions.get(fullName) || [];
|
|
68
|
+
existing.push(info);
|
|
69
|
+
sessions.set(fullName, existing);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return sessions;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get or render HTML for a session.
|
|
79
|
+
* Uses cache if available and content unchanged (unless noCache is true).
|
|
80
|
+
*/
|
|
81
|
+
async function getSessionHtml(
|
|
82
|
+
session: SessionInfo,
|
|
83
|
+
segmentIndex: number,
|
|
84
|
+
noCache = false,
|
|
85
|
+
): Promise<{
|
|
86
|
+
html: string;
|
|
87
|
+
transcript: Transcript;
|
|
88
|
+
contentHash: string;
|
|
89
|
+
} | null> {
|
|
90
|
+
const content = await Bun.file(session.source.path).text();
|
|
91
|
+
const contentHash = computeContentHash(content);
|
|
92
|
+
|
|
93
|
+
// Parse first to validate segment index
|
|
94
|
+
const transcripts = session.adapter.parse(content, session.source.path);
|
|
95
|
+
if (segmentIndex < 0 || segmentIndex >= transcripts.length) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const transcript = transcripts[segmentIndex];
|
|
99
|
+
|
|
100
|
+
// Check cache (unless bypassing for dev)
|
|
101
|
+
const cached = await loadCache(session.source.path);
|
|
102
|
+
if (!noCache) {
|
|
103
|
+
const cachedSegments = getCachedSegments(cached, contentHash, "html");
|
|
104
|
+
const cachedHtml = cachedSegments?.[segmentIndex]?.html;
|
|
105
|
+
if (cachedHtml) {
|
|
106
|
+
return { html: cachedHtml, transcript, contentHash };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Still use cached title if available
|
|
111
|
+
const title =
|
|
112
|
+
cached?.contentHash === contentHash
|
|
113
|
+
? cached.segments[segmentIndex]?.title
|
|
114
|
+
: undefined;
|
|
115
|
+
|
|
116
|
+
const html = await renderTranscriptHtml(transcript, { title });
|
|
117
|
+
|
|
118
|
+
// Update cache (even in noCache mode, for titles)
|
|
119
|
+
if (!noCache) {
|
|
120
|
+
// Deep copy segments to avoid mutating cached objects
|
|
121
|
+
const newCache: CacheEntry = {
|
|
122
|
+
contentHash,
|
|
123
|
+
segments:
|
|
124
|
+
cached?.contentHash === contentHash
|
|
125
|
+
? cached.segments.map((s) => ({ ...s }))
|
|
126
|
+
: [],
|
|
127
|
+
};
|
|
128
|
+
while (newCache.segments.length <= segmentIndex) {
|
|
129
|
+
newCache.segments.push({});
|
|
130
|
+
}
|
|
131
|
+
newCache.segments[segmentIndex].html = html;
|
|
132
|
+
if (title) {
|
|
133
|
+
newCache.segments[segmentIndex].title = title;
|
|
134
|
+
}
|
|
135
|
+
await saveCache(session.source.path, newCache);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { html, transcript, contentHash };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Build session entries for index page.
|
|
143
|
+
*/
|
|
144
|
+
async function buildIndexEntries(
|
|
145
|
+
sessions: Map<string, SessionInfo[]>,
|
|
146
|
+
): Promise<SessionEntry[]> {
|
|
147
|
+
const entries: SessionEntry[] = [];
|
|
148
|
+
|
|
149
|
+
for (const [baseName, infos] of sessions) {
|
|
150
|
+
for (const info of infos) {
|
|
151
|
+
const { segmentIndex } = info;
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const content = await Bun.file(info.source.path).text();
|
|
155
|
+
const contentHash = computeContentHash(content);
|
|
156
|
+
const transcripts = info.adapter.parse(content, info.source.path);
|
|
157
|
+
const transcript = transcripts[segmentIndex];
|
|
158
|
+
|
|
159
|
+
if (!transcript) continue;
|
|
160
|
+
|
|
161
|
+
// Get cached title
|
|
162
|
+
const cached = await loadCache(info.source.path);
|
|
163
|
+
const title =
|
|
164
|
+
cached?.contentHash === contentHash
|
|
165
|
+
? cached.segments[segmentIndex]?.title
|
|
166
|
+
: undefined;
|
|
167
|
+
|
|
168
|
+
const firstUserMessage = extractFirstUserMessage(transcript);
|
|
169
|
+
const { messageCount, startTime, endTime, cwd } = transcript.metadata;
|
|
170
|
+
|
|
171
|
+
entries.push({
|
|
172
|
+
filename: `${baseName}.html`,
|
|
173
|
+
title:
|
|
174
|
+
title ||
|
|
175
|
+
(firstUserMessage.length > 80
|
|
176
|
+
? firstUserMessage.slice(0, 80) + "..."
|
|
177
|
+
: firstUserMessage) ||
|
|
178
|
+
baseName,
|
|
179
|
+
firstUserMessage,
|
|
180
|
+
date: startTime,
|
|
181
|
+
endDate: endTime,
|
|
182
|
+
messageCount,
|
|
183
|
+
cwd,
|
|
184
|
+
});
|
|
185
|
+
} catch {
|
|
186
|
+
// Skip sessions that fail to parse
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return entries;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Start the HTTP server.
|
|
196
|
+
*/
|
|
197
|
+
export async function serve(options: ServeOptions): Promise<void> {
|
|
198
|
+
const { source, port = 3000, quiet = false, noCache = false } = options;
|
|
199
|
+
|
|
200
|
+
if (!quiet) {
|
|
201
|
+
console.error(`Discovering sessions in ${source}...`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Discover sessions on startup
|
|
205
|
+
const sessions = await discoverSessions(source);
|
|
206
|
+
|
|
207
|
+
if (!quiet) {
|
|
208
|
+
console.error(`Found ${sessions.size} session(s)`);
|
|
209
|
+
console.error(`Starting server at http://localhost:${port}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const server = Bun.serve({
|
|
213
|
+
port,
|
|
214
|
+
async fetch(req) {
|
|
215
|
+
const url = new URL(req.url);
|
|
216
|
+
const path = url.pathname;
|
|
217
|
+
|
|
218
|
+
// Log request
|
|
219
|
+
if (!quiet) {
|
|
220
|
+
console.error(`${req.method} ${path}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Index page
|
|
224
|
+
if (path === "/" || path === "/index.html") {
|
|
225
|
+
const entries = await buildIndexEntries(sessions);
|
|
226
|
+
const html = renderIndexFromSessions(entries);
|
|
227
|
+
return new Response(html, {
|
|
228
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Session page
|
|
233
|
+
if (path.endsWith(".html")) {
|
|
234
|
+
const baseName = path.slice(1, -5); // Remove leading "/" and ".html"
|
|
235
|
+
|
|
236
|
+
// First try exact match
|
|
237
|
+
const exactInfos = sessions.get(baseName);
|
|
238
|
+
if (exactInfos && exactInfos.length > 0) {
|
|
239
|
+
try {
|
|
240
|
+
const info = exactInfos[0];
|
|
241
|
+
const result = await getSessionHtml(
|
|
242
|
+
info,
|
|
243
|
+
info.segmentIndex,
|
|
244
|
+
noCache,
|
|
245
|
+
);
|
|
246
|
+
if (!result) {
|
|
247
|
+
return new Response("Not Found", { status: 404 });
|
|
248
|
+
}
|
|
249
|
+
return new Response(result.html, {
|
|
250
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
251
|
+
});
|
|
252
|
+
} catch (error) {
|
|
253
|
+
const message =
|
|
254
|
+
error instanceof Error ? error.message : String(error);
|
|
255
|
+
return new Response(`Error: ${message}`, { status: 500 });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Fallback: parse segment suffix (e.g., "name_2" → base "name" + segment 1)
|
|
260
|
+
// Handles case where URL has suffix but session was stored without it
|
|
261
|
+
const segmentMatch = baseName.match(/^(.+)_(\d+)$/);
|
|
262
|
+
if (segmentMatch) {
|
|
263
|
+
const lookupName = segmentMatch[1];
|
|
264
|
+
const segmentIndex = parseInt(segmentMatch[2], 10) - 1;
|
|
265
|
+
const infos = sessions.get(lookupName);
|
|
266
|
+
if (infos && infos.length > 0 && segmentIndex >= 0) {
|
|
267
|
+
try {
|
|
268
|
+
const result = await getSessionHtml(
|
|
269
|
+
infos[0],
|
|
270
|
+
segmentIndex,
|
|
271
|
+
noCache,
|
|
272
|
+
);
|
|
273
|
+
if (!result) {
|
|
274
|
+
return new Response("Not Found", { status: 404 });
|
|
275
|
+
}
|
|
276
|
+
const { html } = result;
|
|
277
|
+
return new Response(html, {
|
|
278
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
279
|
+
});
|
|
280
|
+
} catch (error) {
|
|
281
|
+
const message =
|
|
282
|
+
error instanceof Error ? error.message : String(error);
|
|
283
|
+
return new Response(`Error: ${message}`, { status: 500 });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return new Response("Not Found", { status: 404 });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return new Response("Not Found", { status: 404 });
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Keep server running
|
|
296
|
+
if (!quiet) {
|
|
297
|
+
console.error(`\nPress Ctrl+C to stop`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Handle shutdown gracefully
|
|
301
|
+
process.on("SIGINT", () => {
|
|
302
|
+
if (!quiet) {
|
|
303
|
+
console.error("\nShutting down...");
|
|
304
|
+
}
|
|
305
|
+
server.stop();
|
|
306
|
+
process.exit(0);
|
|
307
|
+
});
|
|
308
|
+
}
|