@arcreflex/agent-transcripts 0.1.10 → 0.1.12
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/CLAUDE.md +3 -1
- package/README.md +60 -53
- package/package.json +1 -1
- package/src/adapters/claude-code.ts +1 -0
- package/src/adapters/index.ts +0 -6
- package/src/archive.ts +267 -0
- package/src/cli.ts +96 -63
- package/src/convert.ts +19 -86
- package/src/parse.ts +0 -3
- package/src/render-html.ts +38 -195
- package/src/render-index.ts +15 -178
- package/src/render.ts +25 -88
- package/src/serve.ts +124 -215
- package/src/title.ts +24 -102
- package/src/types.ts +3 -0
- package/src/utils/naming.ts +8 -13
- package/src/utils/summary.ts +1 -4
- package/src/utils/text.ts +5 -0
- package/src/utils/theme.ts +152 -0
- package/src/utils/tree.ts +85 -1
- package/src/watch.ts +111 -0
- package/test/archive.test.ts +264 -0
- package/test/fixtures/claude/branching.input.jsonl +6 -0
- package/test/fixtures/claude/branching.output.md +25 -0
- package/test/naming.test.ts +98 -0
- package/test/summary.test.ts +144 -0
- package/test/tree.test.ts +217 -0
- package/tsconfig.json +1 -1
- package/src/cache.ts +0 -129
- package/src/sync.ts +0 -295
- package/src/utils/provenance.ts +0 -212
package/src/render.ts
CHANGED
|
@@ -3,16 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { Transcript, Message, ToolCall } from "./types.ts";
|
|
6
|
-
import {
|
|
7
|
-
buildTree,
|
|
8
|
-
findLatestLeaf,
|
|
9
|
-
tracePath,
|
|
10
|
-
getFirstLine,
|
|
11
|
-
} from "./utils/tree.ts";
|
|
6
|
+
import { walkTranscriptTree } from "./utils/tree.ts";
|
|
12
7
|
|
|
13
|
-
/**
|
|
14
|
-
* Format a single tool call.
|
|
15
|
-
*/
|
|
16
8
|
function formatToolCall(call: ToolCall): string {
|
|
17
9
|
if (call.summary) {
|
|
18
10
|
return `${call.name} \`${call.summary}\``;
|
|
@@ -20,9 +12,6 @@ function formatToolCall(call: ToolCall): string {
|
|
|
20
12
|
return call.name;
|
|
21
13
|
}
|
|
22
14
|
|
|
23
|
-
/**
|
|
24
|
-
* Format tool calls group.
|
|
25
|
-
*/
|
|
26
15
|
function formatToolCalls(calls: ToolCall[]): string {
|
|
27
16
|
if (calls.length === 1) {
|
|
28
17
|
return `**Tool**: ${formatToolCall(calls[0])}`;
|
|
@@ -30,9 +19,6 @@ function formatToolCalls(calls: ToolCall[]): string {
|
|
|
30
19
|
return `**Tools**:\n${calls.map((c) => `- ${formatToolCall(c)}`).join("\n")}`;
|
|
31
20
|
}
|
|
32
21
|
|
|
33
|
-
/**
|
|
34
|
-
* Render a message to markdown.
|
|
35
|
-
*/
|
|
36
22
|
function renderMessage(msg: Message): string {
|
|
37
23
|
switch (msg.type) {
|
|
38
24
|
case "user":
|
|
@@ -76,17 +62,11 @@ export interface RenderTranscriptOptions {
|
|
|
76
62
|
sourcePath?: string; // absolute source path for front matter provenance
|
|
77
63
|
}
|
|
78
64
|
|
|
79
|
-
/**
|
|
80
|
-
* Render transcript to markdown with branch awareness.
|
|
81
|
-
*/
|
|
82
65
|
export function renderTranscript(
|
|
83
66
|
transcript: Transcript,
|
|
84
|
-
options: RenderTranscriptOptions
|
|
67
|
+
options: RenderTranscriptOptions = {},
|
|
85
68
|
): string {
|
|
86
|
-
|
|
87
|
-
const opts: RenderTranscriptOptions =
|
|
88
|
-
typeof options === "string" ? { head: options } : options;
|
|
89
|
-
const { head, sourcePath } = opts;
|
|
69
|
+
const { head, sourcePath } = options;
|
|
90
70
|
|
|
91
71
|
const lines: string[] = [];
|
|
92
72
|
|
|
@@ -116,76 +96,33 @@ export function renderTranscript(
|
|
|
116
96
|
lines.push("");
|
|
117
97
|
lines.push("---");
|
|
118
98
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// Build tree
|
|
125
|
-
const { bySourceRef, children, parents, roots } = buildTree(
|
|
126
|
-
transcript.messages,
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
// Determine target (head or latest leaf)
|
|
130
|
-
let target: string | undefined;
|
|
131
|
-
if (head) {
|
|
132
|
-
if (!bySourceRef.has(head)) {
|
|
133
|
-
lines.push("");
|
|
134
|
-
lines.push(`**Error**: Message ID \`${head}\` not found`);
|
|
135
|
-
return lines.join("\n");
|
|
136
|
-
}
|
|
137
|
-
target = head;
|
|
138
|
-
} else {
|
|
139
|
-
target = findLatestLeaf(bySourceRef, children);
|
|
140
|
-
}
|
|
99
|
+
for (const event of walkTranscriptTree(transcript, { head })) {
|
|
100
|
+
switch (event.type) {
|
|
101
|
+
case "empty":
|
|
102
|
+
break;
|
|
141
103
|
|
|
142
|
-
|
|
143
|
-
// Fallback: just render all messages in order (shouldn't happen normally)
|
|
144
|
-
for (const msg of transcript.messages) {
|
|
145
|
-
const rendered = renderMessage(msg);
|
|
146
|
-
if (rendered) {
|
|
104
|
+
case "head_not_found":
|
|
147
105
|
lines.push("");
|
|
148
|
-
lines.push(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const msgs = bySourceRef.get(sourceRef);
|
|
161
|
-
if (!msgs) continue;
|
|
106
|
+
lines.push(`**Error**: Message ID \`${event.head}\` not found`);
|
|
107
|
+
break;
|
|
108
|
+
|
|
109
|
+
case "messages":
|
|
110
|
+
for (const msg of event.messages) {
|
|
111
|
+
const rendered = renderMessage(msg);
|
|
112
|
+
if (rendered) {
|
|
113
|
+
lines.push("");
|
|
114
|
+
lines.push(rendered);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
162
118
|
|
|
163
|
-
|
|
164
|
-
for (const msg of msgs) {
|
|
165
|
-
const rendered = renderMessage(msg);
|
|
166
|
-
if (rendered) {
|
|
119
|
+
case "branch_note":
|
|
167
120
|
lines.push("");
|
|
168
|
-
lines.push(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
// Check for other branches at this point (only if not using explicit --head)
|
|
173
|
-
if (!head) {
|
|
174
|
-
const childSet = children.get(sourceRef);
|
|
175
|
-
if (childSet && childSet.size > 1) {
|
|
176
|
-
const otherBranches = [...childSet].filter((c) => !pathSet.has(c));
|
|
177
|
-
if (otherBranches.length > 0) {
|
|
178
|
-
lines.push("");
|
|
179
|
-
lines.push("> **Other branches**:");
|
|
180
|
-
for (const branchRef of otherBranches) {
|
|
181
|
-
const branchMsgs = bySourceRef.get(branchRef);
|
|
182
|
-
if (branchMsgs && branchMsgs.length > 0) {
|
|
183
|
-
const firstLine = getFirstLine(branchMsgs[0]);
|
|
184
|
-
lines.push(`> - \`${branchRef}\` "${firstLine}"`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
121
|
+
lines.push("> **Other branches**:");
|
|
122
|
+
for (const branch of event.branches) {
|
|
123
|
+
lines.push(`> - \`${branch.sourceRef}\` "${branch.firstLine}"`);
|
|
187
124
|
}
|
|
188
|
-
|
|
125
|
+
break;
|
|
189
126
|
}
|
|
190
127
|
}
|
|
191
128
|
|
package/src/serve.ts
CHANGED
|
@@ -1,211 +1,146 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Serve command:
|
|
2
|
+
* Serve command: HTTP server for transcripts from the archive.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* for rendered HTML and titles. No output directory needed.
|
|
4
|
+
* Reads archive entries, renders HTML on demand with in-memory LRU cache.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
7
|
+
import type { ArchiveEntryHeader, TranscriptSummary } from "./archive.ts";
|
|
8
|
+
import { listEntryHeaders, loadEntry, DEFAULT_ARCHIVE_DIR } from "./archive.ts";
|
|
10
9
|
import { renderTranscriptHtml } from "./render-html.ts";
|
|
11
10
|
import { renderIndexFromSessions, type SessionEntry } from "./render-index.ts";
|
|
12
|
-
import {
|
|
13
|
-
import { extractFirstUserMessage } from "./utils/provenance.ts";
|
|
14
|
-
import {
|
|
15
|
-
computeContentHash,
|
|
16
|
-
loadCache,
|
|
17
|
-
saveCache,
|
|
18
|
-
getCachedSegments,
|
|
19
|
-
type CacheEntry,
|
|
20
|
-
} from "./cache.ts";
|
|
11
|
+
import { formatDateTimePrefix } from "./utils/naming.ts";
|
|
21
12
|
|
|
22
13
|
export interface ServeOptions {
|
|
23
|
-
|
|
14
|
+
archiveDir?: string;
|
|
24
15
|
port?: number;
|
|
25
16
|
quiet?: boolean;
|
|
26
|
-
noCache?: boolean;
|
|
27
17
|
}
|
|
28
18
|
|
|
29
19
|
interface SessionInfo {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
20
|
+
sessionId: string;
|
|
21
|
+
sourceHash: string;
|
|
22
|
+
title?: string;
|
|
23
|
+
segment: TranscriptSummary;
|
|
24
|
+
segmentIndex: number;
|
|
25
|
+
baseName: string;
|
|
34
26
|
}
|
|
35
27
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
}
|
|
28
|
+
/** Simple LRU cache for rendered HTML. */
|
|
29
|
+
class HtmlCache {
|
|
30
|
+
private map = new Map<string, string>();
|
|
31
|
+
constructor(private maxSize: number) {}
|
|
73
32
|
|
|
74
|
-
|
|
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;
|
|
33
|
+
private key(sessionId: string, segmentIndex: number, sourceHash: string) {
|
|
34
|
+
return `${sessionId}:${segmentIndex}:${sourceHash}`;
|
|
97
35
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
36
|
+
|
|
37
|
+
get(
|
|
38
|
+
sessionId: string,
|
|
39
|
+
segmentIndex: number,
|
|
40
|
+
sourceHash: string,
|
|
41
|
+
): string | undefined {
|
|
42
|
+
const k = this.key(sessionId, segmentIndex, sourceHash);
|
|
43
|
+
const val = this.map.get(k);
|
|
44
|
+
if (val !== undefined) {
|
|
45
|
+
// Move to end (most recently used)
|
|
46
|
+
this.map.delete(k);
|
|
47
|
+
this.map.set(k, val);
|
|
107
48
|
}
|
|
49
|
+
return val;
|
|
108
50
|
}
|
|
109
51
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
segments:
|
|
124
|
-
cached?.contentHash === contentHash
|
|
125
|
-
? cached.segments.map((s) => ({ ...s }))
|
|
126
|
-
: [],
|
|
127
|
-
};
|
|
128
|
-
while (newCache.segments.length <= segmentIndex) {
|
|
129
|
-
newCache.segments.push({});
|
|
52
|
+
set(
|
|
53
|
+
sessionId: string,
|
|
54
|
+
segmentIndex: number,
|
|
55
|
+
sourceHash: string,
|
|
56
|
+
html: string,
|
|
57
|
+
) {
|
|
58
|
+
const k = this.key(sessionId, segmentIndex, sourceHash);
|
|
59
|
+
this.map.delete(k);
|
|
60
|
+
this.map.set(k, html);
|
|
61
|
+
if (this.map.size > this.maxSize) {
|
|
62
|
+
// Evict oldest
|
|
63
|
+
const oldest = this.map.keys().next().value!;
|
|
64
|
+
this.map.delete(oldest);
|
|
130
65
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildSessionMap(
|
|
70
|
+
headers: ArchiveEntryHeader[],
|
|
71
|
+
): Map<string, SessionInfo> {
|
|
72
|
+
const sessions = new Map<string, SessionInfo>();
|
|
73
|
+
|
|
74
|
+
for (const header of headers) {
|
|
75
|
+
for (let i = 0; i < header.segments.length; i++) {
|
|
76
|
+
const segment = header.segments[i];
|
|
77
|
+
const dateTime = formatDateTimePrefix(segment.firstMessageTimestamp);
|
|
78
|
+
const baseName = `${dateTime}-${header.sessionId}`;
|
|
79
|
+
const suffix = header.segments.length > 1 ? `_${i + 1}` : "";
|
|
80
|
+
const fullName = `${baseName}${suffix}`;
|
|
81
|
+
|
|
82
|
+
sessions.set(fullName, {
|
|
83
|
+
sessionId: header.sessionId,
|
|
84
|
+
sourceHash: header.sourceHash,
|
|
85
|
+
title: header.title,
|
|
86
|
+
segment,
|
|
87
|
+
segmentIndex: i,
|
|
88
|
+
baseName: fullName,
|
|
89
|
+
});
|
|
134
90
|
}
|
|
135
|
-
await saveCache(session.source.path, newCache);
|
|
136
91
|
}
|
|
137
92
|
|
|
138
|
-
return
|
|
93
|
+
return sessions;
|
|
139
94
|
}
|
|
140
95
|
|
|
141
|
-
|
|
142
|
-
* Build session entries for index page.
|
|
143
|
-
*/
|
|
144
|
-
async function buildIndexEntries(
|
|
145
|
-
sessions: Map<string, SessionInfo[]>,
|
|
146
|
-
): Promise<SessionEntry[]> {
|
|
96
|
+
function buildIndexEntries(sessions: Map<string, SessionInfo>): SessionEntry[] {
|
|
147
97
|
const entries: SessionEntry[] = [];
|
|
148
98
|
|
|
149
|
-
for (const [baseName,
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
}
|
|
99
|
+
for (const [baseName, info] of sessions) {
|
|
100
|
+
const { segment, title } = info;
|
|
101
|
+
if (segment.metadata.messageCount === 0) continue;
|
|
102
|
+
|
|
103
|
+
const { messageCount, startTime, endTime, cwd } = segment.metadata;
|
|
104
|
+
|
|
105
|
+
entries.push({
|
|
106
|
+
filename: `${baseName}.html`,
|
|
107
|
+
title:
|
|
108
|
+
title ||
|
|
109
|
+
(segment.firstUserMessage.length > 80
|
|
110
|
+
? segment.firstUserMessage.slice(0, 80) + "..."
|
|
111
|
+
: segment.firstUserMessage) ||
|
|
112
|
+
baseName,
|
|
113
|
+
firstUserMessage: segment.firstUserMessage,
|
|
114
|
+
date: startTime,
|
|
115
|
+
endDate: endTime,
|
|
116
|
+
messageCount,
|
|
117
|
+
cwd,
|
|
118
|
+
});
|
|
189
119
|
}
|
|
190
120
|
|
|
191
121
|
return entries;
|
|
192
122
|
}
|
|
193
123
|
|
|
194
|
-
/**
|
|
195
|
-
* Start the HTTP server.
|
|
196
|
-
*/
|
|
197
124
|
export async function serve(options: ServeOptions): Promise<void> {
|
|
198
|
-
const {
|
|
125
|
+
const {
|
|
126
|
+
archiveDir = DEFAULT_ARCHIVE_DIR,
|
|
127
|
+
port = 3000,
|
|
128
|
+
quiet = false,
|
|
129
|
+
} = options;
|
|
199
130
|
|
|
200
131
|
if (!quiet) {
|
|
201
|
-
console.error(`
|
|
132
|
+
console.error(`Loading archive from ${archiveDir}...`);
|
|
202
133
|
}
|
|
203
134
|
|
|
204
|
-
|
|
205
|
-
const sessions =
|
|
135
|
+
const headers = await listEntryHeaders(archiveDir);
|
|
136
|
+
const sessions = buildSessionMap(headers);
|
|
137
|
+
const htmlCache = new HtmlCache(200);
|
|
138
|
+
|
|
139
|
+
// Index is static (session map doesn't change), so build once
|
|
140
|
+
const indexHtml = renderIndexFromSessions(buildIndexEntries(sessions));
|
|
206
141
|
|
|
207
142
|
if (!quiet) {
|
|
208
|
-
console.error(`Found ${sessions.size}
|
|
143
|
+
console.error(`Found ${sessions.size} transcript(s)`);
|
|
209
144
|
console.error(`Starting server at http://localhost:${port}`);
|
|
210
145
|
}
|
|
211
146
|
|
|
@@ -215,38 +150,43 @@ export async function serve(options: ServeOptions): Promise<void> {
|
|
|
215
150
|
const url = new URL(req.url);
|
|
216
151
|
const path = url.pathname;
|
|
217
152
|
|
|
218
|
-
// Log request
|
|
219
153
|
if (!quiet) {
|
|
220
154
|
console.error(`${req.method} ${path}`);
|
|
221
155
|
}
|
|
222
156
|
|
|
223
157
|
// Index page
|
|
224
158
|
if (path === "/" || path === "/index.html") {
|
|
225
|
-
|
|
226
|
-
const html = renderIndexFromSessions(entries);
|
|
227
|
-
return new Response(html, {
|
|
159
|
+
return new Response(indexHtml, {
|
|
228
160
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
229
161
|
});
|
|
230
162
|
}
|
|
231
163
|
|
|
232
164
|
// Session page
|
|
233
165
|
if (path.endsWith(".html")) {
|
|
234
|
-
const baseName = path.slice(1, -5);
|
|
166
|
+
const baseName = path.slice(1, -5);
|
|
167
|
+
const info = sessions.get(baseName);
|
|
235
168
|
|
|
236
|
-
|
|
237
|
-
const exactInfos = sessions.get(baseName);
|
|
238
|
-
if (exactInfos && exactInfos.length > 0) {
|
|
169
|
+
if (info) {
|
|
239
170
|
try {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
171
|
+
const { sessionId, segmentIndex, sourceHash, title } = info;
|
|
172
|
+
|
|
173
|
+
const cached = htmlCache.get(sessionId, segmentIndex, sourceHash);
|
|
174
|
+
if (cached) {
|
|
175
|
+
return new Response(cached, {
|
|
176
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Load full entry from disk on demand
|
|
181
|
+
const entry = await loadEntry(archiveDir, sessionId);
|
|
182
|
+
const transcript = entry?.transcripts[segmentIndex];
|
|
183
|
+
if (!transcript) {
|
|
247
184
|
return new Response("Not Found", { status: 404 });
|
|
248
185
|
}
|
|
249
|
-
|
|
186
|
+
|
|
187
|
+
const html = await renderTranscriptHtml(transcript, { title });
|
|
188
|
+
htmlCache.set(sessionId, segmentIndex, sourceHash, html);
|
|
189
|
+
return new Response(html, {
|
|
250
190
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
251
191
|
});
|
|
252
192
|
} catch (error) {
|
|
@@ -256,35 +196,6 @@ export async function serve(options: ServeOptions): Promise<void> {
|
|
|
256
196
|
}
|
|
257
197
|
}
|
|
258
198
|
|
|
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
199
|
return new Response("Not Found", { status: 404 });
|
|
289
200
|
}
|
|
290
201
|
|
|
@@ -292,12 +203,10 @@ export async function serve(options: ServeOptions): Promise<void> {
|
|
|
292
203
|
},
|
|
293
204
|
});
|
|
294
205
|
|
|
295
|
-
// Keep server running
|
|
296
206
|
if (!quiet) {
|
|
297
207
|
console.error(`\nPress Ctrl+C to stop`);
|
|
298
208
|
}
|
|
299
209
|
|
|
300
|
-
// Handle shutdown gracefully
|
|
301
210
|
process.on("SIGINT", () => {
|
|
302
211
|
if (!quiet) {
|
|
303
212
|
console.error("\nShutting down...");
|