@neta-art/cohub-cli 10.6.0 → 10.8.0
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/README.md +8 -5
- package/dist/board-kernel.js +207 -119
- package/dist/commands/boards/examples.js +16 -16
- package/dist/commands/boards.js +1 -1
- package/dist/commands/generations.js +23 -2
- package/dist/commands/runtime.js +3 -14
- package/dist/runtime/harnesses/chained-transcript.d.ts +24 -0
- package/dist/runtime/harnesses/chained-transcript.js +62 -0
- package/dist/runtime/harnesses/claude-transcript.d.ts +8 -0
- package/dist/runtime/harnesses/claude-transcript.js +138 -0
- package/dist/runtime/harnesses/claude.js +79 -2
- package/dist/runtime/harnesses/codex.js +1 -1
- package/dist/runtime/harnesses/cursor-transcript.d.ts +37 -0
- package/dist/runtime/harnesses/cursor-transcript.js +183 -0
- package/dist/runtime/harnesses/cursor.d.ts +24 -0
- package/dist/runtime/harnesses/cursor.js +620 -0
- package/dist/runtime/harnesses/index.d.ts +1 -3
- package/dist/runtime/harnesses/index.js +4 -10
- package/dist/runtime/harnesses/pi.js +1 -1
- package/dist/runtime/harnesses/stream-json.d.ts +7 -1
- package/dist/runtime/harnesses/stream-json.js +35 -12
- package/dist/runtime/harnesses/types.d.ts +10 -5
- package/dist/runtime/harnesses/workbuddy-transcript.d.ts +8 -0
- package/dist/runtime/harnesses/workbuddy-transcript.js +127 -0
- package/dist/runtime/harnesses/workbuddy.d.ts +1 -1
- package/dist/runtime/harnesses/workbuddy.js +31 -7
- package/dist/runtime/json-rpc.d.ts +2 -0
- package/dist/runtime/json-rpc.js +9 -0
- package/dist/runtime/native/attach.d.ts +1 -1
- package/dist/runtime/native/attach.js +1 -4
- package/dist/runtime/native/execution.js +14 -3
- package/dist/runtime/native/ingest.d.ts +14 -0
- package/dist/runtime/native/ingest.js +51 -5
- package/dist/runtime/native/sessions.js +6 -1
- package/dist/runtime/native/transcript.d.ts +19 -0
- package/dist/runtime/native/transcript.js +33 -2
- package/package.json +3 -3
|
@@ -10,9 +10,9 @@ export const BOARD_EXAMPLES = {
|
|
|
10
10
|
value: {
|
|
11
11
|
board: { title: "Launch plan" },
|
|
12
12
|
items: {
|
|
13
|
-
title: { type: "text", position: { x: 80, y: 60 }, props: { text: "Launch plan", fontSize:
|
|
14
|
-
note: { type: "text", position: { x: 80, y:
|
|
15
|
-
goal: { type: "shape", position: { x: 600, y: 60 }, size: { width:
|
|
13
|
+
title: { type: "text", position: { x: 80, y: 60 }, props: { text: "Launch plan", fontSize: 40, fontWeight: 700 } },
|
|
14
|
+
note: { type: "text", position: { x: 80, y: 128 }, props: { text: "Ship the beta to ten design partners, then widen.", width: 420 } },
|
|
15
|
+
goal: { type: "shape", position: { x: 600, y: 60 }, size: { width: 200, height: 140 }, style: { stroke: "green", fill: "green", fillOpacity: 0.12, radius: 16 }, props: { text: "Beta in March" } },
|
|
16
16
|
},
|
|
17
17
|
},
|
|
18
18
|
},
|
|
@@ -29,14 +29,14 @@ export const BOARD_EXAMPLES = {
|
|
|
29
29
|
},
|
|
30
30
|
},
|
|
31
31
|
slides: {
|
|
32
|
-
description: "
|
|
32
|
+
description: "960×540 frames as slides; children move and clip with their frame.",
|
|
33
33
|
value: {
|
|
34
34
|
items: {
|
|
35
|
-
s1: { type: "frame", position: { x: 0, y: 0 }, size: { width:
|
|
36
|
-
"s1-title": { type: "text", parent: "s1", position: { x:
|
|
37
|
-
"s1-sub": { type: "text", parent: "s1", position: { x:
|
|
38
|
-
s2: { type: "frame", position: { x:
|
|
39
|
-
"s2-title": { type: "text", parent: "s2", position: { x:
|
|
35
|
+
s1: { type: "frame", position: { x: 0, y: 0 }, size: { width: 960, height: 540 }, props: { label: "Intro" } },
|
|
36
|
+
"s1-title": { type: "text", parent: "s1", position: { x: 72, y: 72 }, props: { text: "Special relativity", fontSize: 56, fontWeight: 700 } },
|
|
37
|
+
"s1-sub": { type: "text", parent: "s1", position: { x: 72, y: 160 }, props: { text: "Nothing outruns light" } },
|
|
38
|
+
s2: { type: "frame", position: { x: 1080, y: 0 }, size: { width: 960, height: 540 }, props: { label: "Time dilation" } },
|
|
39
|
+
"s2-title": { type: "text", parent: "s2", position: { x: 72, y: 72 }, props: { text: "Moving clocks run slow", fontSize: 40, fontWeight: 700 } },
|
|
40
40
|
},
|
|
41
41
|
},
|
|
42
42
|
},
|
|
@@ -44,12 +44,12 @@ export const BOARD_EXAMPLES = {
|
|
|
44
44
|
description: "An animated lesson: entrances, a drawn arrow, typed text and camera moves with pause markers.",
|
|
45
45
|
value: {
|
|
46
46
|
items: {
|
|
47
|
-
s1: { type: "frame", size: { width:
|
|
48
|
-
title: { type: "text", parent: "s1", position: { x:
|
|
49
|
-
ruler: { type: "shape", parent: "s1", position: { x:
|
|
50
|
-
caption: { type: "text", parent: "s1", position: { x:
|
|
51
|
-
motion: { type: "arrow", parent: "s1", style: { stroke: "rose", strokeWidth:
|
|
52
|
-
glow: { type: "effect", parent: "s1", position: { x:
|
|
47
|
+
s1: { type: "frame", size: { width: 960, height: 540 }, props: { label: "Length contraction" } },
|
|
48
|
+
title: { type: "text", parent: "s1", position: { x: 72, y: 60 }, props: { text: "Moving rulers get shorter", fontSize: 40, fontWeight: 700 } },
|
|
49
|
+
ruler: { type: "shape", parent: "s1", position: { x: 120, y: 252 }, size: { width: 480, height: 40 }, style: { stroke: "blue", fill: "blue", fillOpacity: 0.15 }, props: { text: "1 m", fontSize: 16 } },
|
|
50
|
+
caption: { type: "text", parent: "s1", position: { x: 120, y: 336 }, props: { text: "L = L₀ √(1 − v²/c²)", font: "serif", reveal: 1 } },
|
|
51
|
+
motion: { type: "arrow", parent: "s1", style: { stroke: "rose", strokeWidth: 3 }, props: { start: { x: 120, y: 216 }, end: { x: 600, y: 216 }, label: "v → c" } },
|
|
52
|
+
glow: { type: "effect", parent: "s1", position: { x: 630, y: 224 }, size: { width: 96, height: 96 }, style: { fill: "amber" }, props: { kind: "glow" } },
|
|
53
53
|
},
|
|
54
54
|
animations: {
|
|
55
55
|
lecture: {
|
|
@@ -69,7 +69,7 @@ export const BOARD_EXAMPLES = {
|
|
|
69
69
|
description: "Custom Canvas content; use --module to print scripts/wave.js.",
|
|
70
70
|
value: {
|
|
71
71
|
items: {
|
|
72
|
-
wave: { type: "custom", position: { x: 0, y: 0 }, size: { width:
|
|
72
|
+
wave: { type: "custom", position: { x: 0, y: 0 }, size: { width: 480, height: 270 }, script: { src: "scripts/wave.js", params: { amplitude: 48 } } },
|
|
73
73
|
},
|
|
74
74
|
},
|
|
75
75
|
},
|
package/dist/commands/boards.js
CHANGED
|
@@ -151,7 +151,7 @@ function registerExportCommand(boards) {
|
|
|
151
151
|
.requiredOption("-o, --out <file>", "Output file; the extension picks the format. Sequences need %d, e.g. frames/%04d.png")
|
|
152
152
|
.option("--at <time>", "A moment, comma-separated moments, or start:end:step")
|
|
153
153
|
.option("--camera", "Follow the animation camera")
|
|
154
|
-
.option("--size <widthxheight>", "Camera
|
|
154
|
+
.option("--size <widthxheight>", "Camera output in pixels; frames keep their own size", "1600x900")
|
|
155
155
|
.option("--animation <id>", "Animation the --at times refer to")
|
|
156
156
|
.option("--frame <item-id>", "Export one frame as a page")
|
|
157
157
|
.option("--items <ids>", "Comma-separated item ids")
|
|
@@ -32,6 +32,20 @@ function parseValue(value) {
|
|
|
32
32
|
}
|
|
33
33
|
return value;
|
|
34
34
|
}
|
|
35
|
+
function parseTextInputs(values) {
|
|
36
|
+
return values.map((value) => {
|
|
37
|
+
const index = value.indexOf("=");
|
|
38
|
+
if (index < 0)
|
|
39
|
+
return error("Invalid --text", "Pass --text <role>=<text>.");
|
|
40
|
+
const role = value.slice(0, index).trim();
|
|
41
|
+
if (!role)
|
|
42
|
+
return error("Invalid --text", "Pass a role: --text <role>=<text>.");
|
|
43
|
+
const text = value.slice(index + 1);
|
|
44
|
+
if (!text)
|
|
45
|
+
return error("Invalid --text", 'Pass text after "=": --text <role>=<text>.');
|
|
46
|
+
return { role, text };
|
|
47
|
+
});
|
|
48
|
+
}
|
|
35
49
|
function parseParams(param, parameters) {
|
|
36
50
|
const result = parameters ? JSON.parse(parameters) : {};
|
|
37
51
|
for (const item of param ?? []) {
|
|
@@ -260,8 +274,9 @@ export function registerGenerations(program) {
|
|
|
260
274
|
program
|
|
261
275
|
.command("generate")
|
|
262
276
|
.description("Generate multimodal outputs")
|
|
263
|
-
.argument("
|
|
277
|
+
.argument("[prompt]", "Prompt text; optional when --text is given")
|
|
264
278
|
.requiredOption("-m, --model <model>", "Multimodal model ID from `cohub models ls --model-type multimodal`")
|
|
279
|
+
.option("--text <role=text>", "Text input with a role from the model's text inputs (`cohub models show <model>`); repeatable, split at the first =, text sent unchanged", collect, [])
|
|
265
280
|
.option("--image <path-or-url>", "Image input file path or URL; prefix with first_frame=, last_frame=, or reference_image= when needed; repeatable", collect, [])
|
|
266
281
|
.option("--video <path-or-url>", "Video input file path or URL; prefix with reference_video= when needed; repeatable", collect, [])
|
|
267
282
|
.option("--audio <path-or-url>", "Audio input file path or URL; prefix with reference_audio= when needed; repeatable", collect, [])
|
|
@@ -285,10 +300,15 @@ Examples:
|
|
|
285
300
|
cohub -s <space-id> generate "Smooth transition" -m seedance-2-0-fast --image first_frame=https://example.com/first.png --image last_frame=https://example.com/last.png
|
|
286
301
|
cohub -s <space-id> generate "Use these references" -m seedance-2-0-fast --image reference_image=https://example.com/a.png --image reference_image=https://example.com/b.png
|
|
287
302
|
cohub -s <space-id> generate "Lip-sync to the reference take" -m seedance-2-0-fast --image reference_image=https://example.com/portrait.png --audio reference_audio=https://example.com/speech.mp3
|
|
303
|
+
cohub -s <space-id> generate -m song2 --text "prompt=upbeat synthwave" --text "lyrics=[Verse] Neon lights" --param seed=42
|
|
288
304
|
cohub -s <space-id> generate "A calm lake" -m <model> --async
|
|
289
305
|
`)
|
|
290
306
|
.action(async (prompt, opts) => {
|
|
291
307
|
try {
|
|
308
|
+
const texts = parseTextInputs(opts.text);
|
|
309
|
+
if (prompt === undefined && texts.length === 0) {
|
|
310
|
+
return error("Missing prompt", "Pass a prompt or at least one --text <role>=<text>.");
|
|
311
|
+
}
|
|
292
312
|
const spaceId = await resolveSpace(program, { home: true });
|
|
293
313
|
const sessionId = envValue("COHUB_SESSION_ID");
|
|
294
314
|
const inputs = await Promise.all([
|
|
@@ -330,7 +350,8 @@ Examples:
|
|
|
330
350
|
return asset.publicUrl;
|
|
331
351
|
};
|
|
332
352
|
const content = [
|
|
333
|
-
{ type: "text", text: prompt },
|
|
353
|
+
...(prompt === undefined ? [] : [{ type: "text", text: prompt }]),
|
|
354
|
+
...texts.map(({ role, text }) => ({ type: "text", text, meta: { role } })),
|
|
334
355
|
...await Promise.all(inputs.map((input) => mediaContent(input, upload))),
|
|
335
356
|
];
|
|
336
357
|
const created = await client.generations.create({
|
package/dist/commands/runtime.js
CHANGED
|
@@ -10,7 +10,6 @@ import { RuntimeArchiveStore } from "../runtime/archive-store.js";
|
|
|
10
10
|
import { readNativeConfig, runtimeStateRoot, writeNativeConfig } from "../runtime/native/config.js";
|
|
11
11
|
import { MAX_IMPORT_CONCURRENCY, DEFAULT_IMPORT_CONCURRENCY } from "../runtime/native/ingest.js";
|
|
12
12
|
import { installPiExtension, piExtensionState } from "../runtime/native/install.js";
|
|
13
|
-
import { nativeSyncHarnesses } from "../runtime/harnesses/index.js";
|
|
14
13
|
import { controlRuntimeInstance, requestRuntimeInstance, runtimeInstanceDirectory } from "../runtime/instance.js";
|
|
15
14
|
import { atLeastLevel, diagnosticLevels, formatDiagnostic, formatImportProgress, formatNativeSync, printRuntimeSummary } from "../runtime/presentation.js";
|
|
16
15
|
import { readRuntimeDiagnosticEvents, RuntimeDiagnosticReader, runtimeDiagnosticsDirectory, serializeDiagnosticError } from "../runtime/diagnostics.js";
|
|
@@ -19,16 +18,6 @@ const reportFailure = (cause) => {
|
|
|
19
18
|
process.stderr.write(`Runtime failed: ${serializeDiagnosticError(cause).message}\n`);
|
|
20
19
|
process.exitCode = 1;
|
|
21
20
|
};
|
|
22
|
-
const SYNCED = nativeSyncHarnesses(LOCAL_HARNESSES);
|
|
23
|
-
const SYNCED_LABELS = SYNCED.map((harness) => LOCAL_HARNESS_SPECS[harness].label).join(" or ");
|
|
24
|
-
/** Native sync and import cover only harnesses whose terminal conversations sync. */
|
|
25
|
-
function parseSyncedHarnesses(values) {
|
|
26
|
-
const harnesses = parseRuntimeHarnesses(values);
|
|
27
|
-
const unsynced = harnesses.filter((harness) => !SYNCED.includes(harness));
|
|
28
|
-
if (unsynced.length)
|
|
29
|
-
throw new Error(`${unsynced.map((harness) => LOCAL_HARNESS_SPECS[harness].label).join(", ")} conversations do not sync; native sync covers ${SYNCED_LABELS}`);
|
|
30
|
-
return harnesses;
|
|
31
|
-
}
|
|
32
21
|
const megabytes = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
33
22
|
/** The bound Space of a directory, with its running Runtime; import and attach act through it. */
|
|
34
23
|
async function boundRuntime(program, dir, space) {
|
|
@@ -97,14 +86,14 @@ export function registerRuntime(program) {
|
|
|
97
86
|
runtime.command("detach")
|
|
98
87
|
.description("Pause native sync; keep all data")
|
|
99
88
|
.option("-s, --space <space>", "Target Space: ID, slug, or username/slug")
|
|
100
|
-
.option("--harness <name>", `${
|
|
89
|
+
.option("--harness <name>", `${LOCAL_HARNESSES.join(", ")}; repeatable`, (value, previous) => [...previous, value], [])
|
|
101
90
|
.option("--json", "JSON output")
|
|
102
91
|
.action(async (options) => {
|
|
103
92
|
try {
|
|
104
93
|
const { identity, spaceId, directory } = await boundRuntime(program, undefined, options.space);
|
|
105
94
|
const stateRoot = runtimeStateRoot(spaceId);
|
|
106
95
|
const config = await readNativeConfig(stateRoot, identity);
|
|
107
|
-
const paused = options.harness.length ?
|
|
96
|
+
const paused = options.harness.length ? parseRuntimeHarnesses(options.harness) : config?.harnesses ?? [...LOCAL_HARNESSES];
|
|
108
97
|
const harnesses = (config?.harnesses ?? []).filter((harness) => !paused.includes(harness));
|
|
109
98
|
if (config)
|
|
110
99
|
await writeNativeConfig(stateRoot, { ...config, harnesses, codexShared: harnesses.includes("codex") && config.codexShared });
|
|
@@ -144,7 +133,7 @@ export function registerRuntime(program) {
|
|
|
144
133
|
throw new Error(`Use a concurrency from 1 to ${MAX_IMPORT_CONCURRENCY}`);
|
|
145
134
|
const { spaceId, root, directory } = await boundRuntime(program, dir, options.space);
|
|
146
135
|
// The Runtime does the work, so an import survives this command: Ctrl-C only pauses it.
|
|
147
|
-
const filter = { harnesses: options.harness.length ?
|
|
136
|
+
const filter = { harnesses: options.harness.length ? parseRuntimeHarnesses(options.harness) : [], ...(options.session ? { nativeSessionId: options.session } : {}) };
|
|
148
137
|
const control = async (command, timeoutMs = 60_000) => {
|
|
149
138
|
const response = await controlRuntimeInstance(directory, "import", { ...filter, command, concurrency }, timeoutMs);
|
|
150
139
|
if (!response)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ContentBlock, LocalHarness } from "@neta-art/cohub";
|
|
2
|
+
import type { JsonRecord } from "../json-rpc.js";
|
|
3
|
+
import { type NativeTranscriptTurn, type TranscriptLine } from "../native/transcript.js";
|
|
4
|
+
import type { SessionHeader } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* `<data root>/projects/<project>/<session id>.jsonl`; sub-agent files live in deeper directories. A
|
|
7
|
+
* subdirectory's project name extends its parent's, so a prefix admits them; the header decides ownership.
|
|
8
|
+
*/
|
|
9
|
+
export declare function projectTranscript(path: string, root: string, project: string): boolean;
|
|
10
|
+
export declare function projectDirectory(path: string, root: string, project: string): boolean;
|
|
11
|
+
/** The session id is the file name; the first record carrying a `cwd` names the project. */
|
|
12
|
+
export declare function chainedSessionHeader(path: string, harness: LocalHarness, signal?: AbortSignal): Promise<SessionHeader | null>;
|
|
13
|
+
export declare function readChainedLines(path: string, signal?: AbortSignal): Promise<{
|
|
14
|
+
lines: TranscriptLine[];
|
|
15
|
+
prefixes: Map<number, string>;
|
|
16
|
+
}>;
|
|
17
|
+
/**
|
|
18
|
+
* The branch ending at the file's last linked record, oldest first. These formats only append, so a
|
|
19
|
+
* parent is the latest earlier record with that id: a rewound or repeated id never forms a cycle.
|
|
20
|
+
*/
|
|
21
|
+
export declare function chainBranch(lines: TranscriptLine[], id: (value: JsonRecord) => string, parent: (value: JsonRecord) => string): TranscriptLine[];
|
|
22
|
+
export declare function startTurn(line: TranscriptLine, key: string, parentKey: string | null, userContent: ContentBlock[], cloudTurnId: string): NativeTranscriptTurn;
|
|
23
|
+
/** Every branch record extends the Turn's bytes; only conversation records extend its content. */
|
|
24
|
+
export declare function extendTurn(turn: NativeTranscriptTurn, line: TranscriptLine, content: boolean): void;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { basename, dirname } from "node:path";
|
|
3
|
+
import { iso, readFirstMatchingRecord, readTranscriptLines } from "../native/transcript.js";
|
|
4
|
+
import { text } from "./process.js";
|
|
5
|
+
/**
|
|
6
|
+
* `<data root>/projects/<project>/<session id>.jsonl`; sub-agent files live in deeper directories. A
|
|
7
|
+
* subdirectory's project name extends its parent's, so a prefix admits them; the header decides ownership.
|
|
8
|
+
*/
|
|
9
|
+
export function projectTranscript(path, root, project) {
|
|
10
|
+
return path.endsWith(".jsonl") && projectDirectory(dirname(path), root, project);
|
|
11
|
+
}
|
|
12
|
+
export function projectDirectory(path, root, project) {
|
|
13
|
+
return dirname(path) === root && basename(path).startsWith(project);
|
|
14
|
+
}
|
|
15
|
+
/** The session id is the file name; the first record carrying a `cwd` names the project. */
|
|
16
|
+
export async function chainedSessionHeader(path, harness, signal) {
|
|
17
|
+
const value = await readFirstMatchingRecord(createReadStream(path, { signal }), (entry) => Boolean(text(entry.cwd)) && Boolean(text(entry.sessionId)));
|
|
18
|
+
return value ? { harness, nativeSessionId: basename(path, ".jsonl"), cwd: text(value.cwd), thread: "user" } : null;
|
|
19
|
+
}
|
|
20
|
+
export async function readChainedLines(path, signal) {
|
|
21
|
+
const read = await readTranscriptLines(createReadStream(path, { signal }));
|
|
22
|
+
if (!read.lines.length)
|
|
23
|
+
throw new Error("Native transcript is empty");
|
|
24
|
+
return read;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The branch ending at the file's last linked record, oldest first. These formats only append, so a
|
|
28
|
+
* parent is the latest earlier record with that id: a rewound or repeated id never forms a cycle.
|
|
29
|
+
*/
|
|
30
|
+
export function chainBranch(lines, id, parent) {
|
|
31
|
+
const latest = new Map();
|
|
32
|
+
const parents = [];
|
|
33
|
+
let leaf = -1;
|
|
34
|
+
lines.forEach((line, index) => {
|
|
35
|
+
const own = id(line.value);
|
|
36
|
+
parents[index] = -1;
|
|
37
|
+
if (!own)
|
|
38
|
+
return;
|
|
39
|
+
const before = latest.get(parent(line.value));
|
|
40
|
+
parents[index] = before ?? -1;
|
|
41
|
+
latest.set(own, index);
|
|
42
|
+
leaf = index;
|
|
43
|
+
});
|
|
44
|
+
const branch = [];
|
|
45
|
+
for (let index = leaf; index >= 0; index = parents[index] ?? -1)
|
|
46
|
+
branch.push(lines[index]);
|
|
47
|
+
return branch.reverse();
|
|
48
|
+
}
|
|
49
|
+
export function startTurn(line, key, parentKey, userContent, cloudTurnId) {
|
|
50
|
+
return {
|
|
51
|
+
key, parentKey, ...(cloudTurnId ? { cloudTurnId } : {}), userContent, messages: [], startedAt: iso(line.value.timestamp),
|
|
52
|
+
startBytes: line.startBytes, endBytes: line.endBytes, contentEndBytes: line.endBytes, boundaries: {}, sha256: line.sha256, result: null,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Every branch record extends the Turn's bytes; only conversation records extend its content. */
|
|
56
|
+
export function extendTurn(turn, line, content) {
|
|
57
|
+
turn.endBytes = line.endBytes;
|
|
58
|
+
turn.sha256 = line.sha256;
|
|
59
|
+
turn.boundaries[line.endBytes] = line.sha256;
|
|
60
|
+
if (content)
|
|
61
|
+
turn.contentEndBytes = line.endBytes;
|
|
62
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type NativeTranscript } from "../native/transcript.js";
|
|
2
|
+
export declare const CLAUDE_REPLY_ENDED: string[];
|
|
3
|
+
/**
|
|
4
|
+
* Claude Code appends one record per content block, linked by `parentUuid` (`logicalParentUuid`
|
|
5
|
+
* across a compaction). A Turn starts at a prompt; its tool results and an interruption share the
|
|
6
|
+
* prompt's `promptId`. Interactive sessions record each end (`turn_duration`), `-p` sessions do not.
|
|
7
|
+
*/
|
|
8
|
+
export declare function readClaudeTranscript(path: string, nativeSessionId: string, signal?: AbortSignal): Promise<NativeTranscript>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { record } from "../json-rpc.js";
|
|
2
|
+
import { iso } from "../native/transcript.js";
|
|
3
|
+
import { chainBranch, extendTurn, readChainedLines, startTurn } from "./chained-transcript.js";
|
|
4
|
+
import { array, text } from "./process.js";
|
|
5
|
+
import { streamContent, streamUsage } from "./stream-json.js";
|
|
6
|
+
const COMMAND = /^<command-name>\s*([^<]*)<\/command-name>/;
|
|
7
|
+
const COMMAND_ARGS = /<command-args>([\s\S]*?)<\/command-args>/;
|
|
8
|
+
const BASH_INPUT = /^<bash-input>([\s\S]*?)<\/bash-input>/;
|
|
9
|
+
/** Output of a local command or `!` shell, recorded as a user message but never sent as a prompt. */
|
|
10
|
+
const LOCAL_OUTPUT = /^<(?:local-command-(?:stdout|stderr|caveat)|bash-(?:stdout|stderr))>/;
|
|
11
|
+
const INTERRUPTED = "[Request interrupted by user";
|
|
12
|
+
export const CLAUDE_REPLY_ENDED = ["end_turn", "stop_sequence", "max_tokens", "refusal", "aborted", "error"];
|
|
13
|
+
const nonEmpty = (block) => !(block.type === "text" && !block.text) && !(block.type === "thinking" && !block.thinking);
|
|
14
|
+
/** A local `/command` or `!shell` is shown as typed; one that never reached the model is no Turn. */
|
|
15
|
+
function prompt(value) {
|
|
16
|
+
const content = typeof value === "string" ? [{ type: "text", text: value }] : streamContent(value).filter((block) => block.type === "text" || block.type === "image");
|
|
17
|
+
const first = content.find((block) => block.type === "text")?.text.trimStart() ?? "";
|
|
18
|
+
if (LOCAL_OUTPUT.test(first) || first.startsWith(INTERRUPTED))
|
|
19
|
+
return null;
|
|
20
|
+
const command = COMMAND.exec(first);
|
|
21
|
+
if (command)
|
|
22
|
+
return { content: [{ type: "text", text: `${command[1]?.trim() ?? ""} ${COMMAND_ARGS.exec(first)?.[1]?.trim() ?? ""}`.trim() }], local: true };
|
|
23
|
+
const bash = BASH_INPUT.exec(first);
|
|
24
|
+
if (bash)
|
|
25
|
+
return { content: [{ type: "text", text: `!${bash[1] ?? ""}` }], local: true };
|
|
26
|
+
const visible = content.filter(nonEmpty);
|
|
27
|
+
return visible.length ? { content: visible, local: false } : null;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Claude Code appends one record per content block, linked by `parentUuid` (`logicalParentUuid`
|
|
31
|
+
* across a compaction). A Turn starts at a prompt; its tool results and an interruption share the
|
|
32
|
+
* prompt's `promptId`. Interactive sessions record each end (`turn_duration`), `-p` sessions do not.
|
|
33
|
+
*/
|
|
34
|
+
export async function readClaudeTranscript(path, nativeSessionId, signal) {
|
|
35
|
+
const read = await readChainedLines(path, signal);
|
|
36
|
+
const branch = chainBranch(read.lines, (value) => value.isSidechain === true ? "" : text(value.uuid), (value) => text(value.parentUuid) || text(value.logicalParentUuid));
|
|
37
|
+
const turns = [];
|
|
38
|
+
const owners = new Map();
|
|
39
|
+
const messageIds = new Map();
|
|
40
|
+
let cwd = "", cloudSessionId = "";
|
|
41
|
+
let current = null;
|
|
42
|
+
let promptId = "", local = false, interrupted = false, completedAt = "";
|
|
43
|
+
const finish = () => {
|
|
44
|
+
if (!current)
|
|
45
|
+
return;
|
|
46
|
+
// A local command that never reached the model is not a conversation Turn.
|
|
47
|
+
if (local && !current.messages.length) {
|
|
48
|
+
current = null;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const last = current.messages.at(-1);
|
|
52
|
+
const status = interrupted || !last || last.stopReason === "tool_use" ? "interrupted" : last.stopReason === "error" ? "failed" : "completed";
|
|
53
|
+
current.result = { messages: current.messages, completedAt: completedAt || current.startedAt, status };
|
|
54
|
+
turns.push(current);
|
|
55
|
+
current = null;
|
|
56
|
+
};
|
|
57
|
+
for (const line of branch) {
|
|
58
|
+
const value = line.value;
|
|
59
|
+
cwd ||= text(value.cwd);
|
|
60
|
+
if (value.type === "user") {
|
|
61
|
+
const message = record(value.message);
|
|
62
|
+
const results = array(message.content).map(record).filter((block) => block.type === "tool_result");
|
|
63
|
+
const opened = value.isMeta === true || value.isCompactSummary === true || results.length ? null : prompt(message.content);
|
|
64
|
+
if (opened && !(current && text(value.promptId) && text(value.promptId) === promptId)) {
|
|
65
|
+
finish();
|
|
66
|
+
const cohub = record(value.cohub);
|
|
67
|
+
cloudSessionId ||= text(cohub.sessionId);
|
|
68
|
+
// Cohub sends its Turn id as the prompt's record id; a projected record names its Turn.
|
|
69
|
+
current = startTurn(line, text(value.uuid), turns.at(-1)?.key ?? null, opened.content, text(cohub.turnId) || text(value.uuid));
|
|
70
|
+
promptId = text(value.promptId);
|
|
71
|
+
local = opened.local;
|
|
72
|
+
interrupted = false;
|
|
73
|
+
completedAt = current.startedAt;
|
|
74
|
+
owners.clear();
|
|
75
|
+
messageIds.clear();
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (!current)
|
|
79
|
+
continue;
|
|
80
|
+
for (const block of results) {
|
|
81
|
+
const id = text(block.tool_use_id);
|
|
82
|
+
const owner = owners.get(id);
|
|
83
|
+
if (!owner)
|
|
84
|
+
continue;
|
|
85
|
+
owner.content.push({ type: "tool_result", tool_use_id: id, content: typeof block.content === "string" ? block.content : streamContent(block.content), is_error: block.is_error === true });
|
|
86
|
+
}
|
|
87
|
+
if (array(message.content).some((block) => text(record(block).text).startsWith(INTERRUPTED)) || text(message.content).startsWith(INTERRUPTED)) {
|
|
88
|
+
interrupted = true;
|
|
89
|
+
current.ended = true;
|
|
90
|
+
}
|
|
91
|
+
completedAt = iso(value.timestamp);
|
|
92
|
+
extendTurn(current, line, true);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (!current)
|
|
96
|
+
continue;
|
|
97
|
+
if (value.type === "assistant") {
|
|
98
|
+
const native = record(value.message);
|
|
99
|
+
const id = text(native.id);
|
|
100
|
+
let target = current.messages.at(-1);
|
|
101
|
+
if (!target || !id || messageIds.get(target) !== id) {
|
|
102
|
+
target = { content: [], provider: null, model: null, stopReason: null, errorMessage: null };
|
|
103
|
+
current.messages.push(target);
|
|
104
|
+
messageIds.set(target, id);
|
|
105
|
+
}
|
|
106
|
+
const content = streamContent(native.content).filter(nonEmpty);
|
|
107
|
+
for (const block of content) {
|
|
108
|
+
const index = block.type === "tool_use" ? target.content.findIndex((existing) => existing.type === "tool_use" && existing.id === block.id) : -1;
|
|
109
|
+
if (index >= 0)
|
|
110
|
+
target.content[index] = block;
|
|
111
|
+
else
|
|
112
|
+
target.content.push(block);
|
|
113
|
+
if (block.type === "tool_use")
|
|
114
|
+
owners.set(block.id, target);
|
|
115
|
+
}
|
|
116
|
+
const model = text(native.model);
|
|
117
|
+
if (model && model !== "<synthetic>")
|
|
118
|
+
target.model = model;
|
|
119
|
+
if (text(native.stop_reason))
|
|
120
|
+
target.stopReason = text(native.stop_reason);
|
|
121
|
+
const usage = streamUsage(native.usage, undefined);
|
|
122
|
+
if (usage)
|
|
123
|
+
target.usage = usage;
|
|
124
|
+
if (value.isApiErrorMessage === true) {
|
|
125
|
+
target.stopReason = "error";
|
|
126
|
+
target.errorMessage = content.flatMap((block) => block.type === "text" ? [block.text] : []).join("\n") || "API error";
|
|
127
|
+
}
|
|
128
|
+
completedAt = iso(value.timestamp);
|
|
129
|
+
extendTurn(current, line, true);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (value.type === "system" && value.subtype === "turn_duration")
|
|
133
|
+
current.ended = true;
|
|
134
|
+
extendTurn(current, line, false);
|
|
135
|
+
}
|
|
136
|
+
finish();
|
|
137
|
+
return { nativeSessionId, cwd, ...(cloudSessionId ? { cloudSessionId } : {}), turns, prefixes: read.prefixes };
|
|
138
|
+
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { homedir } from "node:os";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
|
+
import { JsonRpcProcess, record } from "../json-rpc.js";
|
|
3
5
|
import { copyNativeArchive } from "../native/archive.js";
|
|
4
6
|
import { projectChainedSession, projectionMeta, projectionToolId, rebindSessionId } from "./chained-projection.js";
|
|
7
|
+
import { chainedSessionHeader, projectDirectory, projectTranscript } from "./chained-transcript.js";
|
|
8
|
+
import { CLAUDE_REPLY_ENDED, readClaudeTranscript } from "./claude-transcript.js";
|
|
9
|
+
import { array, harnessCommand, text } from "./process.js";
|
|
5
10
|
import { DEFAULT_MODEL, runStreamJson, streamJsonHelp, streamJsonModel } from "./stream-json.js";
|
|
6
11
|
const MAX_PROJECT_NAME = 200;
|
|
12
|
+
const INITIALIZE_TIMEOUT_MS = 30_000;
|
|
7
13
|
const dataRoot = () => process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude");
|
|
8
14
|
/**
|
|
9
15
|
* Claude Code names a project directory by replacing every non-alphanumeric UTF-16 unit; a longer
|
|
@@ -19,6 +25,9 @@ export function claudeProjectName(cwd) {
|
|
|
19
25
|
return `${name.slice(0, MAX_PROJECT_NAME)}-${Math.abs(hash).toString(36)}`;
|
|
20
26
|
}
|
|
21
27
|
export const claudeProjectDirectory = (cwd) => join(dataRoot(), "projects", claudeProjectName(cwd));
|
|
28
|
+
const projectsRoot = () => join(dataRoot(), "projects");
|
|
29
|
+
/** Every subdirectory's project name starts with this; a hashed long name keeps its first 200 units. */
|
|
30
|
+
const projectPrefix = (cwd) => cwd.replace(/[^a-zA-Z0-9]/g, "-").slice(0, MAX_PROJECT_NAME);
|
|
22
31
|
const profile = {
|
|
23
32
|
harness: "claude",
|
|
24
33
|
label: "Claude Code",
|
|
@@ -27,6 +36,7 @@ const profile = {
|
|
|
27
36
|
dataRoot,
|
|
28
37
|
projectDirectory: claudeProjectDirectory,
|
|
29
38
|
effortLevels: ["low", "medium", "high", "xhigh", "max"],
|
|
39
|
+
promptUuid: true,
|
|
30
40
|
};
|
|
31
41
|
function anthropicBlock(block) {
|
|
32
42
|
if (block.type === "text")
|
|
@@ -47,11 +57,66 @@ function withAssistantIds(projection) {
|
|
|
47
57
|
...entry, record: { ...entry.record, message: { id: `msg_${String(entry.record.uuid).replaceAll("-", "")}`, ...entry.record.message } },
|
|
48
58
|
}) };
|
|
49
59
|
}
|
|
60
|
+
/** Claude Code's own `/model` list (what the SDK's `supportedModels()` reads), gateway-discovered models included. */
|
|
61
|
+
async function claudeInitialize(executable, cwd) {
|
|
62
|
+
const args = ["-p", "--input-format", "stream-json", "--output-format", "stream-json", "--verbose", "--no-session-persistence", "--strict-mcp-config"];
|
|
63
|
+
const host = new JsonRpcProcess(...harnessCommand(executable, args, cwd), cwd, "stream");
|
|
64
|
+
const requestId = randomUUID();
|
|
65
|
+
let timer;
|
|
66
|
+
try {
|
|
67
|
+
return await new Promise((resolve, reject) => {
|
|
68
|
+
timer = setTimeout(() => reject(new Error("Claude Code initialize timed out")), INITIALIZE_TIMEOUT_MS);
|
|
69
|
+
host.onFailure(reject);
|
|
70
|
+
host.onEvent((event) => {
|
|
71
|
+
const response = record(event.response);
|
|
72
|
+
if (event.type !== "control_response" || response.request_id !== requestId)
|
|
73
|
+
return;
|
|
74
|
+
if (response.subtype === "success")
|
|
75
|
+
resolve(record(response.response));
|
|
76
|
+
else
|
|
77
|
+
reject(new Error(text(response.error) || "Claude Code initialize failed"));
|
|
78
|
+
});
|
|
79
|
+
host.write({ type: "control_request", request_id: requestId, request: { subtype: "initialize" } });
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
await host.close();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Gateway models are served by `ANTHROPIC_BASE_URL`, so they carry its host as provider. */
|
|
88
|
+
function claudeModelCatalog(entries, baseUrl) {
|
|
89
|
+
let gateway = "gateway";
|
|
90
|
+
try {
|
|
91
|
+
if (baseUrl)
|
|
92
|
+
gateway = new URL(baseUrl).hostname.slice(0, 100) || gateway;
|
|
93
|
+
}
|
|
94
|
+
catch { /* Keep the generic name. */ }
|
|
95
|
+
const seen = new Set();
|
|
96
|
+
return entries.flatMap((value) => {
|
|
97
|
+
const model = record(value);
|
|
98
|
+
const id = text(model.value);
|
|
99
|
+
if (!id || id.length > 255 || seen.has(id))
|
|
100
|
+
return [];
|
|
101
|
+
seen.add(id);
|
|
102
|
+
const effortLevels = model.supportsEffort === true ? array(model.supportedEffortLevels).filter((level) => typeof level === "string") : [];
|
|
103
|
+
return [streamJsonModel(profile, id, (text(model.displayName) || id).slice(0, 255), {
|
|
104
|
+
reasoning: effortLevels.length > 0, effortLevels, isDefault: id === DEFAULT_MODEL,
|
|
105
|
+
provider: text(model.description).startsWith("From gateway") ? gateway : profile.provider,
|
|
106
|
+
})];
|
|
107
|
+
});
|
|
108
|
+
}
|
|
50
109
|
export const claudeHarness = {
|
|
51
110
|
id: "claude",
|
|
52
111
|
binary: profile.binary,
|
|
53
112
|
async models(executable, cwd) {
|
|
54
113
|
await streamJsonHelp(profile, executable, cwd);
|
|
114
|
+
try {
|
|
115
|
+
const models = claudeModelCatalog(array((await claudeInitialize(executable, cwd)).models), process.env.ANTHROPIC_BASE_URL);
|
|
116
|
+
if (models.some((model) => model.isDefault))
|
|
117
|
+
return models;
|
|
118
|
+
}
|
|
119
|
+
catch { /* A CLI without the initialize model list keeps the account aliases. */ }
|
|
55
120
|
// Aliases follow the signed-in account; Haiku takes no effort level.
|
|
56
121
|
return [
|
|
57
122
|
streamJsonModel(profile, DEFAULT_MODEL, "Default", { reasoning: true, isDefault: true }),
|
|
@@ -90,5 +155,17 @@ export const claudeHarness = {
|
|
|
90
155
|
},
|
|
91
156
|
},
|
|
92
157
|
},
|
|
93
|
-
|
|
158
|
+
transcript: {
|
|
159
|
+
roots: () => [projectsRoot()],
|
|
160
|
+
acceptsDirectory: (path, root, cwd) => projectDirectory(path, root, projectPrefix(cwd)),
|
|
161
|
+
acceptsTranscript: (path, cwd) => projectTranscript(path, projectsRoot(), projectPrefix(cwd)),
|
|
162
|
+
header: (path, signal) => chainedSessionHeader(path, "claude", signal),
|
|
163
|
+
read: (path, signal) => readClaudeTranscript(path, basename(path, ".jsonl"), signal),
|
|
164
|
+
boundaries: ['"promptSource"', '"subtype":"turn_duration"', '"interruptedMessageId"', '"isApiErrorMessage":true', ...CLAUDE_REPLY_ENDED.map((reason) => `"stop_reason":"${reason}"`)],
|
|
165
|
+
turnEndRecorded: false,
|
|
166
|
+
replyEnded: (turn) => CLAUDE_REPLY_ENDED.includes(turn.messages.at(-1)?.stopReason ?? ""),
|
|
167
|
+
userFinalMarked: false,
|
|
168
|
+
globalTurnKeys: false,
|
|
169
|
+
marksCohubTurns: true,
|
|
170
|
+
},
|
|
94
171
|
};
|
|
@@ -223,7 +223,7 @@ export const codexHarness = {
|
|
|
223
223
|
turnEndRecorded: true,
|
|
224
224
|
userFinalMarked: true,
|
|
225
225
|
globalTurnKeys: true,
|
|
226
|
+
marksCohubTurns: true,
|
|
226
227
|
archiveSource: (path, cacheDirectory) => ensurePlainCodexRollout(path, cacheDirectory),
|
|
227
228
|
},
|
|
228
|
-
nativeSync: true,
|
|
229
229
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { NativeTranscript } from "../native/transcript.js";
|
|
2
|
+
import type { SessionHeader } from "./types.js";
|
|
3
|
+
/** Chats (`store.db` and `meta.json`) live under the config directory, transcripts under the data directory. */
|
|
4
|
+
export declare const cursorConfigDirectory: () => string;
|
|
5
|
+
export declare const cursorProjectsRoot: () => string;
|
|
6
|
+
export declare const cursorChatsRoot: () => string;
|
|
7
|
+
/** Cursor's project name for a workspace path. */
|
|
8
|
+
export declare const cursorProjectName: (cwd: string) => string;
|
|
9
|
+
export declare const cursorTranscriptPath: (nativeSessionId: string, cwd: string) => string;
|
|
10
|
+
/** A chat's store directory is keyed by the MD5 of the directory the CLI ran in. */
|
|
11
|
+
export declare const cursorChatDirectory: (nativeSessionId: string, cwd: string) => string;
|
|
12
|
+
export declare function acceptsCursorDirectory(path: string, root: string, cwd: string): boolean;
|
|
13
|
+
export declare function acceptsCursorTranscript(path: string, cwd: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The directory a project name stands for, matched against real directories. On Windows the name
|
|
16
|
+
* starts with the drive letter and matching ignores case. Several directories with the same name
|
|
17
|
+
* (`a/b` and `a-b`) cannot be told apart, so none is chosen.
|
|
18
|
+
*/
|
|
19
|
+
type Entry = {
|
|
20
|
+
name: string;
|
|
21
|
+
isDirectory(): boolean;
|
|
22
|
+
isSymbolicLink(): boolean;
|
|
23
|
+
};
|
|
24
|
+
export declare function projectDirectory(name: string, platform?: NodeJS.Platform, list?: (directory: string) => Promise<Entry[]>): Promise<string | null>;
|
|
25
|
+
/**
|
|
26
|
+
* The transcript does not name its project. A CLI chat's `meta.json` does; the IDE writes the same
|
|
27
|
+
* transcripts without one, so its project name is resolved to the directory it stands for.
|
|
28
|
+
*/
|
|
29
|
+
export declare function cursorSessionHeader(path: string): Promise<SessionHeader | null>;
|
|
30
|
+
/**
|
|
31
|
+
* Cursor writes the model's messages without ids or times: a prompt (`<user_query>`, or a bare text
|
|
32
|
+
* that no other user line follows), one line per reply step with its tool calls (results are not
|
|
33
|
+
* recorded), and `turn_ended` after every Turn. A user line followed by another is context. A Turn
|
|
34
|
+
* is keyed by the file's checksum through its prompt, so a rewritten history never reuses a key.
|
|
35
|
+
*/
|
|
36
|
+
export declare function readCursorTranscript(path: string, nativeSessionId: string, signal?: AbortSignal): Promise<NativeTranscript>;
|
|
37
|
+
export {};
|