spexcode 0.7.0-next.0 → 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.
- package/node_modules/@spexcode/session-application/package.json +5 -5
- package/node_modules/@spexcode/session-events/package.json +2 -2
- package/node_modules/@spexcode/session-protocol/package.json +1 -1
- package/node_modules/@spexcode/session-runtime/package.json +2 -2
- package/node_modules/@spexcode/session-selflaunch/package.json +3 -3
- package/node_modules/@spexcode/session-topology/package.json +2 -2
- package/node_modules/@spexcode/spec-cli/dist/sessions.js +1 -1
- package/node_modules/@spexcode/spec-cli/package.json +7 -7
- package/node_modules/@spexcode/spec-core/package.json +1 -1
- package/node_modules/@spexcode/spec-eval/package.json +2 -2
- package/node_modules/@spexcode/spec-forge/package.json +2 -2
- package/node_modules/@spexcode/transcript/dist/frames.d.ts +2 -0
- package/node_modules/@spexcode/transcript/dist/frames.js +2 -0
- package/node_modules/@spexcode/transcript/dist/parsers.d.ts +3 -0
- package/node_modules/@spexcode/transcript/dist/parsers.js +111 -1
- package/node_modules/@spexcode/transcript/dist/readers.d.ts +6 -0
- package/node_modules/@spexcode/transcript/dist/readers.js +80 -1
- package/node_modules/@spexcode/transcript/package.json +1 -1
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/session-application",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.1",
|
|
4
4
|
"publishConfig": { "access": "public" },
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Adopter-owned composition of session protocol and topology transactions.",
|
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
"test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@spexcode/session-events": "0.7.0-next.
|
|
20
|
-
"@spexcode/session-protocol": "0.7.0-next.
|
|
21
|
-
"@spexcode/session-runtime": "0.7.0-next.
|
|
22
|
-
"@spexcode/session-topology": "0.7.0-next.
|
|
19
|
+
"@spexcode/session-events": "0.7.0-next.1",
|
|
20
|
+
"@spexcode/session-protocol": "0.7.0-next.1",
|
|
21
|
+
"@spexcode/session-runtime": "0.7.0-next.1",
|
|
22
|
+
"@spexcode/session-topology": "0.7.0-next.1"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/node": "^20.16.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/session-events",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.1",
|
|
4
4
|
"publishConfig": { "access": "public" },
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Append-only session facts and deterministic ordered replay inputs.",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"prepublishOnly": "node ../../scripts/release-publish.mjs --from-package-publish",
|
|
16
16
|
"test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
|
|
17
17
|
},
|
|
18
|
-
"dependencies": { "@spexcode/session-protocol": "0.7.0-next.
|
|
18
|
+
"dependencies": { "@spexcode/session-protocol": "0.7.0-next.1" },
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^20.16.0",
|
|
21
21
|
"tsx": "^4.19.2",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/session-runtime",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.1",
|
|
4
4
|
"publishConfig": { "access": "public" },
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Adopter-owned bindings between protocol addresses and native runtime identities.",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"prepublishOnly": "node ../../scripts/release-publish.mjs --from-package-publish",
|
|
16
16
|
"test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
|
|
17
17
|
},
|
|
18
|
-
"dependencies": { "@spexcode/session-protocol": "0.7.0-next.
|
|
18
|
+
"dependencies": { "@spexcode/session-protocol": "0.7.0-next.1" },
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^20.16.0",
|
|
21
21
|
"tsx": "^4.19.2",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/session-selflaunch",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.1",
|
|
4
4
|
"publishConfig": { "access": "public" },
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A fail-closed self-launch adopter for the SpexCode session protocol.",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@spexcode/session-protocol": "0.7.0-next.
|
|
28
|
-
"@spexcode/session-runtime": "0.7.0-next.
|
|
27
|
+
"@spexcode/session-protocol": "0.7.0-next.1",
|
|
28
|
+
"@spexcode/session-runtime": "0.7.0-next.1"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^20.16.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/session-topology",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.1",
|
|
4
4
|
"publishConfig": { "access": "public" },
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A neutral SQLite relation model for session addresses.",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"prepublishOnly": "node ../../scripts/release-publish.mjs --from-package-publish",
|
|
16
16
|
"test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
|
|
17
17
|
},
|
|
18
|
-
"dependencies": { "@spexcode/session-protocol": "0.7.0-next.
|
|
18
|
+
"dependencies": { "@spexcode/session-protocol": "0.7.0-next.1" },
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^20.16.0",
|
|
21
21
|
"tsx": "^4.19.2",
|
|
@@ -2988,7 +2988,7 @@ async function prepareSession(prompt, parent, launcher, name, context) {
|
|
|
2988
2988
|
try {
|
|
2989
2989
|
gitMutationStarted = true;
|
|
2990
2990
|
traceSessionCreate(id, requestDigest, phase, 'start', 'worktree-add');
|
|
2991
|
-
const added = await withGitAbortSignal(signal, () => gitTry(['-C', root, 'worktree', 'add', '-b', branch, path, startPoint], { extraEnv: DEFER_FOOTPRINT_REFRESH }));
|
|
2991
|
+
const added = await withGitAbortSignal(signal, () => gitTry(['-C', root, 'worktree', 'add', '--no-track', '-b', branch, path, startPoint], { extraEnv: DEFER_FOOTPRINT_REFRESH }));
|
|
2992
2992
|
traceSessionCreate(id, requestDigest, phase, 'finish', 'worktree-add');
|
|
2993
2993
|
if (added.ok)
|
|
2994
2994
|
Object.assign(owned, { path: true, worktree: true, branch: true });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/spec-cli",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SpexCode CLI + server. The root spexcode package delegates to this compiled package; dashboard assets live in @spexcode/spec-dashboard.",
|
|
6
6
|
"bin": {
|
|
@@ -35,12 +35,12 @@
|
|
|
35
35
|
"test": "tsx --import ../scripts/test-home.mjs --test src/*.test.ts"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@spexcode/session-application": "0.7.0-next.
|
|
39
|
-
"@spexcode/session-selflaunch": "0.7.0-next.
|
|
40
|
-
"@spexcode/spec-core": "0.7.0-next.
|
|
41
|
-
"@spexcode/spec-eval": "0.7.0-next.
|
|
42
|
-
"@spexcode/spec-forge": "0.7.0-next.
|
|
43
|
-
"@spexcode/transcript": "0.7.0-next.
|
|
38
|
+
"@spexcode/session-application": "0.7.0-next.1",
|
|
39
|
+
"@spexcode/session-selflaunch": "0.7.0-next.1",
|
|
40
|
+
"@spexcode/spec-core": "0.7.0-next.1",
|
|
41
|
+
"@spexcode/spec-eval": "0.7.0-next.1",
|
|
42
|
+
"@spexcode/spec-forge": "0.7.0-next.1",
|
|
43
|
+
"@spexcode/transcript": "0.7.0-next.1",
|
|
44
44
|
"smol-toml": "^1.8.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/spec-eval",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dist/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"test": "tsx --import ../scripts/test-home.mjs --test src/*.test.ts"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@spexcode/spec-core": "0.7.0-next.
|
|
29
|
+
"@spexcode/spec-core": "0.7.0-next.1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^20.16.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/spec-forge",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"test": "tsx --test src/*.test.ts"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@spexcode/spec-core": "0.7.0-next.
|
|
25
|
+
"@spexcode/spec-core": "0.7.0-next.1"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^20.16.0",
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type TranscriptRead, type TranscriptReader, type TranscriptTool, type TranscriptTurn } from './turns.js';
|
|
2
|
+
export { TranscriptReadError } from './turns.js';
|
|
3
|
+
export type { TranscriptRange, TranscriptRead, TranscriptReader, TranscriptTail, TranscriptTool, TranscriptTurn } from './turns.js';
|
|
2
4
|
export type StreamTool = Readonly<Omit<TranscriptTool, 'output'> & {
|
|
3
5
|
output?: null;
|
|
4
6
|
}>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { TranscriptReadError } from './turns.js';
|
|
2
|
+
// the browser-safe entry is complete on its own: the normalized shape travels with the frames that carry it
|
|
3
|
+
export { TranscriptReadError } from './turns.js';
|
|
2
4
|
export const isErrorFrame = (frame) => 'error' in frame;
|
|
3
5
|
// The revision an absent source reports: the thread has not started writing. Not an error.
|
|
4
6
|
export const ABSENT_REVISION = 'absent';
|
|
@@ -28,6 +28,9 @@ 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
30
|
export declare function piEvent(value: unknown): ParsedEvent | null;
|
|
31
|
+
export declare function geminiEvent(value: unknown): ParsedEvent | null;
|
|
32
|
+
export declare function openclawEvent(value: unknown): ParsedEvent | null;
|
|
33
|
+
export declare function hermesEvents(value: unknown): ParsedEvent[];
|
|
31
34
|
export declare function opencodeEvents(value: unknown): ParsedEvent[];
|
|
32
35
|
export declare class IntervalCollector {
|
|
33
36
|
readonly turns: MutableTurn[];
|
|
@@ -51,7 +51,7 @@ const compact = (value) => {
|
|
|
51
51
|
}
|
|
52
52
|
};
|
|
53
53
|
const lineCount = (value) => value ? value.split(/\r?\n/).length : 0;
|
|
54
|
-
// --- the
|
|
54
|
+
// --- the seven native shapes -------------------------------------------------------------------------------
|
|
55
55
|
export function claudeEvent(value) {
|
|
56
56
|
const entry = object(value);
|
|
57
57
|
const message = object(entry?.message);
|
|
@@ -154,6 +154,116 @@ export function piEvent(value) {
|
|
|
154
154
|
}
|
|
155
155
|
return null;
|
|
156
156
|
}
|
|
157
|
+
export function geminiEvent(value) {
|
|
158
|
+
const entry = object(value);
|
|
159
|
+
const message = (entry?.type === 'user' || entry?.type === 'gemini')
|
|
160
|
+
? entry
|
|
161
|
+
: object(items(entry?.messages)[0]) ?? object(items(object(entry?.$set)?.messages)[0]);
|
|
162
|
+
if (!entry || !message)
|
|
163
|
+
return null;
|
|
164
|
+
const eventAt = at(message) ?? at(entry);
|
|
165
|
+
if (eventAt === null)
|
|
166
|
+
return { at: null, turn: null };
|
|
167
|
+
const content = message.content;
|
|
168
|
+
const blocks = items(content);
|
|
169
|
+
const outputs = blocks.flatMap((blockValue) => {
|
|
170
|
+
const block = object(blockValue);
|
|
171
|
+
const response = object(block?.functionResponse);
|
|
172
|
+
const id = string(response?.id);
|
|
173
|
+
return id ? [{ id, text: compact(response?.response ?? '') }] : [];
|
|
174
|
+
});
|
|
175
|
+
if (outputs.length)
|
|
176
|
+
return { at: eventAt, turn: null, toolOutputs: outputs };
|
|
177
|
+
if (message.type === 'user') {
|
|
178
|
+
const text = typeof content === 'string' ? string(content) : blocks.map((block) => string(object(block)?.text)).filter(Boolean).join('\n') || null;
|
|
179
|
+
return text ? { at: eventAt, turn: { id: idOf(message), at: eventAt, role: 'user', text, tools: [] } } : null;
|
|
180
|
+
}
|
|
181
|
+
if (message.type === 'gemini') {
|
|
182
|
+
const turn = { id: idOf(message), at: eventAt, role: 'assistant', tools: [] };
|
|
183
|
+
if (typeof content === 'string')
|
|
184
|
+
turn.text = string(content) ?? undefined;
|
|
185
|
+
for (const callValue of items(message.toolCalls)) {
|
|
186
|
+
const call = object(callValue);
|
|
187
|
+
const id = string(call?.id) ?? `tool-${turn.tools.length}`;
|
|
188
|
+
turn.tools.push({ id, name: string(call?.name) ?? 'tool', input: call?.args === undefined ? undefined : compact(call.args), outputLines: 0, outputBytes: 0 });
|
|
189
|
+
}
|
|
190
|
+
if (!turn.text && !turn.tools.length)
|
|
191
|
+
return null;
|
|
192
|
+
return { at: eventAt, turn };
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
export function openclawEvent(value) {
|
|
197
|
+
const entry = object(value);
|
|
198
|
+
const message = object(entry?.message);
|
|
199
|
+
if (!entry || entry.type !== 'message' || !message)
|
|
200
|
+
return null;
|
|
201
|
+
const eventAt = at(message) ?? at(entry);
|
|
202
|
+
if (eventAt === null)
|
|
203
|
+
return { at: null, turn: null };
|
|
204
|
+
if (message.role === 'user') {
|
|
205
|
+
const text = blockText(message.content);
|
|
206
|
+
return text ? { at: eventAt, turn: { id: idOf(entry) ?? idOf(message), at: eventAt, role: 'user', text, tools: [] } } : null;
|
|
207
|
+
}
|
|
208
|
+
if (message.role === 'toolResult') {
|
|
209
|
+
const id = string(message.toolCallId);
|
|
210
|
+
return id ? { at: eventAt, turn: null, toolOutputs: [{ id, text: blockText(message.content) ?? compact(message.content ?? '') }] } : null;
|
|
211
|
+
}
|
|
212
|
+
if (message.role === 'assistant') {
|
|
213
|
+
const turn = { id: idOf(entry) ?? idOf(message), at: eventAt, role: 'assistant', tools: [] };
|
|
214
|
+
for (const blockValue of items(message.content)) {
|
|
215
|
+
const block = object(blockValue);
|
|
216
|
+
if (block?.type === 'text')
|
|
217
|
+
turn.text = [turn.text, string(block.text)].filter(Boolean).join('\n') || undefined;
|
|
218
|
+
if (block?.type === 'toolCall') {
|
|
219
|
+
const id = string(block.id) ?? `tool-${turn.tools.length}`;
|
|
220
|
+
turn.tools.push({ id, name: string(block.name) ?? 'tool', input: block.arguments === undefined ? undefined : compact(block.arguments), outputLines: 0, outputBytes: 0 });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return turn.text || turn.tools.length ? { at: eventAt, turn } : null;
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
export function hermesEvents(value) {
|
|
228
|
+
const root = object(value);
|
|
229
|
+
const events = [];
|
|
230
|
+
for (const messageValue of items(root?.messages)) {
|
|
231
|
+
const message = object(messageValue);
|
|
232
|
+
if (!message)
|
|
233
|
+
continue;
|
|
234
|
+
const eventAt = at(message);
|
|
235
|
+
if (eventAt === null) {
|
|
236
|
+
events.push({ at: null, turn: null });
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const role = message.role;
|
|
240
|
+
if (role === 'user') {
|
|
241
|
+
const text = string(message.content);
|
|
242
|
+
if (text)
|
|
243
|
+
events.push({ at: eventAt, turn: { id: idOf(message), at: eventAt, role: 'user', text, tools: [] } });
|
|
244
|
+
}
|
|
245
|
+
else if (role === 'assistant') {
|
|
246
|
+
const turn = { id: idOf(message), at: eventAt, role: 'assistant', tools: [] };
|
|
247
|
+
const text = string(message.content);
|
|
248
|
+
if (text)
|
|
249
|
+
turn.text = text;
|
|
250
|
+
for (const callValue of items(message.tool_calls)) {
|
|
251
|
+
const call = object(callValue);
|
|
252
|
+
const fn = object(call?.function);
|
|
253
|
+
const id = string(call?.id) ?? `tool-${turn.tools.length}`;
|
|
254
|
+
turn.tools.push({ id, name: string(fn?.name) ?? 'tool', input: fn?.arguments === undefined ? undefined : compact(fn.arguments), outputLines: 0, outputBytes: 0 });
|
|
255
|
+
}
|
|
256
|
+
if (turn.text || turn.tools.length)
|
|
257
|
+
events.push({ at: eventAt, turn });
|
|
258
|
+
}
|
|
259
|
+
else if (role === 'tool') {
|
|
260
|
+
const id = string(message.tool_call_id);
|
|
261
|
+
if (id)
|
|
262
|
+
events.push({ at: eventAt, turn: null, toolOutputs: [{ id, text: compact(message.content ?? '') }] });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return events;
|
|
266
|
+
}
|
|
157
267
|
// OpenCode's export is one JSON document, not a line stream: every message arrives with its parts, and a tool
|
|
158
268
|
// part already carries its own result — so its turn is complete on arrival, and a part still running simply has
|
|
159
269
|
// no output yet.
|
|
@@ -2,9 +2,15 @@ import { type TranscriptReader } from './turns.js';
|
|
|
2
2
|
export declare function claudeTranscriptPath(threadId: string, root?: string): string | null;
|
|
3
3
|
export declare function codexRolloutPath(threadId: string, root?: string): string | null;
|
|
4
4
|
export declare function piSessionPath(threadId: string, root?: string): string | null;
|
|
5
|
+
export declare function geminiTranscriptPath(threadId: string, root?: string): string | null;
|
|
6
|
+
export declare function openclawTranscriptPath(threadId: string, root?: string): string | null;
|
|
5
7
|
export declare const claudeTranscript: TranscriptReader;
|
|
6
8
|
export declare const codexTranscript: TranscriptReader;
|
|
7
9
|
export declare const piTranscript: TranscriptReader;
|
|
10
|
+
export declare const geminiTranscript: TranscriptReader;
|
|
11
|
+
export declare const openclawTranscript: TranscriptReader;
|
|
8
12
|
export declare function opencodeTranscriptReader(root?: string, load?: (threadId: string) => string): TranscriptReader;
|
|
9
13
|
export declare const opencodeTranscript: TranscriptReader;
|
|
14
|
+
export declare function hermesTranscriptReader(root?: string, load?: (threadId: string) => string): TranscriptReader;
|
|
15
|
+
export declare const hermesTranscript: TranscriptReader;
|
|
10
16
|
export declare function unsupportedTranscript(harness: string): TranscriptReader;
|
|
@@ -3,7 +3,7 @@ import { closeSync, openSync, readFileSync, readSync, readdirSync, statSync } fr
|
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { TranscriptReadError } from './turns.js';
|
|
6
|
-
import { IntervalCollector, claudeEvent, codexEvent, opencodeEvents, piEvent } from './parsers.js';
|
|
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,
|
|
8
8
|
// Codex's rollout, pi's session JSONL, OpenCode's store behind `opencode export` — and this module is the only
|
|
9
9
|
// place that knows where. It answers exactly one question for every harness: "what happened in this thread
|
|
@@ -72,6 +72,36 @@ export function piSessionPath(threadId, root = piSessionsRoot()) {
|
|
|
72
72
|
}
|
|
73
73
|
return null;
|
|
74
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); }
|
|
75
105
|
const opencodeStoreRoot = () => process.env.SPEXCODE_OPENCODE_DATA_DIR
|
|
76
106
|
|| join(process.env.XDG_DATA_HOME || join(homedir(), '.local', 'share'), 'opencode');
|
|
77
107
|
function opencodeStoreRevision(root) {
|
|
@@ -243,6 +273,8 @@ function lineFileReader(harness, locate, parse) {
|
|
|
243
273
|
export const claudeTranscript = lineFileReader('claude', (threadId) => claudeTranscriptPath(threadId), claudeEvent);
|
|
244
274
|
export const codexTranscript = lineFileReader('codex', (threadId) => codexRolloutPath(threadId), codexEvent);
|
|
245
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);
|
|
246
278
|
// OpenCode has no per-thread file: the store's revision is the change token, and one export per
|
|
247
279
|
// revision is parsed and kept, so repeated interval reads of a quiet thread cost nothing new.
|
|
248
280
|
const opencodeExports = new Map();
|
|
@@ -286,6 +318,53 @@ export function opencodeTranscriptReader(root = opencodeStoreRoot(), load = open
|
|
|
286
318
|
};
|
|
287
319
|
}
|
|
288
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();
|
|
289
368
|
export function unsupportedTranscript(harness) {
|
|
290
369
|
const refuse = async () => { throw new TranscriptReadError('unsupported', `${harness} does not support transcript access`); };
|
|
291
370
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/transcript",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.1",
|
|
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": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spexcode",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SpexCode — a spec-driven, self-developing dev tool. The `spex` CLI + spec server reads the .spec tree and its git history, and serves the dashboard.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"prepack": "node scripts/sync-init-plugins.mjs --check && node scripts/prepack.mjs"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@spexcode/spec-cli": "0.7.0-next.
|
|
47
|
+
"@spexcode/spec-cli": "0.7.0-next.1"
|
|
48
48
|
},
|
|
49
49
|
"publishConfig": {
|
|
50
50
|
"access": "public"
|