spexcode 0.6.8 → 0.7.0-next.1

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.
Files changed (72) hide show
  1. package/node_modules/@spexcode/session-application/package.json +5 -5
  2. package/node_modules/@spexcode/session-events/package.json +2 -2
  3. package/node_modules/@spexcode/session-protocol/package.json +1 -1
  4. package/node_modules/@spexcode/session-runtime/package.json +2 -2
  5. package/node_modules/@spexcode/session-selflaunch/dist/locality.d.ts +12 -1
  6. package/node_modules/@spexcode/session-selflaunch/dist/locality.js +62 -10
  7. package/node_modules/@spexcode/session-selflaunch/package.json +3 -3
  8. package/node_modules/@spexcode/session-topology/package.json +2 -2
  9. package/node_modules/@spexcode/spec-cli/bin/spex.mjs +127 -10
  10. package/node_modules/@spexcode/spec-cli/dist/cli.js +19 -0
  11. package/node_modules/@spexcode/spec-cli/dist/file-write.js +14 -2
  12. package/node_modules/@spexcode/spec-cli/dist/guide.js +9 -11
  13. package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +11 -5
  14. package/node_modules/@spexcode/spec-cli/dist/harness.js +185 -28
  15. package/node_modules/@spexcode/spec-cli/dist/host.js +5 -1
  16. package/node_modules/@spexcode/spec-cli/dist/index.js +21 -65
  17. package/node_modules/@spexcode/spec-cli/dist/machine-peer.d.ts +2 -1
  18. package/node_modules/@spexcode/spec-cli/dist/machine-peer.js +45 -14
  19. package/node_modules/@spexcode/spec-cli/dist/opencode.js +10 -2
  20. package/node_modules/@spexcode/spec-cli/dist/pi-harness.js +18 -5
  21. package/node_modules/@spexcode/spec-cli/dist/pi-headless.d.ts +2 -0
  22. package/node_modules/@spexcode/spec-cli/dist/pi-headless.js +27 -0
  23. package/node_modules/@spexcode/spec-cli/dist/session-transcript.d.ts +57 -0
  24. package/node_modules/@spexcode/spec-cli/dist/session-transcript.js +137 -0
  25. package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +4 -0
  26. package/node_modules/@spexcode/spec-cli/dist/sessions.js +104 -36
  27. package/node_modules/@spexcode/spec-cli/dist/shim-runtime.js +14 -0
  28. package/node_modules/@spexcode/spec-cli/dist/supervise.js +1 -1
  29. package/node_modules/@spexcode/spec-cli/package.json +8 -6
  30. package/node_modules/@spexcode/spec-core/dist/layout.d.ts +0 -1
  31. package/node_modules/@spexcode/spec-core/package.json +1 -1
  32. package/node_modules/@spexcode/spec-core/templates/spexcode.json +36 -13
  33. package/node_modules/@spexcode/spec-eval/package.json +2 -2
  34. package/node_modules/@spexcode/spec-forge/package.json +2 -2
  35. package/node_modules/@spexcode/transcript/dist/frames.d.ts +46 -0
  36. package/node_modules/@spexcode/transcript/dist/frames.js +87 -0
  37. package/node_modules/@spexcode/transcript/dist/index.d.ts +5 -0
  38. package/node_modules/@spexcode/transcript/dist/index.js +5 -0
  39. package/node_modules/@spexcode/transcript/dist/live.d.ts +16 -0
  40. package/node_modules/@spexcode/transcript/dist/live.js +65 -0
  41. package/node_modules/@spexcode/transcript/dist/parsers.d.ts +50 -0
  42. package/node_modules/@spexcode/transcript/dist/parsers.js +395 -0
  43. package/node_modules/@spexcode/transcript/dist/readers.d.ts +16 -0
  44. package/node_modules/@spexcode/transcript/dist/readers.js +375 -0
  45. package/node_modules/@spexcode/{spec-cli/dist/transcript-reader.d.ts → transcript/dist/turns.d.ts} +11 -5
  46. package/node_modules/@spexcode/transcript/dist/turns.js +11 -0
  47. package/node_modules/@spexcode/transcript/package.json +30 -0
  48. package/node_modules/smol-toml/LICENSE +24 -0
  49. package/node_modules/smol-toml/README.md +418 -0
  50. package/node_modules/smol-toml/dist/date.d.ts +41 -0
  51. package/node_modules/smol-toml/dist/date.js +127 -0
  52. package/node_modules/smol-toml/dist/error.d.ts +38 -0
  53. package/node_modules/smol-toml/dist/error.js +63 -0
  54. package/node_modules/smol-toml/dist/extract.js +69 -0
  55. package/node_modules/smol-toml/dist/index.cjs +734 -0
  56. package/node_modules/smol-toml/dist/index.d.ts +43 -0
  57. package/node_modules/smol-toml/dist/index.js +33 -0
  58. package/node_modules/smol-toml/dist/parse.d.ts +36 -0
  59. package/node_modules/smol-toml/dist/parse.js +149 -0
  60. package/node_modules/smol-toml/dist/primitive.js +238 -0
  61. package/node_modules/smol-toml/dist/stringify.d.ts +31 -0
  62. package/node_modules/smol-toml/dist/stringify.js +181 -0
  63. package/node_modules/smol-toml/dist/struct.js +179 -0
  64. package/node_modules/smol-toml/dist/util.d.ts +38 -0
  65. package/node_modules/smol-toml/dist/util.js +89 -0
  66. package/node_modules/smol-toml/package.json +68 -0
  67. package/package.json +3 -3
  68. package/node_modules/@spexcode/spec-cli/dist/execution-trace.d.ts +0 -27
  69. package/node_modules/@spexcode/spec-cli/dist/execution-trace.js +0 -475
  70. package/node_modules/@spexcode/spec-cli/dist/session-execution.d.ts +0 -10
  71. package/node_modules/@spexcode/spec-cli/dist/session-execution.js +0 -70
  72. package/node_modules/@spexcode/spec-cli/dist/transcript-reader.js +0 -251
@@ -0,0 +1,375 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { closeSync, openSync, readFileSync, readSync, readdirSync, statSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { TranscriptReadError } from './turns.js';
6
+ import { IntervalCollector, claudeEvent, codexEvent, geminiEvent, hermesEvents, openclawEvent, opencodeEvents, piEvent } from './parsers.js';
7
+ // THE NATIVE-THREAD READERS. Each harness keeps its conversation somewhere private — Claude's project JSONL,
8
+ // Codex's rollout, pi's session JSONL, OpenCode's store behind `opencode export` — and this module is the only
9
+ // place that knows where. It answers exactly one question for every harness: "what happened in this thread
10
+ // between `from` and `to`?", as normalized turns, through the three reader verbs of [[transcript]].
11
+ const POST_RANGE_LOOKAHEAD_LINES = 256;
12
+ const children = (dir) => { try {
13
+ return readdirSync(dir).sort().reverse();
14
+ }
15
+ catch {
16
+ return [];
17
+ } };
18
+ // --- where each harness keeps the thread ------------------------------------------------------------------
19
+ const projectTranscriptRoot = () => join(process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude'), 'projects');
20
+ export function claudeTranscriptPath(threadId, root = projectTranscriptRoot()) {
21
+ for (const project of children(root)) {
22
+ const path = join(root, project, `${threadId}.jsonl`);
23
+ try {
24
+ if (statSync(path).isFile())
25
+ return path;
26
+ }
27
+ catch { /* try next */ }
28
+ }
29
+ return null;
30
+ }
31
+ const codexSessionsDir = () => join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'sessions');
32
+ // Walk newest day first and return on the first hit; the walk is exhaustive rather than capped, because
33
+ // future-dated junk under sessions/ sorts above every real day and a cap once masked every real rollout.
34
+ export function codexRolloutPath(threadId, root = codexSessionsDir()) {
35
+ for (const year of children(root))
36
+ for (const month of children(join(root, year)))
37
+ for (const day of children(join(root, year, month))) {
38
+ const dir = join(root, year, month, day);
39
+ const file = children(dir).find((name) => name.includes(threadId));
40
+ if (file)
41
+ return join(dir, file);
42
+ }
43
+ return null;
44
+ }
45
+ const piSessionsRoot = () => join(process.env.SPEXCODE_PI_AGENT_DIR || join(homedir(), '.pi', 'agent'), 'sessions');
46
+ const piSessionPaths = new Map();
47
+ export function piSessionPath(threadId, root = piSessionsRoot()) {
48
+ const key = `${root}:${threadId}`;
49
+ const cached = piSessionPaths.get(key);
50
+ if (cached) {
51
+ try {
52
+ if (statSync(cached).isFile())
53
+ return cached;
54
+ }
55
+ catch {
56
+ piSessionPaths.delete(key);
57
+ }
58
+ }
59
+ for (const directory of children(root))
60
+ for (const file of children(join(root, directory))) {
61
+ if (!file.endsWith('.jsonl'))
62
+ continue;
63
+ const path = join(root, directory, file);
64
+ try {
65
+ const header = JSON.parse(readFileSync(path, 'utf8').split('\n', 1)[0]);
66
+ if (header && typeof header === 'object' && header.type === 'session' && header.id === threadId) {
67
+ piSessionPaths.set(key, path);
68
+ return path;
69
+ }
70
+ }
71
+ catch { /* unreadable entries are not a match */ }
72
+ }
73
+ return null;
74
+ }
75
+ const findJsonl = (root, threadId, maxDepth = 5) => {
76
+ const walk = (dir, depth) => {
77
+ if (depth > maxDepth)
78
+ return null;
79
+ for (const name of children(dir)) {
80
+ const path = join(dir, name);
81
+ try {
82
+ if (statSync(path).isFile() && name.endsWith('.jsonl')) {
83
+ if (name.includes(threadId))
84
+ return path;
85
+ const header = JSON.parse(readFileSync(path, 'utf8').split('\n', 1)[0]);
86
+ if (header.sessionId === threadId || header.id === threadId)
87
+ return path;
88
+ }
89
+ else if (statSync(path).isDirectory()) {
90
+ const found = walk(path, depth + 1);
91
+ if (found)
92
+ return found;
93
+ }
94
+ }
95
+ catch { /* skip entries that disappear or are not JSON */ }
96
+ }
97
+ return null;
98
+ };
99
+ return walk(root, 0);
100
+ };
101
+ const geminiRoot = () => process.env.GEMINI_HOME || process.env.GEMINI_CONFIG_DIR || join(homedir(), '.gemini');
102
+ export function geminiTranscriptPath(threadId, root = geminiRoot()) { return findJsonl(root, threadId); }
103
+ const openclawRoot = () => process.env.OPENCLAW_STATE_DIR || join(homedir(), '.openclaw', 'state');
104
+ export function openclawTranscriptPath(threadId, root = openclawRoot()) { return findJsonl(root, threadId); }
105
+ const opencodeStoreRoot = () => process.env.SPEXCODE_OPENCODE_DATA_DIR
106
+ || join(process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share'), 'opencode');
107
+ function opencodeStoreRevision(root) {
108
+ try {
109
+ const database = statSync(join(root, 'opencode.db'));
110
+ let writeAheadLog = '0:0';
111
+ try {
112
+ const stat = statSync(join(root, 'opencode.db-wal'));
113
+ writeAheadLog = `${stat.size}:${Math.floor(stat.mtimeMs)}`;
114
+ }
115
+ catch { /* a checkpointed database has no separate write-ahead log */ }
116
+ return `${database.size}:${Math.floor(database.mtimeMs)}:${writeAheadLog}`;
117
+ }
118
+ catch {
119
+ return null;
120
+ }
121
+ }
122
+ // The export is read RAW: `--sanitize` replaces every prose and tool-output part with a `[redacted:…]` token,
123
+ // which made the whole conversation unreadable; the reader hands over the same local bytes the other harnesses'
124
+ // files hold, and nothing here leaves the machine that ran the thread.
125
+ function opencodeExport(threadId) {
126
+ return execFileSync(process.env.SPEXCODE_OPENCODE_CMD || 'opencode', ['export', threadId], {
127
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
128
+ });
129
+ }
130
+ const fileRevision = (path) => {
131
+ try {
132
+ const stat = statSync(path);
133
+ return `${stat.size}:${Math.floor(stat.mtimeMs)}`;
134
+ }
135
+ catch {
136
+ return null;
137
+ }
138
+ };
139
+ // @@@interval-seek - a native file is append-only, so the byte where an interval's first event sits never
140
+ // moves. The open tail re-reads its interval on every change; remembering that offset per (file, from) turns
141
+ // each re-read into "parse the current stretch" instead of "parse the whole thread again".
142
+ const intervalOffsets = new Map();
143
+ // One pass over the bytes from `scan.position` to the end of the file. Every complete line is parsed as JSON
144
+ // and handed to `onLine` with its byte offset; `onLine` returning true stops the scan early (a bounded
145
+ // lookahead), which abandons the rest — only a one-shot read does that.
146
+ function scanLines(harness, fd, scan, onLine) {
147
+ const chunk = Buffer.allocUnsafe(64 * 1024);
148
+ let { position, carry } = scan;
149
+ let lineStart = position - carry.length;
150
+ while (true) {
151
+ const read = readSync(fd, chunk, 0, chunk.length, position);
152
+ if (read <= 0)
153
+ break;
154
+ position += read;
155
+ const buffer = carry.length ? Buffer.concat([carry, chunk.subarray(0, read)]) : Buffer.from(chunk.subarray(0, read));
156
+ let cut = 0;
157
+ for (let index = 0; index < buffer.length; index++) {
158
+ if (buffer[index] !== 10)
159
+ continue;
160
+ const line = buffer.subarray(cut, index).toString('utf8');
161
+ const lineOffset = lineStart;
162
+ lineStart += index - cut + 1;
163
+ cut = index + 1;
164
+ if (!line.trim())
165
+ continue;
166
+ let value;
167
+ try {
168
+ value = JSON.parse(line);
169
+ }
170
+ catch (error) {
171
+ throw new TranscriptReadError('invalid', `${harness} transcript cannot be parsed: ${error instanceof Error ? error.message : String(error)}`);
172
+ }
173
+ if (onLine(value, lineOffset))
174
+ return { position, carry: Buffer.alloc(0) };
175
+ }
176
+ carry = Buffer.from(buffer.subarray(cut));
177
+ }
178
+ return { position, carry };
179
+ }
180
+ // The cursor over one interval of one line file. A one-shot `read` is a cursor advanced once and dropped; the
181
+ // open interval's `tail` keeps it, so each advance parses only what the harness appended since the last one.
182
+ class LineFileCursor {
183
+ harness;
184
+ path;
185
+ parse;
186
+ from;
187
+ collector;
188
+ scan = { position: 0, carry: Buffer.alloc(0) };
189
+ started = false;
190
+ seekKey;
191
+ constructor(harness, path, parse, from) {
192
+ this.harness = harness;
193
+ this.path = path;
194
+ this.parse = parse;
195
+ this.from = from;
196
+ this.seekKey = `${path}\n${from}`;
197
+ this.collector = new IntervalCollector({ from, to: from });
198
+ }
199
+ restart(size) {
200
+ const seek = intervalOffsets.get(this.seekKey) ?? 0;
201
+ const start = seek > 0 && seek < size ? seek : 0;
202
+ this.collector = new IntervalCollector({ from: this.from, to: this.from });
203
+ // a seek lands on the interval's first event, so the timestamps before it are known to exist
204
+ if (start > 0)
205
+ this.collector.sawTimestamp = true;
206
+ this.scan = { position: start, carry: Buffer.alloc(0) };
207
+ }
208
+ advance(to, lookahead = Number.POSITIVE_INFINITY) {
209
+ let size = 0;
210
+ try {
211
+ size = statSync(this.path).size;
212
+ }
213
+ catch (error) {
214
+ throw new TranscriptReadError('unreadable', `${this.harness} transcript is unreadable: ${error instanceof Error ? error.message : String(error)}`);
215
+ }
216
+ if (size <= 0)
217
+ throw new TranscriptReadError('unreadable', `${this.harness} transcript is unreadable: file is empty`);
218
+ // a source that shrank was rewritten underneath the cursor: forget the position and read the interval afresh
219
+ if (!this.started || size < this.scan.position) {
220
+ this.restart(size);
221
+ this.started = true;
222
+ }
223
+ this.collector.extend(to);
224
+ let fd = null;
225
+ try {
226
+ fd = openSync(this.path, 'r');
227
+ let postRangeLines = 0;
228
+ this.scan = scanLines(this.harness, fd, this.scan, (value, offset) => {
229
+ const event = this.parse(value);
230
+ if (!event)
231
+ return false;
232
+ const inRange = event.at !== null && event.at >= this.from && event.at <= to;
233
+ if (inRange && !intervalOffsets.has(this.seekKey))
234
+ intervalOffsets.set(this.seekKey, offset);
235
+ const pastRange = this.collector.add(event);
236
+ return pastRange && ++postRangeLines >= lookahead;
237
+ });
238
+ }
239
+ catch (error) {
240
+ if (error instanceof TranscriptReadError)
241
+ throw error;
242
+ throw new TranscriptReadError('unreadable', `${this.harness} transcript could not be read: ${error instanceof Error ? error.message : String(error)}`);
243
+ }
244
+ finally {
245
+ if (fd !== null)
246
+ closeSync(fd);
247
+ }
248
+ return this.collector.finish(fileRevision(this.path) ?? `${size}`, this.harness);
249
+ }
250
+ }
251
+ function lineFileReader(harness, locate, parse) {
252
+ const find = (threadId) => {
253
+ const path = locate(threadId);
254
+ if (!path)
255
+ throw new TranscriptReadError('missing', `${harness} transcript for ${threadId} is unavailable: file was not found`);
256
+ return path;
257
+ };
258
+ return {
259
+ revision: (threadId) => { const path = locate(threadId); return path ? fileRevision(path) : null; },
260
+ // after passing `to`, a one-shot read scans a fixed lookahead window for timestamp disorder before stopping
261
+ read: async (threadId, range) => new LineFileCursor(harness, find(threadId), parse, range.from).advance(range.to, POST_RANGE_LOOKAHEAD_LINES),
262
+ tail: (threadId, from) => {
263
+ let cursor = null;
264
+ return {
265
+ // the file is located on the first advance, so a tail opened before the thread exists fails as `missing`
266
+ // there rather than at construction
267
+ advance: async (to) => (cursor ??= new LineFileCursor(harness, find(threadId), parse, from)).advance(to),
268
+ close: () => { cursor = null; },
269
+ };
270
+ },
271
+ };
272
+ }
273
+ export const claudeTranscript = lineFileReader('claude', (threadId) => claudeTranscriptPath(threadId), claudeEvent);
274
+ export const codexTranscript = lineFileReader('codex', (threadId) => codexRolloutPath(threadId), codexEvent);
275
+ export const piTranscript = lineFileReader('pi', (threadId) => piSessionPath(threadId), piEvent);
276
+ export const geminiTranscript = lineFileReader('gemini', (threadId) => geminiTranscriptPath(threadId), geminiEvent);
277
+ export const openclawTranscript = lineFileReader('openclaw', (threadId) => openclawTranscriptPath(threadId), openclawEvent);
278
+ // OpenCode has no per-thread file: the store's revision is the change token, and one export per
279
+ // revision is parsed and kept, so repeated interval reads of a quiet thread cost nothing new.
280
+ const opencodeExports = new Map();
281
+ export function opencodeTranscriptReader(root = opencodeStoreRoot(), load = opencodeExport) {
282
+ const reader = {
283
+ revision: () => opencodeStoreRevision(root),
284
+ read: async (threadId, range) => {
285
+ const revision = opencodeStoreRevision(root);
286
+ if (!revision)
287
+ throw new TranscriptReadError('missing', `opencode transcript for ${threadId} is unavailable: store was not found`);
288
+ const key = `${root}:${threadId}`;
289
+ let cached = opencodeExports.get(key);
290
+ if (!cached || cached.revision !== revision) {
291
+ let exported;
292
+ try {
293
+ exported = load(threadId);
294
+ }
295
+ catch (error) {
296
+ throw new TranscriptReadError('unreadable', `opencode transcript could not be exported: ${error instanceof Error ? error.message : String(error)}`);
297
+ }
298
+ let value;
299
+ try {
300
+ value = JSON.parse(exported);
301
+ }
302
+ catch (error) {
303
+ throw new TranscriptReadError('invalid', `opencode transcript cannot be parsed: ${error instanceof Error ? error.message : String(error)}`);
304
+ }
305
+ cached = { revision, events: opencodeEvents(value) };
306
+ opencodeExports.set(key, cached);
307
+ }
308
+ const collector = new IntervalCollector(range);
309
+ for (const event of cached.events)
310
+ collector.add(event);
311
+ return collector.finish(revision, 'opencode');
312
+ },
313
+ };
314
+ return {
315
+ ...reader,
316
+ // no file grows here: an open interval is re-collected from the cached export, which is one export per revision
317
+ tail: (threadId, from) => ({ advance: (to) => reader.read(threadId, { from, to }), close: () => { } }),
318
+ };
319
+ }
320
+ export const opencodeTranscript = opencodeTranscriptReader();
321
+ const hermesRoot = () => process.env.HERMES_HOME || join(homedir(), '.hermes', 'profiles', 'default');
322
+ function hermesRevision(root) {
323
+ try {
324
+ const stat = statSync(join(root, 'state.db'));
325
+ return `${stat.size}:${Math.floor(stat.mtimeMs)}`;
326
+ }
327
+ catch {
328
+ return null;
329
+ }
330
+ }
331
+ function hermesExport(threadId) {
332
+ return execFileSync(process.env.SPEXCODE_HERMES_CMD || 'hermes', ['sessions', 'export', '--format', 'jsonl', '--session-id', threadId, '--yes'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
333
+ }
334
+ const hermesExports = new Map();
335
+ export function hermesTranscriptReader(root = hermesRoot(), load = hermesExport) {
336
+ const read = async (threadId, range) => {
337
+ const revision = hermesRevision(root);
338
+ if (!revision)
339
+ throw new TranscriptReadError('missing', `hermes transcript for ${threadId} is unavailable: state.db was not found`);
340
+ const key = `${root}:${threadId}`;
341
+ let cached = hermesExports.get(key);
342
+ if (!cached || cached.revision !== revision) {
343
+ let exported;
344
+ try {
345
+ exported = load(threadId);
346
+ }
347
+ catch (error) {
348
+ throw new TranscriptReadError('unreadable', `hermes transcript could not be exported: ${error instanceof Error ? error.message : String(error)}`);
349
+ }
350
+ let value;
351
+ try {
352
+ value = JSON.parse(exported);
353
+ }
354
+ catch (error) {
355
+ throw new TranscriptReadError('invalid', `hermes transcript cannot be parsed: ${error instanceof Error ? error.message : String(error)}`);
356
+ }
357
+ cached = { revision, events: hermesEvents(value) };
358
+ hermesExports.set(key, cached);
359
+ }
360
+ const collector = new IntervalCollector(range);
361
+ for (const event of cached.events)
362
+ collector.add(event);
363
+ return collector.finish(revision, 'hermes');
364
+ };
365
+ return { revision: () => hermesRevision(root), read, tail: (threadId, from) => ({ advance: (to) => read(threadId, { from, to }), close: () => { } }) };
366
+ }
367
+ export const hermesTranscript = hermesTranscriptReader();
368
+ export function unsupportedTranscript(harness) {
369
+ const refuse = async () => { throw new TranscriptReadError('unsupported', `${harness} does not support transcript access`); };
370
+ return {
371
+ revision: () => null,
372
+ read: refuse,
373
+ tail: () => ({ advance: refuse, close: () => { } }),
374
+ };
375
+ }
@@ -11,13 +11,14 @@ export type TranscriptTool = Readonly<{
11
11
  outputBytes: number;
12
12
  }>;
13
13
  export type TranscriptTurn = Readonly<{
14
- id: string | null;
14
+ id: string;
15
15
  at: number;
16
16
  role: 'user' | 'assistant';
17
17
  text?: string;
18
18
  tools?: readonly TranscriptTool[];
19
19
  }>;
20
20
  export type TranscriptRead = Readonly<{
21
+ revision: string;
21
22
  from: number;
22
23
  to: number;
23
24
  turns: readonly TranscriptTurn[];
@@ -26,11 +27,16 @@ export type TranscriptRead = Readonly<{
26
27
  omittedBytes: number;
27
28
  outOfOrderEvents: number;
28
29
  }>;
30
+ export type TranscriptTail = Readonly<{
31
+ advance(to: number): Promise<TranscriptRead>;
32
+ close(): void;
33
+ }>;
34
+ export type TranscriptReader = Readonly<{
35
+ revision(threadId: string): string | null;
36
+ read(threadId: string, range: TranscriptRange): Promise<TranscriptRead>;
37
+ tail(threadId: string, from: number): TranscriptTail;
38
+ }>;
29
39
  export declare class TranscriptReadError extends Error {
30
40
  readonly reason: 'unsupported' | 'missing' | 'unreadable' | 'invalid';
31
41
  constructor(reason: 'unsupported' | 'missing' | 'unreadable' | 'invalid', message: string);
32
42
  }
33
- export declare function readTranscript(harness: string, threadId: string, range: TranscriptRange): Promise<TranscriptRead>;
34
- export declare function readClaudeTranscript(threadId: string, range: TranscriptRange): Promise<TranscriptRead>;
35
- export declare function readCodexTranscript(threadId: string, range: TranscriptRange): Promise<TranscriptRead>;
36
- export declare function unsupportedTranscriptReader(harness: string, _threadId: string, _range: TranscriptRange): Promise<TranscriptRead>;
@@ -0,0 +1,11 @@
1
+ // THE NORMALIZED TRANSCRIPT. Every harness keeps its conversation in a private shape; this is the one shape
2
+ // every surface reads instead: user and assistant prose, tool calls with their input, and each call's output
3
+ // once the harness recorded it. Nothing here imports Node, so a browser renderer and a Node reader share it.
4
+ export class TranscriptReadError extends Error {
5
+ reason;
6
+ constructor(reason, message) {
7
+ super(message);
8
+ this.reason = reason;
9
+ this.name = 'TranscriptReadError';
10
+ }
11
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@spexcode/transcript",
3
+ "version": "0.7.0-next.1",
4
+ "type": "module",
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
+ "files": [
7
+ "dist"
8
+ ],
9
+ "exports": {
10
+ ".": "./dist/index.js",
11
+ "./frames": "./dist/frames.js",
12
+ "./package.json": "./package.json"
13
+ },
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "scripts": {
21
+ "build": "node ../../scripts/build-dist.mjs",
22
+ "prepublishOnly": "node ../../scripts/release-publish.mjs --from-package-publish",
23
+ "test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^18.19.0",
27
+ "tsx": "^4.19.2",
28
+ "typescript": "^5.6.3"
29
+ }
30
+ }
@@ -0,0 +1,24 @@
1
+ Copyright (c) Squirrel Chat et al., All rights reserved.
2
+
3
+ Redistribution and use in source and binary forms, with or without
4
+ modification, are permitted provided that the following conditions are met:
5
+
6
+ 1. Redistributions of source code must retain the above copyright notice, this
7
+ list of conditions and the following disclaimer.
8
+ 2. Redistributions in binary form must reproduce the above copyright notice,
9
+ this list of conditions and the following disclaimer in the
10
+ documentation and/or other materials provided with the distribution.
11
+ 3. Neither the name of the copyright holder nor the names of its contributors
12
+ may be used to endorse or promote products derived from this software without
13
+ specific prior written permission.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.