@spexcode/transcript 0.7.0-next.1 → 0.7.0-next.2
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/parsers.d.ts +2 -0
- package/dist/parsers.js +120 -8
- package/dist/readers.d.ts +1 -1
- package/dist/readers.js +7 -3
- package/package.json +1 -1
package/dist/parsers.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export type ParsedEvent = {
|
|
|
27
27
|
export type Parse = (value: unknown) => ParsedEvent | null;
|
|
28
28
|
export declare function claudeEvent(value: unknown): ParsedEvent | null;
|
|
29
29
|
export declare function codexEvent(value: unknown): ParsedEvent | null;
|
|
30
|
+
export declare function codexAppServerEvent(value: unknown): ParsedEvent | null;
|
|
31
|
+
export declare function codexAppServerStream(): Parse;
|
|
30
32
|
export declare function piEvent(value: unknown): ParsedEvent | null;
|
|
31
33
|
export declare function geminiEvent(value: unknown): ParsedEvent | null;
|
|
32
34
|
export declare function openclawEvent(value: unknown): ParsedEvent | null;
|
package/dist/parsers.js
CHANGED
|
@@ -120,6 +120,101 @@ export function codexEvent(value) {
|
|
|
120
120
|
}
|
|
121
121
|
return null;
|
|
122
122
|
}
|
|
123
|
+
// Codex app-server notifications are a different native stream from rollout lines. Keep this mapping stateless:
|
|
124
|
+
// a caller that needs streamed prose uses codexAppServerStream below, while file and in-memory sources still
|
|
125
|
+
// share the same one-record parser contract.
|
|
126
|
+
export function codexAppServerEvent(value) {
|
|
127
|
+
const entry = object(value);
|
|
128
|
+
const params = object(entry?.params);
|
|
129
|
+
if (!entry || !params || typeof entry.method !== 'string')
|
|
130
|
+
return null;
|
|
131
|
+
const item = object(params.item);
|
|
132
|
+
const method = entry.method;
|
|
133
|
+
const recognized = method === 'item/agentMessage/delta' || method === 'item/started' || method === 'item/completed';
|
|
134
|
+
if (!recognized)
|
|
135
|
+
return null;
|
|
136
|
+
const eventAt = timestamp(params.emittedAtMs) ?? timestamp(params.startedAtMs) ?? timestamp(params.completedAtMs);
|
|
137
|
+
if (eventAt === null)
|
|
138
|
+
return { at: null, turn: null };
|
|
139
|
+
if (method === 'item/agentMessage/delta') {
|
|
140
|
+
const id = string(params.itemId);
|
|
141
|
+
const delta = string(params.delta);
|
|
142
|
+
return id && delta !== null
|
|
143
|
+
? { at: eventAt, turn: { id, at: eventAt, role: 'assistant', text: delta, tools: [] } }
|
|
144
|
+
: null;
|
|
145
|
+
}
|
|
146
|
+
if ((method !== 'item/started' && method !== 'item/completed') || !item)
|
|
147
|
+
return null;
|
|
148
|
+
const id = string(item.id);
|
|
149
|
+
const type = string(item.type);
|
|
150
|
+
if (!id || !type)
|
|
151
|
+
return null;
|
|
152
|
+
if (type === 'userMessage') {
|
|
153
|
+
const text = blockText(item.content);
|
|
154
|
+
return text ? { at: eventAt, turn: { id, at: eventAt, role: 'user', text, tools: [] } } : null;
|
|
155
|
+
}
|
|
156
|
+
if (type === 'agentMessage') {
|
|
157
|
+
const text = string(item.text);
|
|
158
|
+
return { at: eventAt, turn: { id, at: eventAt, role: 'assistant', text: text ?? undefined, tools: [] } };
|
|
159
|
+
}
|
|
160
|
+
const toolTypes = new Set(['commandExecution', 'functionCall', 'customToolCall', 'mcpToolCall', 'dynamicToolCall']);
|
|
161
|
+
if (!toolTypes.has(type))
|
|
162
|
+
return null;
|
|
163
|
+
if (method === 'item/started') {
|
|
164
|
+
const name = type === 'commandExecution' ? 'command'
|
|
165
|
+
: string(item.name) ?? string(item.tool) ?? (type === 'mcpToolCall' ? 'mcp' : 'tool');
|
|
166
|
+
const input = item.arguments !== undefined ? item.arguments
|
|
167
|
+
: item.input !== undefined ? item.input
|
|
168
|
+
: item.command !== undefined ? item.command
|
|
169
|
+
: undefined;
|
|
170
|
+
return { at: eventAt, turn: { id, at: eventAt, role: 'assistant', tools: [{ id, name, input: input === undefined ? undefined : compact(input), outputLines: 0, outputBytes: 0 }] } };
|
|
171
|
+
}
|
|
172
|
+
let output = undefined;
|
|
173
|
+
if (type === 'commandExecution')
|
|
174
|
+
output = item.aggregatedOutput;
|
|
175
|
+
else if (type === 'functionCall' || type === 'customToolCall')
|
|
176
|
+
output = item.output ?? item.result;
|
|
177
|
+
else if (type === 'mcpToolCall')
|
|
178
|
+
output = item.result ?? item.error;
|
|
179
|
+
else if (type === 'dynamicToolCall')
|
|
180
|
+
output = item.contentItems ?? item.output ?? item.error;
|
|
181
|
+
return output === undefined || output === null
|
|
182
|
+
? { at: eventAt, turn: null }
|
|
183
|
+
: { at: eventAt, turn: null, toolOutputs: [{ id, text: compact(output) }] };
|
|
184
|
+
}
|
|
185
|
+
// Agent-message deltas are fragments of one native item. The closure remembers only that item's text and
|
|
186
|
+
// re-emits its native id, allowing IntervalCollector to replace the earlier turn in place.
|
|
187
|
+
export function codexAppServerStream() {
|
|
188
|
+
const textByItem = new Map();
|
|
189
|
+
return (value) => {
|
|
190
|
+
const parsed = codexAppServerEvent(value);
|
|
191
|
+
if (!parsed)
|
|
192
|
+
return null;
|
|
193
|
+
const entry = object(value);
|
|
194
|
+
const params = object(entry?.params);
|
|
195
|
+
const item = object(params?.item);
|
|
196
|
+
if (entry?.method === 'item/agentMessage/delta') {
|
|
197
|
+
const id = string(params?.itemId);
|
|
198
|
+
if (!id || !parsed.turn)
|
|
199
|
+
return parsed;
|
|
200
|
+
const text = (textByItem.get(id) ?? '') + (parsed.turn.text ?? '');
|
|
201
|
+
textByItem.set(id, text);
|
|
202
|
+
return { ...parsed, turn: { ...parsed.turn, text } };
|
|
203
|
+
}
|
|
204
|
+
if (item && item.type === 'agentMessage') {
|
|
205
|
+
const id = string(item.id);
|
|
206
|
+
if (id) {
|
|
207
|
+
const text = string(item.text);
|
|
208
|
+
if (text !== null || !textByItem.has(id))
|
|
209
|
+
textByItem.set(id, text ?? '');
|
|
210
|
+
const turn = parsed.turn;
|
|
211
|
+
if (turn)
|
|
212
|
+
return { ...parsed, turn: { ...turn, text: textByItem.get(id) || undefined } };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return parsed;
|
|
216
|
+
};
|
|
217
|
+
}
|
|
123
218
|
const blockText = (content) => typeof content === 'string'
|
|
124
219
|
? string(content)
|
|
125
220
|
: items(content).map((block) => string(object(block)?.text)).filter(Boolean).join('\n') || null;
|
|
@@ -366,14 +461,31 @@ export class IntervalCollector {
|
|
|
366
461
|
this.synthesized.set(base, seen + 1);
|
|
367
462
|
turn.id = seen ? `${base}#${seen}` : base;
|
|
368
463
|
}
|
|
369
|
-
this.turns.
|
|
370
|
-
|
|
371
|
-
this.
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
464
|
+
const existingAt = this.turns.findIndex((candidate) => candidate.id === turn.id);
|
|
465
|
+
if (existingAt >= 0) {
|
|
466
|
+
const existing = this.turns[existingAt];
|
|
467
|
+
const incomingTools = new Map(turn.tools.map((tool) => [tool.id, tool]));
|
|
468
|
+
const tools = existing.tools.map((tool) => {
|
|
469
|
+
const incoming = incomingTools.get(tool.id);
|
|
470
|
+
incomingTools.delete(tool.id);
|
|
471
|
+
return incoming ? { ...tool, ...incoming, output: incoming.output ?? tool.output } : tool;
|
|
472
|
+
});
|
|
473
|
+
tools.push(...incomingTools.values());
|
|
474
|
+
const replacement = { ...existing, ...turn, text: turn.text ?? existing.text, tools };
|
|
475
|
+
this.turns[existingAt] = replacement;
|
|
476
|
+
for (const tool of replacement.tools)
|
|
477
|
+
this.byTool.set(tool.id, tool);
|
|
478
|
+
}
|
|
479
|
+
else {
|
|
480
|
+
this.turns.push(turn);
|
|
481
|
+
for (const tool of turn.tools)
|
|
482
|
+
this.byTool.set(tool.id, tool);
|
|
483
|
+
if (this.turns.length > MAX_TURNS) {
|
|
484
|
+
const dropped = this.turns.shift();
|
|
485
|
+
for (const tool of dropped.tools)
|
|
486
|
+
this.evicted.add(tool.id);
|
|
487
|
+
this.omittedTurns++;
|
|
488
|
+
}
|
|
377
489
|
}
|
|
378
490
|
}
|
|
379
491
|
return this.pastRange;
|
package/dist/readers.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type TranscriptReader } from './turns.js';
|
|
2
2
|
export declare function claudeTranscriptPath(threadId: string, root?: string): string | null;
|
|
3
|
-
export declare function codexRolloutPath(threadId: string, root?: string): string | null;
|
|
3
|
+
export declare function codexRolloutPath(threadId: string, root?: string, archive?: string): string | null;
|
|
4
4
|
export declare function piSessionPath(threadId: string, root?: string): string | null;
|
|
5
5
|
export declare function geminiTranscriptPath(threadId: string, root?: string): string | null;
|
|
6
6
|
export declare function openclawTranscriptPath(threadId: string, root?: string): string | null;
|
package/dist/readers.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { closeSync, openSync, readFileSync, readSync, readdirSync, statSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
5
|
import { TranscriptReadError } from './turns.js';
|
|
6
6
|
import { IntervalCollector, claudeEvent, codexEvent, geminiEvent, hermesEvents, openclawEvent, opencodeEvents, piEvent } from './parsers.js';
|
|
7
7
|
// THE NATIVE-THREAD READERS. Each harness keeps its conversation somewhere private — Claude's project JSONL,
|
|
@@ -31,7 +31,10 @@ export function claudeTranscriptPath(threadId, root = projectTranscriptRoot()) {
|
|
|
31
31
|
const codexSessionsDir = () => join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'sessions');
|
|
32
32
|
// Walk newest day first and return on the first hit; the walk is exhaustive rather than capped, because
|
|
33
33
|
// future-dated junk under sessions/ sorts above every real day and a cap once masked every real rollout.
|
|
34
|
-
|
|
34
|
+
// A thread Codex has ARCHIVED (the app-server's thread/archive, which a closed session runs) keeps its
|
|
35
|
+
// rollout, moved out of the dated tree into the flat `archived_sessions/` beside it — a closed session's
|
|
36
|
+
// conversation is still on disk and still readable, so the locator looks there second.
|
|
37
|
+
export function codexRolloutPath(threadId, root = codexSessionsDir(), archive = join(dirname(root), 'archived_sessions')) {
|
|
35
38
|
for (const year of children(root))
|
|
36
39
|
for (const month of children(join(root, year)))
|
|
37
40
|
for (const day of children(join(root, year, month))) {
|
|
@@ -40,7 +43,8 @@ export function codexRolloutPath(threadId, root = codexSessionsDir()) {
|
|
|
40
43
|
if (file)
|
|
41
44
|
return join(dir, file);
|
|
42
45
|
}
|
|
43
|
-
|
|
46
|
+
const archived = children(archive).find((name) => name.includes(threadId));
|
|
47
|
+
return archived ? join(archive, archived) : null;
|
|
44
48
|
}
|
|
45
49
|
const piSessionsRoot = () => join(process.env.SPEXCODE_PI_AGENT_DIR || join(homedir(), '.pi', 'agent'), 'sessions');
|
|
46
50
|
const piSessionPaths = new Map();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/transcript",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Normalized agent transcripts: one parser per harness, a bounded interval reader over a native thread file or an in-memory event stream, and the full/delta frame protocol every transport and renderer share.",
|
|
6
6
|
"files": [
|