@neta-art/cohub-cli 10.5.0 → 10.6.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 +4 -2
- package/dist/commands/runtime.js +21 -8
- package/dist/commands/spaces.js +3 -3
- package/dist/runtime/archive-store.d.ts +2 -2
- package/dist/runtime/archive-store.js +2 -2
- package/dist/runtime/diagnostics.d.ts +3 -3
- package/dist/runtime/harnesses/chained-projection.d.ts +40 -0
- package/dist/runtime/harnesses/chained-projection.js +95 -0
- package/dist/runtime/harnesses/claude.d.ts +8 -0
- package/dist/runtime/harnesses/claude.js +94 -0
- package/dist/runtime/harnesses/codex.d.ts +4 -0
- package/dist/runtime/harnesses/codex.js +229 -0
- package/dist/runtime/harnesses/index.d.ts +16 -0
- package/dist/runtime/harnesses/index.js +57 -0
- package/dist/runtime/harnesses/pi.d.ts +4 -0
- package/dist/runtime/harnesses/pi.js +247 -0
- package/dist/runtime/harnesses/process.d.ts +31 -0
- package/dist/runtime/harnesses/process.js +75 -0
- package/dist/runtime/harnesses/stream-json.d.ts +28 -0
- package/dist/runtime/harnesses/stream-json.js +291 -0
- package/dist/runtime/harnesses/types.d.ts +108 -0
- package/dist/runtime/harnesses/types.js +1 -0
- package/dist/runtime/harnesses/workbuddy.d.ts +15 -0
- package/dist/runtime/harnesses/workbuddy.js +99 -0
- package/dist/runtime/json-rpc.d.ts +7 -1
- package/dist/runtime/json-rpc.js +23 -2
- package/dist/runtime/launch.d.ts +6 -4
- package/dist/runtime/launch.js +21 -11
- package/dist/runtime/native/adapters.d.ts +11 -28
- package/dist/runtime/native/adapters.js +7 -75
- package/dist/runtime/native/archive.d.ts +9 -7
- package/dist/runtime/native/archive.js +99 -38
- package/dist/runtime/native/attach.d.ts +3 -5
- package/dist/runtime/native/attach.js +8 -2
- package/dist/runtime/native/daemon.d.ts +2 -2
- package/dist/runtime/native/daemon.js +5 -4
- package/dist/runtime/native/execution.d.ts +9 -30
- package/dist/runtime/native/execution.js +7 -325
- package/dist/runtime/native/identity.d.ts +4 -4
- package/dist/runtime/native/identity.js +4 -4
- package/dist/runtime/native/ingest.d.ts +2 -2
- package/dist/runtime/native/ingest.js +20 -25
- package/dist/runtime/native/sessions.d.ts +4 -4
- package/dist/runtime/native/sessions.js +12 -22
- package/dist/runtime/native/transcript.d.ts +3 -0
- package/dist/runtime/native/transcript.js +6 -5
- package/dist/runtime/native/translate.d.ts +2 -0
- package/dist/runtime/native/translate.js +34 -1
- package/dist/runtime/native/version.js +1 -1
- package/dist/runtime/presentation.js +2 -1
- package/dist/runtime/projection-store.js +2 -12
- package/dist/runtime/supervisor.d.ts +4 -4
- package/dist/runtime/supervisor.js +1 -1
- package/dist/runtime/turn-projection.d.ts +1 -1
- package/dist/runtime/turn-projection.js +2 -2
- package/package.json +2 -2
- package/dist/runtime/harness.d.ts +0 -26
- package/dist/runtime/harness.js +0 -142
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
3
|
import { dirname, join } from "node:path";
|
|
5
4
|
import { serializeProjectionRecords } from "@neta-art/cohub";
|
|
6
|
-
import { ArchiveNotRestorableError
|
|
7
|
-
import {
|
|
5
|
+
import { ArchiveNotRestorableError } from "./archive.js";
|
|
6
|
+
import { readTranscript } from "./adapters.js";
|
|
7
|
+
import { harnessAdapter } from "../harnesses/index.js";
|
|
8
8
|
import { ProjectionStore, rebindProjectionNativeSession } from "../projection-store.js";
|
|
9
9
|
/** Chooses the native file a Turn runs in. 1. */
|
|
10
10
|
export class NativeSessions {
|
|
@@ -36,18 +36,19 @@ export class NativeSessions {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
const projected = await this.project(input);
|
|
39
|
-
// A
|
|
40
|
-
if (projected.resume !== "new" || input.harness
|
|
39
|
+
// A harness that creates its own file gets it when the Turn starts; the executor adopts it then.
|
|
40
|
+
if (projected.resume !== "new" || !harnessAdapter(input.harness).session.startsWithoutFile)
|
|
41
41
|
this.options.ingest.adopt(projected.session, input.sessionId);
|
|
42
42
|
return projected;
|
|
43
43
|
}
|
|
44
44
|
async restore(input, archive, signal) {
|
|
45
45
|
const id = randomUUID();
|
|
46
46
|
const raw = join(this.options.root, "native", "restored", `${id}.jsonl`);
|
|
47
|
-
const
|
|
47
|
+
const format = harnessAdapter(input.harness).session.archive;
|
|
48
48
|
try {
|
|
49
49
|
const restored = await this.options.archives.restore(archive, raw, signal);
|
|
50
|
-
const
|
|
50
|
+
const path = format.target({ id, nativeSessionId: restored.nativeSessionId, cwd: input.cwd });
|
|
51
|
+
const imported = await format.import({ source: raw, target: path, nativeSessionId: restored.nativeSessionId, id, cwd: input.cwd, signal });
|
|
51
52
|
return { harness: input.harness, nativeSessionId: imported.nativeSessionId, path, cwd: input.cwd };
|
|
52
53
|
}
|
|
53
54
|
finally {
|
|
@@ -55,8 +56,9 @@ export class NativeSessions {
|
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
async project(input) {
|
|
59
|
+
const format = harnessAdapter(input.harness).session;
|
|
58
60
|
const nativeSessionId = randomUUID();
|
|
59
|
-
const path =
|
|
61
|
+
const path = format.path(nativeSessionId, input.cwd);
|
|
60
62
|
const projection = rebindProjectionNativeSession(await this.projections.project({
|
|
61
63
|
spaceId: this.options.spaceId,
|
|
62
64
|
sessionId: input.sessionId,
|
|
@@ -68,8 +70,8 @@ export class NativeSessions {
|
|
|
68
70
|
throughTurnId: input.context.throughTurnId,
|
|
69
71
|
}, input.signal), nativeSessionId);
|
|
70
72
|
const session = { harness: input.harness, nativeSessionId, path, cwd: input.cwd };
|
|
71
|
-
//
|
|
72
|
-
if (!projection.turns.length
|
|
73
|
+
// Nothing to carry (no Turns, or none that project) starts natively where the harness creates its own file.
|
|
74
|
+
if ((!projection.turns.length || !projection.projection.records.length) && format.startsWithoutFile)
|
|
73
75
|
return { session, resume: "new" };
|
|
74
76
|
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
75
77
|
const temporary = `${path}.${randomUUID()}.projection`;
|
|
@@ -78,15 +80,3 @@ export class NativeSessions {
|
|
|
78
80
|
return { session, resume: projection.turns.length ? "handoff" : "new" };
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
|
-
/** Where a harness keeps a new transcript for the bound directory. */
|
|
82
|
-
function nativePath(harness, nativeSessionId, cwd) {
|
|
83
|
-
const stamp = new Date().toISOString();
|
|
84
|
-
if (harness === "pi") {
|
|
85
|
-
const agent = process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent");
|
|
86
|
-
const directory = process.env.PI_CODING_AGENT_SESSION_DIR?.trim() || join(agent, "sessions", piSessionDirectoryName(cwd));
|
|
87
|
-
return join(directory, `${stamp.replace(/[:.]/g, "-")}_${nativeSessionId}.jsonl`);
|
|
88
|
-
}
|
|
89
|
-
const home = process.env.CODEX_HOME?.trim() || join(homedir(), ".codex");
|
|
90
|
-
const day = stamp.replace(/\.\d{3}Z$/, "").replaceAll(":", "-");
|
|
91
|
-
return join(home, "sessions", day.slice(0, 4), day.slice(5, 7), day.slice(8, 10), `rollout-${day}-${nativeSessionId}.jsonl`);
|
|
92
|
-
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ContentBlock, NativeTurnComplete, NativeTurnMessage } from "@neta-art/cohub";
|
|
2
|
+
import { type JsonRecord } from "../json-rpc.js";
|
|
2
3
|
export type NativeTranscriptTurn = {
|
|
3
4
|
key: string;
|
|
4
5
|
parentKey: string | null;
|
|
@@ -47,6 +48,8 @@ export type NativeTranscriptHeader = {
|
|
|
47
48
|
rolloutId?: string;
|
|
48
49
|
historyBase?: CodexHistoryBase;
|
|
49
50
|
};
|
|
51
|
+
/** Read exactly the first JSONL record of a stream without re-concatenating per chunk. */
|
|
52
|
+
export declare function readFirstRecord(stream: AsyncIterable<Buffer>): Promise<JsonRecord>;
|
|
50
53
|
/** Read only the session header before deciding whether a global Codex file belongs to this project. */
|
|
51
54
|
export declare function readNativeTranscriptHeader(path: string, harness: "pi" | "codex", signal?: AbortSignal): Promise<NativeTranscriptHeader>;
|
|
52
55
|
/** Raw JSONL bytes of a rollout, transparently decompressing `.zst` and honoring a lineage cutoff. */
|
|
@@ -6,7 +6,8 @@ import { homedir } from "node:os";
|
|
|
6
6
|
import { basename, join } from "node:path";
|
|
7
7
|
import { createZstdDecompress } from "node:zlib";
|
|
8
8
|
import { pipeline } from "node:stream/promises";
|
|
9
|
-
import { identifiable
|
|
9
|
+
import { identifiable } from "../harnesses/process.js";
|
|
10
|
+
import { piContent } from "./translate.js";
|
|
10
11
|
import { codexTokenTotals, codexUsage } from "../codex-usage.js";
|
|
11
12
|
import { record } from "../json-rpc.js";
|
|
12
13
|
const text = (value) => typeof value === "string" ? value : "";
|
|
@@ -43,8 +44,8 @@ function parseCodexHistoryBase(value) {
|
|
|
43
44
|
throw new Error("Codex history reference is invalid; retain the original files");
|
|
44
45
|
return { rolloutId, endOrdinal, endBytes };
|
|
45
46
|
}
|
|
46
|
-
/** Read exactly the first JSONL
|
|
47
|
-
async function
|
|
47
|
+
/** Read exactly the first JSONL record of a stream without re-concatenating per chunk. */
|
|
48
|
+
export async function readFirstRecord(stream) {
|
|
48
49
|
const chunks = [];
|
|
49
50
|
let buffered = 0;
|
|
50
51
|
for await (const chunk of stream) {
|
|
@@ -64,7 +65,7 @@ async function readFirstLine(stream) {
|
|
|
64
65
|
throw new Error("Native transcript is empty");
|
|
65
66
|
}
|
|
66
67
|
async function readCodexHeader(path, signal) {
|
|
67
|
-
const value = await
|
|
68
|
+
const value = await readFirstRecord(codexRolloutBytes(path, null, signal));
|
|
68
69
|
const payload = record(value.payload);
|
|
69
70
|
const threadId = text(payload.id), cwd = text(payload.cwd);
|
|
70
71
|
if (value.type !== "session_meta" || !threadId || !cwd)
|
|
@@ -75,7 +76,7 @@ async function readCodexHeader(path, signal) {
|
|
|
75
76
|
/** Read only the session header before deciding whether a global Codex file belongs to this project. */
|
|
76
77
|
export async function readNativeTranscriptHeader(path, harness, signal) {
|
|
77
78
|
if (harness === "pi") {
|
|
78
|
-
const value = await
|
|
79
|
+
const value = await readFirstRecord(createReadStream(path, { signal }));
|
|
79
80
|
if (value.type !== "session" || !text(value.id) || !text(value.cwd))
|
|
80
81
|
throw new Error("Invalid native session header");
|
|
81
82
|
return { nativeSessionId: text(value.id), cwd: text(value.cwd) };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ContentBlock, NativeTurnProgress, RuntimeExecutionEvent, RuntimeMessage } from "@neta-art/cohub";
|
|
2
2
|
import { type JsonRecord } from "../json-rpc.js";
|
|
3
|
+
export declare function piContent(value: unknown): ContentBlock[];
|
|
4
|
+
export declare function codexItemContent(item: JsonRecord): ContentBlock[];
|
|
3
5
|
/** Harness events mapped onto Cohub's streaming protocol. */
|
|
4
6
|
export type Translator = {
|
|
5
7
|
push(event: JsonRecord): boolean;
|
|
@@ -1,9 +1,42 @@
|
|
|
1
|
-
import { codexItemContent, identifiable, piContent } from "../harness.js";
|
|
2
1
|
import { codexTokenTotals, codexUsage, subtractCodexTokens } from "../codex-usage.js";
|
|
2
|
+
import { identifiable } from "../harnesses/process.js";
|
|
3
3
|
import { record } from "../json-rpc.js";
|
|
4
4
|
const text = (value) => typeof value === "string" ? value : "";
|
|
5
5
|
const list = (value) => Array.isArray(value) ? value : [];
|
|
6
6
|
const hasToolCall = (content, id) => content.some((block) => block.type === "tool_use" && block.id === id);
|
|
7
|
+
export function piContent(value) {
|
|
8
|
+
return list(value).flatMap((entry) => {
|
|
9
|
+
const block = record(entry);
|
|
10
|
+
if (block.type === "text")
|
|
11
|
+
return [{ type: "text", text: text(block.text) }];
|
|
12
|
+
if (block.type === "thinking")
|
|
13
|
+
return [{ type: "thinking", thinking: text(block.thinking), ...(typeof (block.thinkingSignature ?? block.signature) === "string" ? { signature: String(block.thinkingSignature ?? block.signature) } : {}) }];
|
|
14
|
+
if (block.type === "toolCall")
|
|
15
|
+
return identifiable(block.id) && identifiable(block.name) ? [{ type: "tool_use", id: block.id, name: block.name, input: record(block.arguments) }] : [];
|
|
16
|
+
if (block.type === "image")
|
|
17
|
+
return [{ type: "image", source: { type: "base64", data: text(block.data), media_type: text(block.mimeType) } }];
|
|
18
|
+
return [{ type: "text", text: JSON.stringify(block) }];
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export function codexItemContent(item) {
|
|
22
|
+
if (item.type === "agentMessage" || item.type === "plan")
|
|
23
|
+
return [{ type: "text", text: text(item.text) }];
|
|
24
|
+
if (item.type === "reasoning")
|
|
25
|
+
return [{ type: "thinking", thinking: [...list(item.summary), ...list(item.content)].map(text).join("\n") }];
|
|
26
|
+
if (item.type === "userMessage")
|
|
27
|
+
return [];
|
|
28
|
+
if (item.type === "contextCompaction")
|
|
29
|
+
return [{ type: "system_note", note_type: "compacted", text: "Context compacted" }];
|
|
30
|
+
const id = item.id;
|
|
31
|
+
if (!identifiable(id))
|
|
32
|
+
return [];
|
|
33
|
+
const name = item.type === "commandExecution" ? "bash" : item.type === "fileChange" ? "apply_patch" : text(item.tool) || text(item.type);
|
|
34
|
+
const input = item.type === "commandExecution" ? { command: item.command, cwd: item.cwd } : item.type === "fileChange" ? { changes: item.changes } : record(item.arguments);
|
|
35
|
+
return [
|
|
36
|
+
{ type: "tool_use", id, name, input },
|
|
37
|
+
{ type: "tool_result", tool_use_id: id, content: typeof item.aggregatedOutput === "string" ? item.aggregatedOutput : JSON.stringify(item.result ?? item.error ?? item.changes ?? item), is_error: item.status === "failed" || typeof item.exitCode === "number" && item.exitCode !== 0 },
|
|
38
|
+
];
|
|
39
|
+
}
|
|
7
40
|
/** Pi emits the same event shapes to its extensions as over RPC. */
|
|
8
41
|
export function piTranslator(emit) {
|
|
9
42
|
let ordinal = -1;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
-
import { harnessCommand } from "../
|
|
3
|
+
import { harnessCommand } from "../harnesses/process.js";
|
|
4
4
|
/** A harness binary's own version, or `null` when it cannot say. */
|
|
5
5
|
export async function harnessVersion(binary) {
|
|
6
6
|
const { stdout } = await Promise.resolve()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { LOCAL_HARNESS_SPECS } from "@neta-art/cohub";
|
|
1
2
|
import { webUrl } from "../web.js";
|
|
2
3
|
export const diagnosticLevels = ["debug", "info", "warn", "error"];
|
|
3
4
|
export const atLeastLevel = (level, minimum) => diagnosticLevels.indexOf(level) >= diagnosticLevels.indexOf(minimum);
|
|
@@ -98,7 +99,7 @@ export function formatNativeSync(config, native, error = null) {
|
|
|
98
99
|
return `Native sync unknown — ${error} · fix or remove the config, then runtime up\n`;
|
|
99
100
|
if (!config?.harnesses.length)
|
|
100
101
|
return "Native sync off · run cohub runtime up to enable\n";
|
|
101
|
-
const lines = [`Native sync ${config.harnesses.map((harness) => harness
|
|
102
|
+
const lines = [`Native sync ${config.harnesses.map((harness) => LOCAL_HARNESS_SPECS[harness].label).join(" · ")}`];
|
|
102
103
|
if (!native)
|
|
103
104
|
return `${lines[0]} · Runtime not running\n`;
|
|
104
105
|
if (native.pi) {
|
|
@@ -1,18 +1,8 @@
|
|
|
1
|
+
import { harnessAdapter } from "./harnesses/index.js";
|
|
1
2
|
import { listSessionProjectionTurns, projectTurnBatch } from "./turn-projection.js";
|
|
2
3
|
/** Give the projection's own records (the native header) the identity of the file it becomes. */
|
|
3
4
|
export function rebindProjectionNativeSession(result, nativeSessionId) {
|
|
4
|
-
const records = result.projection.
|
|
5
|
-
if (entry.sourceTurnId !== null)
|
|
6
|
-
return entry;
|
|
7
|
-
if (entry.record.type === "session") {
|
|
8
|
-
return { ...entry, record: { ...entry.record, id: nativeSessionId, affinity: { ...entry.record.affinity, threadId: nativeSessionId } } };
|
|
9
|
-
}
|
|
10
|
-
if (entry.record.type === "session_meta") {
|
|
11
|
-
const payload = entry.record.payload;
|
|
12
|
-
return { ...entry, record: { ...entry.record, payload: { ...payload, id: nativeSessionId, session_id: nativeSessionId } } };
|
|
13
|
-
}
|
|
14
|
-
return entry;
|
|
15
|
-
});
|
|
5
|
+
const records = harnessAdapter(result.projection.target).session.rebind(result.projection.records, nativeSessionId);
|
|
16
6
|
return { ...result, projection: { ...result.projection, records } };
|
|
17
7
|
}
|
|
18
8
|
/**
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { type RuntimeCapabilities } from "@neta-art/cohub";
|
|
2
|
-
import { type
|
|
1
|
+
import { type LocalHarness, type RuntimeCapabilities } from "@neta-art/cohub";
|
|
2
|
+
import { type HarnessExecutables } from "./harnesses/index.js";
|
|
3
3
|
import { type RuntimeDiagnostic, type RuntimeDiagnosticLevel } from "./diagnostics.js";
|
|
4
4
|
import { type RuntimeSummary } from "./presentation.js";
|
|
5
5
|
export type RuntimeLaunch = {
|
|
6
6
|
spaceId: string;
|
|
7
7
|
root: string;
|
|
8
8
|
identity: string;
|
|
9
|
-
harnesses:
|
|
10
|
-
executables:
|
|
9
|
+
harnesses: LocalHarness[];
|
|
10
|
+
executables: HarnessExecutables;
|
|
11
11
|
capabilities?: RuntimeCapabilities;
|
|
12
12
|
background: boolean;
|
|
13
13
|
verbose?: boolean;
|
|
@@ -10,7 +10,7 @@ import { createClient } from "../client.js";
|
|
|
10
10
|
import { currentIdentityKey } from "../space.js";
|
|
11
11
|
import { ensureSandboxdBinary } from "../commands/sandboxd-binary.js";
|
|
12
12
|
import { serveRuntime } from "./connection.js";
|
|
13
|
-
import { discoverHarnesses } from "./
|
|
13
|
+
import { discoverHarnesses } from "./harnesses/index.js";
|
|
14
14
|
import { RuntimeDiagnostics, serializeDiagnosticError } from "./diagnostics.js";
|
|
15
15
|
import { ownRuntimeInstance, runtimeInstanceDirectory } from "./instance.js";
|
|
16
16
|
import { createDiagnosticConsole } from "./presentation.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { CanonicalProjectionTurn, MessageToolCallsFile, ProjectionInput, ProjectionTarget, SessionTurnRecord, StoredIntermediateMessage, TurnIntermediateMessagesFile } from "@neta-art/cohub";
|
|
2
2
|
export type SessionTurnProjectionClient = {
|
|
3
3
|
session(sessionId: string): {
|
|
4
4
|
turns: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { harnessAdapter } from "./harnesses/index.js";
|
|
2
2
|
const record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
3
3
|
const iso = (value) => value ?? new Date(0).toISOString();
|
|
4
4
|
const sourceSessionId = (turn) => turn.sourceSessionId ?? turn.sessionId;
|
|
@@ -116,5 +116,5 @@ export async function listSessionProjectionTurns(source, sessionId, options = {}
|
|
|
116
116
|
return mapWithConcurrency(turns, 4, (turn) => hydrateSessionTurn(turn, client, options.signal));
|
|
117
117
|
}
|
|
118
118
|
export function projectTurnBatch(input, target) {
|
|
119
|
-
return
|
|
119
|
+
return harnessAdapter(target).session.project(input);
|
|
120
120
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neta-art/cohub-cli",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.6.0",
|
|
4
4
|
"description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"NOTICE"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@neta-art/cohub": "11.
|
|
21
|
+
"@neta-art/cohub": "11.5.0",
|
|
22
22
|
"@neta-art/generation": "^0.1.39",
|
|
23
23
|
"commander": "^15.0.0",
|
|
24
24
|
"sharp": "^0.35.4",
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { ContentBlock, RuntimeCapabilities } from "@neta-art/cohub";
|
|
2
|
-
import { type JsonRecord } from "./json-rpc.js";
|
|
3
|
-
export type HarnessOptions = {
|
|
4
|
-
pi?: string;
|
|
5
|
-
codex?: string;
|
|
6
|
-
};
|
|
7
|
-
export declare function harnessExecutableCandidates(binary: string, cwd: string, path: string, platform?: NodeJS.Platform, pathExt?: string | undefined): string[];
|
|
8
|
-
/**
|
|
9
|
-
* Windows npm/pnpm installs expose harnesses as .cmd shims, which cannot be spawned
|
|
10
|
-
* without a shell. Run an .exe directly, or the shim's Node entry with this Node.
|
|
11
|
-
*/
|
|
12
|
-
export declare function harnessCommand(binary: string, args: string[], cwd: string): [string, string[]];
|
|
13
|
-
/** Discover installed executables only; authentication/capability errors stay explicit. */
|
|
14
|
-
export declare function installedHarnesses(cwd: string, options: HarnessOptions, path?: string): Promise<("pi" | "codex")[]>;
|
|
15
|
-
/** Aborted streams leave tool calls with an empty id or name. */
|
|
16
|
-
export declare const identifiable: (value: unknown) => value is string;
|
|
17
|
-
export declare function piContent(value: unknown): ContentBlock[];
|
|
18
|
-
export declare function imageForPi(block: Extract<ContentBlock, {
|
|
19
|
-
type: "image";
|
|
20
|
-
}>): Promise<{
|
|
21
|
-
type: string;
|
|
22
|
-
data: string;
|
|
23
|
-
mimeType: string;
|
|
24
|
-
}>;
|
|
25
|
-
export declare function discoverHarnesses(harnesses: ("pi" | "codex")[], options: HarnessOptions, cwd: string): Promise<RuntimeCapabilities>;
|
|
26
|
-
export declare function codexItemContent(item: JsonRecord): ContentBlock[];
|
package/dist/runtime/harness.js
DELETED
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { access, stat } from "node:fs/promises";
|
|
2
|
-
import { constants, readFileSync, statSync } from "node:fs";
|
|
3
|
-
import { delimiter, join, resolve, win32 } from "node:path";
|
|
4
|
-
import { JsonRpcProcess, record } from "./json-rpc.js";
|
|
5
|
-
import { codexModelCatalog, piModelCatalog } from "./model-catalog.js";
|
|
6
|
-
import { downloadPublicImage } from "../safe-remote-image.js";
|
|
7
|
-
export function harnessExecutableCandidates(binary, cwd, path, platform = process.platform, pathExt = process.env.PATHEXT) {
|
|
8
|
-
const windows = platform === "win32";
|
|
9
|
-
const paths = windows ? win32 : { delimiter, join, resolve };
|
|
10
|
-
// Windows runs only PATHEXT names; npm/pnpm also write an extensionless sh script beside the .cmd shim.
|
|
11
|
-
const extensions = windows && !win32.extname(binary)
|
|
12
|
-
? (pathExt || ".COM;.EXE;.BAT;.CMD").split(";").filter((ext) => /^\.[a-z0-9]+$/i.test(ext))
|
|
13
|
-
: [""];
|
|
14
|
-
const roots = binary.includes("/") || binary.includes("\\")
|
|
15
|
-
? [paths.resolve(cwd, binary)]
|
|
16
|
-
: path.split(paths.delimiter).map((directory) => directory.replace(/^"(.*)"$/, "$1")).filter(Boolean).map((directory) => paths.resolve(cwd, directory, binary));
|
|
17
|
-
return roots.flatMap((root) => extensions.map((ext) => `${root}${ext}`));
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Windows npm/pnpm installs expose harnesses as .cmd shims, which cannot be spawned
|
|
21
|
-
* without a shell. Run an .exe directly, or the shim's Node entry with this Node.
|
|
22
|
-
*/
|
|
23
|
-
export function harnessCommand(binary, args, cwd) {
|
|
24
|
-
if (process.platform !== "win32")
|
|
25
|
-
return [binary, args];
|
|
26
|
-
const path = harnessExecutableCandidates(binary, cwd, process.env.PATH ?? "").find((candidate) => statSync(candidate, { throwIfNoEntry: false })?.isFile());
|
|
27
|
-
if (!path)
|
|
28
|
-
throw new Error(`${binary} was not found on PATH`);
|
|
29
|
-
const extension = win32.extname(path).toLowerCase();
|
|
30
|
-
if (extension === ".exe" || extension === ".com")
|
|
31
|
-
return [path, args];
|
|
32
|
-
if ([".js", ".mjs", ".cjs"].includes(extension))
|
|
33
|
-
return [process.execPath, [path, ...args]];
|
|
34
|
-
if (extension === ".cmd") {
|
|
35
|
-
// npm (cmd-shim): SET "_prog=node" … "%dp0%\entry" %*; pnpm (@zkochan/cmd-shim): node "%~dp0\entry" %*
|
|
36
|
-
const shim = readFileSync(path, "utf8");
|
|
37
|
-
const runsNode = /SET "_prog=node"/i.test(shim) || /^\s*node\s+"%~dp0\\/im.test(shim);
|
|
38
|
-
const entry = runsNode ? /"%~?dp0%?\\([^"]+)"\s+%\*/i.exec(shim)?.[1] : undefined;
|
|
39
|
-
if (entry)
|
|
40
|
-
return [process.execPath, [win32.join(win32.dirname(path), entry), ...args]];
|
|
41
|
-
}
|
|
42
|
-
throw new Error(`Cannot run ${path} without a shell; pass --pi/--codex with an .exe or Node script`);
|
|
43
|
-
}
|
|
44
|
-
/** Discover installed executables only; authentication/capability errors stay explicit. */
|
|
45
|
-
export async function installedHarnesses(cwd, options, path = process.env.PATH ?? "") {
|
|
46
|
-
const names = options.pi || options.codex ? ["pi", "codex"].filter((name) => options[name]) : ["pi", "codex"];
|
|
47
|
-
const found = await Promise.all(names.map(async (name) => {
|
|
48
|
-
const binary = options[name] || name;
|
|
49
|
-
const candidates = harnessExecutableCandidates(binary, cwd, path);
|
|
50
|
-
for (const candidate of candidates) {
|
|
51
|
-
try {
|
|
52
|
-
await access(candidate, process.platform === "win32" ? constants.F_OK : constants.X_OK);
|
|
53
|
-
if ((await stat(candidate)).isFile())
|
|
54
|
-
return name;
|
|
55
|
-
}
|
|
56
|
-
catch { /* Try the next PATH entry. */ }
|
|
57
|
-
}
|
|
58
|
-
return null;
|
|
59
|
-
}));
|
|
60
|
-
return found.filter((name) => name !== null);
|
|
61
|
-
}
|
|
62
|
-
const array = (value) => Array.isArray(value) ? value : [];
|
|
63
|
-
const text = (value) => typeof value === "string" ? value : "";
|
|
64
|
-
/** Aborted streams leave tool calls with an empty id or name. */
|
|
65
|
-
export const identifiable = (value) => typeof value === "string" && value.length > 0;
|
|
66
|
-
export function piContent(value) {
|
|
67
|
-
return array(value).flatMap((entry) => {
|
|
68
|
-
const block = record(entry);
|
|
69
|
-
if (block.type === "text")
|
|
70
|
-
return [{ type: "text", text: text(block.text) }];
|
|
71
|
-
if (block.type === "thinking")
|
|
72
|
-
return [{ type: "thinking", thinking: text(block.thinking), ...(typeof (block.thinkingSignature ?? block.signature) === "string" ? { signature: String(block.thinkingSignature ?? block.signature) } : {}) }];
|
|
73
|
-
if (block.type === "toolCall")
|
|
74
|
-
return identifiable(block.id) && identifiable(block.name) ? [{ type: "tool_use", id: block.id, name: block.name, input: record(block.arguments) }] : [];
|
|
75
|
-
if (block.type === "image")
|
|
76
|
-
return [{ type: "image", source: { type: "base64", data: text(block.data), media_type: text(block.mimeType) } }];
|
|
77
|
-
return [{ type: "text", text: JSON.stringify(block) }];
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
export async function imageForPi(block) {
|
|
81
|
-
if (block.source.type === "base64")
|
|
82
|
-
return { type: "image", data: block.source.data, mimeType: block.source.media_type };
|
|
83
|
-
const image = await downloadPublicImage(block.source.url);
|
|
84
|
-
return { type: "image", data: Buffer.from(image.bytes).toString("base64"), mimeType: image.mimeType };
|
|
85
|
-
}
|
|
86
|
-
async function initializeCodex(rpc) {
|
|
87
|
-
await rpc.request("initialize", { clientInfo: { name: "cohub", title: "Cohub", version: "1" }, capabilities: { experimentalApi: true } });
|
|
88
|
-
rpc.write({ method: "initialized", params: {} });
|
|
89
|
-
}
|
|
90
|
-
export async function discoverHarnesses(harnesses, options, cwd) {
|
|
91
|
-
const models = [];
|
|
92
|
-
await Promise.all(harnesses.map(async (harness) => {
|
|
93
|
-
const rpc = new JsonRpcProcess(...harnessCommand(options[harness] || harness, harness === "pi" ? ["--mode", "rpc", "--no-session"] : ["app-server", "--listen", "stdio://"], cwd), cwd, harness);
|
|
94
|
-
try {
|
|
95
|
-
if (harness === "pi") {
|
|
96
|
-
const result = await rpc.request("get_available_models");
|
|
97
|
-
const state = await rpc.request("get_state");
|
|
98
|
-
models.push(...piModelCatalog(array(result.models), state));
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
await initializeCodex(rpc);
|
|
102
|
-
const config = await rpc.request("config/read", { includeLayers: false, cwd });
|
|
103
|
-
const entries = [];
|
|
104
|
-
const cursors = new Set();
|
|
105
|
-
let cursor = null;
|
|
106
|
-
do {
|
|
107
|
-
const result = await rpc.request("model/list", { cursor, limit: 100 });
|
|
108
|
-
entries.push(...array(result.data));
|
|
109
|
-
cursor = typeof result.nextCursor === "string" ? result.nextCursor : null;
|
|
110
|
-
if (entries.length > 2000 || cursor && cursors.has(cursor))
|
|
111
|
-
throw new Error("Invalid model catalog pagination");
|
|
112
|
-
if (cursor)
|
|
113
|
-
cursors.add(cursor);
|
|
114
|
-
} while (cursor);
|
|
115
|
-
models.push(...codexModelCatalog(config, entries));
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
finally {
|
|
119
|
-
await rpc.close();
|
|
120
|
-
}
|
|
121
|
-
}));
|
|
122
|
-
return { harnesses, models };
|
|
123
|
-
}
|
|
124
|
-
export function codexItemContent(item) {
|
|
125
|
-
if (item.type === "agentMessage" || item.type === "plan")
|
|
126
|
-
return [{ type: "text", text: text(item.text) }];
|
|
127
|
-
if (item.type === "reasoning")
|
|
128
|
-
return [{ type: "thinking", thinking: [...array(item.summary), ...array(item.content)].map(text).join("\n") }];
|
|
129
|
-
if (item.type === "userMessage")
|
|
130
|
-
return [];
|
|
131
|
-
if (item.type === "contextCompaction")
|
|
132
|
-
return [{ type: "system_note", note_type: "compacted", text: "Context compacted" }];
|
|
133
|
-
const id = item.id;
|
|
134
|
-
if (!identifiable(id))
|
|
135
|
-
return [];
|
|
136
|
-
const name = item.type === "commandExecution" ? "bash" : item.type === "fileChange" ? "apply_patch" : text(item.tool) || text(item.type);
|
|
137
|
-
const input = item.type === "commandExecution" ? { command: item.command, cwd: item.cwd } : item.type === "fileChange" ? { changes: item.changes } : record(item.arguments);
|
|
138
|
-
return [
|
|
139
|
-
{ type: "tool_use", id, name, input },
|
|
140
|
-
{ type: "tool_result", tool_use_id: id, content: typeof item.aggregatedOutput === "string" ? item.aggregatedOutput : JSON.stringify(item.result ?? item.error ?? item.changes ?? item), is_error: item.status === "failed" || typeof item.exitCode === "number" && item.exitCode !== 0 },
|
|
141
|
-
];
|
|
142
|
-
}
|