@neta-art/cohub-cli 8.1.1 → 8.3.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/dist/commands/runtime.d.ts +0 -12
- package/dist/commands/runtime.js +129 -119
- package/dist/commands/search.js +3 -9
- package/dist/commands/spaces.js +5 -2
- package/dist/output.d.ts +6 -0
- package/dist/output.js +11 -0
- package/dist/runtime/archive-store.d.ts +4 -0
- package/dist/runtime/archive-store.js +10 -6
- package/dist/runtime/codex-usage.d.ts +0 -2
- package/dist/runtime/codex-usage.js +0 -12
- package/dist/runtime/connection.d.ts +5 -5
- package/dist/runtime/connection.js +27 -16
- package/dist/runtime/harness.d.ts +8 -16
- package/dist/runtime/harness.js +1 -366
- package/dist/runtime/instance.d.ts +8 -1
- package/dist/runtime/instance.js +15 -8
- package/dist/runtime/launch.js +10 -5
- package/dist/runtime/native/adapters.d.ts +32 -0
- package/dist/runtime/native/adapters.js +79 -0
- package/dist/runtime/{native-archive.d.ts → native/archive.d.ts} +4 -3
- package/dist/runtime/{native-archive.js → native/archive.js} +7 -14
- package/dist/runtime/native/attach.d.ts +33 -0
- package/dist/runtime/native/attach.js +103 -0
- package/dist/runtime/native/codex-link.d.ts +40 -0
- package/dist/runtime/native/codex-link.js +249 -0
- package/dist/runtime/native/config.d.ts +16 -0
- package/dist/runtime/native/config.js +24 -0
- package/dist/runtime/native/daemon.d.ts +88 -0
- package/dist/runtime/native/daemon.js +367 -0
- package/dist/runtime/native/execution.d.ts +45 -0
- package/dist/runtime/native/execution.js +337 -0
- package/dist/runtime/native/identity.d.ts +9 -0
- package/dist/runtime/native/identity.js +15 -0
- package/dist/runtime/native/ingest.d.ts +241 -0
- package/dist/runtime/native/ingest.js +823 -0
- package/dist/runtime/native/install.d.ts +9 -0
- package/dist/runtime/native/install.js +105 -0
- package/dist/runtime/native/pi-extension.d.ts +35 -0
- package/dist/runtime/native/pi-extension.js +160 -0
- package/dist/runtime/native/pi-link.d.ts +67 -0
- package/dist/runtime/native/pi-link.js +238 -0
- package/dist/runtime/native/results.d.ts +41 -0
- package/dist/runtime/native/results.js +128 -0
- package/dist/runtime/native/sessions.d.ts +45 -0
- package/dist/runtime/native/sessions.js +92 -0
- package/dist/runtime/native/transcript.d.ts +63 -0
- package/dist/runtime/native/transcript.js +594 -0
- package/dist/runtime/native/translate.d.ts +44 -0
- package/dist/runtime/native/translate.js +263 -0
- package/dist/runtime/native/transport.d.ts +7 -0
- package/dist/runtime/native/transport.js +16 -0
- package/dist/runtime/native/version.d.ts +5 -0
- package/dist/runtime/native/version.js +18 -0
- package/dist/runtime/presentation.d.ts +7 -10
- package/dist/runtime/presentation.js +35 -10
- package/dist/runtime/projection-store.d.ts +6 -13
- package/dist/runtime/projection-store.js +11 -74
- package/dist/runtime/supervisor.d.ts +2 -0
- package/dist/runtime/supervisor.js +36 -86
- package/dist/runtime/turn-projection.d.ts +1 -3
- package/dist/runtime/turn-projection.js +3 -10
- package/package.json +4 -5
- package/dist/runtime/native-attach.d.ts +0 -28
- package/dist/runtime/native-attach.js +0 -47
- package/dist/runtime/native-codex-hook.d.ts +0 -1
- package/dist/runtime/native-codex-hook.js +0 -28
- package/dist/runtime/native-install.d.ts +0 -21
- package/dist/runtime/native-install.js +0 -130
- package/dist/runtime/native-ipc.d.ts +0 -37
- package/dist/runtime/native-ipc.js +0 -118
- package/dist/runtime/native-pi-extension.d.ts +0 -20
- package/dist/runtime/native-pi-extension.js +0 -47
- package/dist/runtime/native-sync-store.d.ts +0 -102
- package/dist/runtime/native-sync-store.js +0 -368
- package/dist/runtime/native-sync.d.ts +0 -50
- package/dist/runtime/native-sync.js +0 -221
- package/dist/runtime/native-transcript.d.ts +0 -35
- package/dist/runtime/native-transcript.js +0 -310
- package/dist/runtime/session-store.d.ts +0 -84
- package/dist/runtime/session-store.js +0 -587
|
@@ -1,15 +1,3 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
-
import { type NativeIpcResponse } from "../runtime/native-ipc.js";
|
|
3
2
|
export { resolveLocalSpaceName, parseRuntimeHarnesses } from "../runtime/launch.js";
|
|
4
|
-
export declare function applyNativeImportResponse(result: {
|
|
5
|
-
imported: number;
|
|
6
|
-
failed: Array<{
|
|
7
|
-
path: string;
|
|
8
|
-
message: string;
|
|
9
|
-
}>;
|
|
10
|
-
skipped: Array<{
|
|
11
|
-
path: string;
|
|
12
|
-
message: string;
|
|
13
|
-
}>;
|
|
14
|
-
}, path: string, response: NativeIpcResponse): void;
|
|
15
3
|
export declare function registerRuntime(program: Command): void;
|
package/dist/commands/runtime.js
CHANGED
|
@@ -4,28 +4,31 @@ import { json as outJson, jsonRequested } from "../output.js";
|
|
|
4
4
|
import { currentIdentityKey } from "../space.js";
|
|
5
5
|
import { resolveRuntimeTarget, runtimeUp, parseRuntimeHarnesses } from "../runtime/launch.js";
|
|
6
6
|
import { canonicalRuntimeRoot, getRuntimeSpaceBinding } from "../runtime/space-binding.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { RuntimeArchiveStore } from "../runtime/archive-store.js";
|
|
9
|
+
import { readNativeConfig, runtimeStateRoot, writeNativeConfig } from "../runtime/native/config.js";
|
|
10
|
+
import { MAX_IMPORT_CONCURRENCY, DEFAULT_IMPORT_CONCURRENCY } from "../runtime/native/ingest.js";
|
|
11
|
+
import { installPiExtension, piExtensionState } from "../runtime/native/install.js";
|
|
12
|
+
import { controlRuntimeInstance, requestRuntimeInstance, runtimeInstanceDirectory } from "../runtime/instance.js";
|
|
13
|
+
import { atLeastLevel, diagnosticLevels, formatDiagnostic, formatImportProgress, formatNativeSync, printRuntimeSummary } from "../runtime/presentation.js";
|
|
14
14
|
import { readRuntimeDiagnosticEvents, RuntimeDiagnosticReader, runtimeDiagnosticsDirectory, serializeDiagnosticError } from "../runtime/diagnostics.js";
|
|
15
15
|
export { resolveLocalSpaceName, parseRuntimeHarnesses } from "../runtime/launch.js";
|
|
16
16
|
const reportFailure = (cause) => {
|
|
17
17
|
process.stderr.write(`Runtime failed: ${serializeDiagnosticError(cause).message}\n`);
|
|
18
18
|
process.exitCode = 1;
|
|
19
19
|
};
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
const megabytes = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
21
|
+
/** The bound Space of a directory, with its running Runtime; import and attach act through it. */
|
|
22
|
+
async function boundRuntime(program, dir, space) {
|
|
23
|
+
const identity = currentIdentityKey();
|
|
24
|
+
if (!identity)
|
|
25
|
+
throw new Error("Sign in first");
|
|
26
|
+
const root = await canonicalRuntimeRoot(dir ?? process.cwd());
|
|
27
|
+
const spaceId = await resolveRuntimeTarget(program, space);
|
|
28
|
+
const binding = await getRuntimeSpaceBinding(root, identity);
|
|
29
|
+
if (!binding || binding.spaceId !== spaceId)
|
|
30
|
+
throw new Error("Bind this directory with runtime up first");
|
|
31
|
+
return { identity, root, spaceId, directory: runtimeInstanceDirectory(identity, spaceId) };
|
|
29
32
|
}
|
|
30
33
|
export function registerRuntime(program) {
|
|
31
34
|
const runtime = program.command("runtime").description("Connect a local workspace");
|
|
@@ -49,37 +52,68 @@ export function registerRuntime(program) {
|
|
|
49
52
|
reportFailure(cause);
|
|
50
53
|
}
|
|
51
54
|
});
|
|
52
|
-
runtime.command("
|
|
53
|
-
.description("
|
|
55
|
+
runtime.command("attach")
|
|
56
|
+
.description("Connect native harnesses to Cohub: install the Pi extension")
|
|
54
57
|
.option("-s, --space <id>", "Target Space")
|
|
55
|
-
.option("--harness <name>", "Pi
|
|
58
|
+
.option("--harness <name>", "Pi; repeatable", (value, previous) => [...previous, value], [])
|
|
56
59
|
.option("--json", "JSON output")
|
|
57
60
|
.action(async (options) => {
|
|
58
61
|
try {
|
|
59
|
-
const
|
|
62
|
+
const harnesses = parseRuntimeHarnesses(options.harness.length ? options.harness : ["pi"]);
|
|
63
|
+
if (!harnesses.includes("pi"))
|
|
64
|
+
throw new Error("Codex needs no extension; runtime up offers its shared app-server");
|
|
65
|
+
const before = await piExtensionState();
|
|
66
|
+
if (before === "foreign")
|
|
67
|
+
throw new Error("A Pi extension named cohub exists and is not managed by Cohub; move it aside, then retry");
|
|
68
|
+
await installPiExtension();
|
|
69
|
+
const spaceId = await resolveRuntimeTarget(program, options.space).catch(() => null);
|
|
60
70
|
const identity = currentIdentityKey();
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
const root = await canonicalRuntimeRoot(process.cwd());
|
|
64
|
-
if ((await getRuntimeSpaceBinding(root, identity))?.spaceId !== spaceId)
|
|
65
|
-
throw new Error("Bind this directory with runtime up first");
|
|
66
|
-
const instance = await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId));
|
|
67
|
-
const harnesses = parseRuntimeHarnesses(options.harness.length ? options.harness : instance?.harnesses ?? ["pi", "codex"]);
|
|
68
|
-
const result = await installNativeSync({ root, spaceId, identity, harnesses, disabled: true });
|
|
71
|
+
if (spaceId && identity)
|
|
72
|
+
await controlRuntimeInstance(runtimeInstanceDirectory(identity, spaceId), "reload").catch(() => undefined);
|
|
69
73
|
if (jsonRequested(options))
|
|
70
|
-
outJson({
|
|
74
|
+
outJson({ harness: "pi", extension: "installed", updated: before !== "installed" });
|
|
71
75
|
else
|
|
72
|
-
process.stdout.write("
|
|
76
|
+
process.stdout.write(before === "installed" ? "Pi extension is up to date\n" : "Pi extension installed. Run /reload in open Pi sessions to connect them\n");
|
|
77
|
+
}
|
|
78
|
+
catch (cause) {
|
|
79
|
+
reportFailure(cause);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
runtime.command("detach")
|
|
83
|
+
.description("Pause native sync; keep all data")
|
|
84
|
+
.option("-s, --space <id>", "Target Space")
|
|
85
|
+
.option("--harness <name>", "Pi or Codex; repeatable", (value, previous) => [...previous, value], [])
|
|
86
|
+
.option("--json", "JSON output")
|
|
87
|
+
.action(async (options) => {
|
|
88
|
+
try {
|
|
89
|
+
const { identity, spaceId, directory } = await boundRuntime(program, undefined, options.space);
|
|
90
|
+
const stateRoot = runtimeStateRoot(spaceId);
|
|
91
|
+
const config = await readNativeConfig(stateRoot, identity);
|
|
92
|
+
const paused = parseRuntimeHarnesses(options.harness.length ? options.harness : config?.harnesses ?? ["pi", "codex"]);
|
|
93
|
+
const harnesses = (config?.harnesses ?? []).filter((harness) => !paused.includes(harness));
|
|
94
|
+
if (config)
|
|
95
|
+
await writeNativeConfig(stateRoot, { ...config, harnesses, codexShared: harnesses.includes("codex") && config.codexShared });
|
|
96
|
+
await controlRuntimeInstance(directory, "reload").catch(() => undefined);
|
|
97
|
+
const codexWasShared = Boolean(config?.codexShared) && paused.includes("codex");
|
|
98
|
+
if (jsonRequested(options))
|
|
99
|
+
outJson({ spaceId, harnesses, paused, enabled: harnesses.length > 0 });
|
|
100
|
+
else {
|
|
101
|
+
process.stdout.write(harnesses.length ? `Native sync paused for ${paused.join(", ")}; still syncing ${harnesses.join(", ")}\n` : "Native sync paused; all data retained\n");
|
|
102
|
+
// Cohub never stops a server that terminal clients may be attached to.
|
|
103
|
+
if (codexWasShared)
|
|
104
|
+
process.stdout.write("Codex's shared app-server keeps running; stop it with codex app-server daemon stop\n");
|
|
105
|
+
}
|
|
73
106
|
}
|
|
74
107
|
catch (cause) {
|
|
75
108
|
reportFailure(cause);
|
|
76
109
|
}
|
|
77
110
|
});
|
|
78
111
|
runtime.command("import [dir]")
|
|
79
|
-
.description("Import local conversations")
|
|
112
|
+
.description("Import earlier local conversations, newest first")
|
|
80
113
|
.option("-s, --space <id>", "Target Space")
|
|
81
114
|
.option("--harness <name>", "Filter by harness; repeatable", (value, previous) => [...previous, value], [])
|
|
82
115
|
.option("--session <id>", "Filter by native session ID")
|
|
116
|
+
.option("--concurrency <count>", `Conversations read in parallel, 1 to ${MAX_IMPORT_CONCURRENCY}`, String(DEFAULT_IMPORT_CONCURRENCY))
|
|
83
117
|
.option("--dry-run", "Preview without importing")
|
|
84
118
|
.option("-y, --yes", "Skip confirmation")
|
|
85
119
|
.option("--json", "Output as JSON")
|
|
@@ -88,66 +122,34 @@ export function registerRuntime(program) {
|
|
|
88
122
|
const stop = () => controller.abort();
|
|
89
123
|
process.once("SIGINT", stop);
|
|
90
124
|
process.once("SIGTERM", stop);
|
|
125
|
+
const asJson = jsonRequested(options);
|
|
91
126
|
try {
|
|
92
|
-
const
|
|
93
|
-
if (!
|
|
94
|
-
throw new Error(
|
|
95
|
-
const root = await
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const config = await readNativeSyncConfig(nativeRuntimeRoot(spaceId), identity);
|
|
104
|
-
if (!config || config.root !== root)
|
|
105
|
-
throw new Error("Native sync is not enabled for this directory");
|
|
106
|
-
const harnesses = parseRuntimeHarnesses(options.harness.length ? options.harness : config.harnesses);
|
|
107
|
-
let lastProgress = 0;
|
|
108
|
-
const discovered = await discoverNativeImportCandidates(root, harnesses, {
|
|
109
|
-
signal: controller.signal,
|
|
110
|
-
onProgress: ({ harness, scanned, candidates: found }) => {
|
|
111
|
-
if (jsonRequested(options) || scanned - lastProgress < 25)
|
|
112
|
-
return;
|
|
113
|
-
lastProgress = scanned;
|
|
114
|
-
process.stderr.write(`Scanned ${scanned} ${harness} transcript${scanned === 1 ? "" : "s"}; found ${found}\n`);
|
|
115
|
-
},
|
|
116
|
-
});
|
|
117
|
-
const candidates = discovered.candidates.filter((candidate) => !options.session || candidate.nativeSessionId === options.session);
|
|
118
|
-
const result = {
|
|
119
|
-
spaceId, root, harnesses, candidates, errors: discovered.errors,
|
|
120
|
-
imported: 0, failed: [], skipped: [], dryRun: Boolean(options.dryRun),
|
|
121
|
-
complete: discovered.errors.length === 0,
|
|
127
|
+
const concurrency = Number(options.concurrency);
|
|
128
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1 || concurrency > MAX_IMPORT_CONCURRENCY)
|
|
129
|
+
throw new Error(`Use a concurrency from 1 to ${MAX_IMPORT_CONCURRENCY}`);
|
|
130
|
+
const { spaceId, root, directory } = await boundRuntime(program, dir, options.space);
|
|
131
|
+
// The Runtime does the work, so an import survives this command: Ctrl-C only pauses it.
|
|
132
|
+
const filter = { harnesses: options.harness.length ? parseRuntimeHarnesses(options.harness) : [], ...(options.session ? { nativeSessionId: options.session } : {}) };
|
|
133
|
+
const control = async (command, timeoutMs = 60_000) => {
|
|
134
|
+
const response = await controlRuntimeInstance(directory, "import", { ...filter, command, concurrency }, timeoutMs);
|
|
135
|
+
if (!response)
|
|
136
|
+
throw new Error("Local Runtime is not running; start it with cohub runtime up");
|
|
137
|
+
return response.result;
|
|
122
138
|
};
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (!candidates.length) {
|
|
131
|
-
if (!result.complete)
|
|
132
|
-
process.exitCode = 1;
|
|
133
|
-
if (jsonRequested(options))
|
|
134
|
-
outJson(result);
|
|
135
|
-
else
|
|
136
|
-
process.stdout.write("No existing native conversations found\n");
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (options.dryRun) {
|
|
140
|
-
if (jsonRequested(options))
|
|
141
|
-
outJson(result);
|
|
139
|
+
const plan = await control("plan");
|
|
140
|
+
const summary = { spaceId, root, ...plan, dryRun: Boolean(options.dryRun) };
|
|
141
|
+
if (options.dryRun || !plan.files) {
|
|
142
|
+
if (asJson)
|
|
143
|
+
outJson(summary);
|
|
144
|
+
else if (!plan.files)
|
|
145
|
+
process.stdout.write("No earlier conversations left to import\n");
|
|
142
146
|
else {
|
|
143
|
-
process.stdout.write(`Found ${
|
|
144
|
-
for (const
|
|
145
|
-
process.stdout.write(` ${
|
|
146
|
-
|
|
147
|
-
process.stdout.write(`
|
|
147
|
+
process.stdout.write(`Found ${plan.files} conversation${plan.files === 1 ? "" : "s"} · ${megabytes(plan.bytes)}\n`);
|
|
148
|
+
for (const item of plan.sample)
|
|
149
|
+
process.stdout.write(` ${item.harness.padEnd(5)} ${new Date(item.mtimeMs).toISOString().slice(0, 16).replace("T", " ")} ${item.nativeSessionId ?? ""} ${item.path}\n`);
|
|
150
|
+
if (plan.files > plan.sample.length)
|
|
151
|
+
process.stdout.write(` … and ${plan.files - plan.sample.length} more\n`);
|
|
148
152
|
}
|
|
149
|
-
if (!result.complete)
|
|
150
|
-
process.exitCode = 1;
|
|
151
153
|
return;
|
|
152
154
|
}
|
|
153
155
|
if (!options.yes) {
|
|
@@ -156,10 +158,10 @@ export function registerRuntime(program) {
|
|
|
156
158
|
const { createInterface } = await import("node:readline/promises");
|
|
157
159
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
158
160
|
try {
|
|
159
|
-
const answer = await rl.question(`Import ${
|
|
161
|
+
const answer = await rl.question(`Import ${plan.files} conversation${plan.files === 1 ? "" : "s"} (${megabytes(plan.bytes)}) to Space ${spaceId}? [Y/n] `);
|
|
160
162
|
if (!/^(|y(es)?)$/i.test(answer.trim())) {
|
|
161
|
-
if (
|
|
162
|
-
outJson({ ...
|
|
163
|
+
if (asJson)
|
|
164
|
+
outJson({ ...summary, cancelled: true });
|
|
163
165
|
else
|
|
164
166
|
process.stdout.write("Import cancelled\n");
|
|
165
167
|
return;
|
|
@@ -169,26 +171,31 @@ export function registerRuntime(program) {
|
|
|
169
171
|
rl.close();
|
|
170
172
|
}
|
|
171
173
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
174
|
+
let job = await control("start");
|
|
175
|
+
while (!controller.signal.aborted && job.state === "running") {
|
|
176
|
+
if (!asJson && process.stderr.isTTY)
|
|
177
|
+
process.stderr.write(`\r\x1b[2KImporting ${formatImportProgress(job)}`);
|
|
178
|
+
await delay(500, undefined, { signal: controller.signal }).catch(() => undefined);
|
|
179
|
+
const status = await requestRuntimeInstance(directory);
|
|
180
|
+
if (!status?.native)
|
|
181
|
+
throw new Error("Local Runtime stopped; the import resumes when it starts again");
|
|
182
|
+
job = status.native.import;
|
|
181
183
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
if (controller.signal.aborted)
|
|
185
|
+
job = await control("pause", 5_000);
|
|
186
|
+
const paused = job.state === "paused";
|
|
187
|
+
if (asJson)
|
|
188
|
+
outJson({ ...summary, ...job, paused });
|
|
185
189
|
else {
|
|
186
|
-
process.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
+
if (process.stderr.isTTY)
|
|
191
|
+
process.stderr.write("\r\x1b[2K");
|
|
192
|
+
process.stdout.write(`${paused ? "Paused" : "Imported"} ${formatImportProgress(job)}\n`);
|
|
193
|
+
for (const failure of job.failed)
|
|
194
|
+
process.stdout.write(`Skipped ${failure.path}: ${failure.message}\n`);
|
|
195
|
+
if (paused)
|
|
196
|
+
process.stdout.write("Run cohub runtime import again to continue\n");
|
|
190
197
|
}
|
|
191
|
-
if (
|
|
198
|
+
if (job.failed.length)
|
|
192
199
|
process.exitCode = 1;
|
|
193
200
|
}
|
|
194
201
|
catch (cause) {
|
|
@@ -208,16 +215,15 @@ export function registerRuntime(program) {
|
|
|
208
215
|
const identity = currentIdentityKey();
|
|
209
216
|
const local = identity ? await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId)) : null;
|
|
210
217
|
const space = createClient().space(spaceId);
|
|
211
|
-
const
|
|
212
|
-
const
|
|
218
|
+
const stateRoot = runtimeStateRoot(spaceId);
|
|
219
|
+
const archives = new RuntimeArchiveStore(join(stateRoot, "archives"));
|
|
220
|
+
const [remote, pendingLocalArchives, failedLocalArchives] = await Promise.all([
|
|
213
221
|
space.getRuntime(undefined, { signal: AbortSignal.timeout(5000) }).then((value) => ({ value, error: null })).catch((error) => ({ value: null, error: serializeDiagnosticError(error).message })),
|
|
214
|
-
|
|
215
|
-
identity ? listNativeSyncStores(store.root, spaceId, identity) : [],
|
|
222
|
+
archives.pendingCount(), archives.failedCaptureCount(),
|
|
216
223
|
]);
|
|
217
|
-
const nativeSessions = await Promise.all(nativeStores.map((native) => native.status()));
|
|
218
224
|
// A corrupt native sync config must never take down the whole status report.
|
|
219
|
-
const nativeSync = identity ? await
|
|
220
|
-
const result = { ...remote.value, spaceId, local, remote: remote.value, remoteError: remote.error, diagnosticsPath: runtimeDiagnosticsDirectory(
|
|
225
|
+
const nativeSync = identity ? await readNativeConfig(stateRoot, identity).then((config) => ({ config, error: null }), (error) => ({ config: null, error: serializeDiagnosticError(error).message })) : { config: null, error: null };
|
|
226
|
+
const result = { ...remote.value, spaceId, local, remote: remote.value, remoteError: remote.error, diagnosticsPath: runtimeDiagnosticsDirectory(stateRoot), pendingLocalArchives, failedLocalArchives, nativeSync: nativeSync.config, nativeSyncError: nativeSync.error, native: local?.native ?? null };
|
|
221
227
|
if (jsonRequested(options))
|
|
222
228
|
outJson(result);
|
|
223
229
|
else {
|
|
@@ -226,7 +232,7 @@ export function registerRuntime(program) {
|
|
|
226
232
|
else
|
|
227
233
|
process.stdout.write(`Local process Not running\nSpace ${spaceId}\nLogs ${result.diagnosticsPath}\n`);
|
|
228
234
|
process.stdout.write(`Server ${remote.error ? `Unknown — ${remote.error}` : remote.value?.online ? "Harness connected" : "Offline"}\nArchives ${pendingLocalArchives} pending · ${failedLocalArchives} failed\n`);
|
|
229
|
-
process.stdout.write(formatNativeSync(nativeSync.config,
|
|
235
|
+
process.stdout.write(formatNativeSync(nativeSync.config, local?.native, nativeSync.error));
|
|
230
236
|
}
|
|
231
237
|
}
|
|
232
238
|
catch (cause) {
|
|
@@ -260,8 +266,12 @@ export function registerRuntime(program) {
|
|
|
260
266
|
throw new Error("Runtime is still stopping; inspect logs");
|
|
261
267
|
if (jsonRequested(options))
|
|
262
268
|
outJson({ spaceId, stopped: true });
|
|
263
|
-
else
|
|
269
|
+
else {
|
|
264
270
|
process.stdout.write("Runtime stopped; data retained\n");
|
|
271
|
+
// Terminal clients may still be attached to it, so it is never stopped for them.
|
|
272
|
+
if (local?.native?.codex?.control === "shared")
|
|
273
|
+
process.stdout.write("Codex's shared app-server keeps running; stop it with codex app-server daemon stop\n");
|
|
274
|
+
}
|
|
265
275
|
}
|
|
266
276
|
catch (cause) {
|
|
267
277
|
reportFailure(cause);
|
|
@@ -278,18 +288,18 @@ export function registerRuntime(program) {
|
|
|
278
288
|
const stop = () => controller.abort();
|
|
279
289
|
try {
|
|
280
290
|
const spaceId = await resolveRuntimeTarget(program, options.space);
|
|
281
|
-
const
|
|
291
|
+
const stateRoot = runtimeStateRoot(spaceId);
|
|
282
292
|
const limit = Number(options.limit);
|
|
283
293
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 10_000)
|
|
284
294
|
throw new Error("Use a limit from 1 to 10000");
|
|
285
295
|
if (!diagnosticLevels.includes(options.level))
|
|
286
296
|
throw new Error("Use debug, info, warn or error");
|
|
287
297
|
const asJson = jsonRequested(options);
|
|
288
|
-
const reader = new RuntimeDiagnosticReader(
|
|
298
|
+
const reader = new RuntimeDiagnosticReader(stateRoot);
|
|
289
299
|
process.once("SIGINT", stop);
|
|
290
300
|
process.once("SIGTERM", stop);
|
|
291
301
|
do {
|
|
292
|
-
const events = (options.follow ? await reader.read({ limit }) : await readRuntimeDiagnosticEvents(
|
|
302
|
+
const events = (options.follow ? await reader.read({ limit }) : await readRuntimeDiagnosticEvents(stateRoot, { limit }))
|
|
293
303
|
.filter((event) => atLeastLevel(event.level, options.level));
|
|
294
304
|
if (asJson && !options.follow)
|
|
295
305
|
outJson(events);
|
package/dist/commands/search.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isUuid } from "@neta-art/cohub";
|
|
2
2
|
import { createClient } from "../client.js";
|
|
3
|
-
import { formatLocalDateTime, table, json as outJson, jsonRequested, error, handleHttp } from "../output.js";
|
|
3
|
+
import { formatLocalDateTime, table, json as outJson, jsonRequested, error, handleHttp, truncateText } from "../output.js";
|
|
4
4
|
const DEFAULT_LIMIT = 20;
|
|
5
5
|
const MAX_TITLE_LENGTH = 72;
|
|
6
6
|
const MAX_CONTEXT_LENGTH = 42;
|
|
@@ -11,12 +11,6 @@ function clampLimit(value) {
|
|
|
11
11
|
return DEFAULT_LIMIT;
|
|
12
12
|
return Math.min(Math.max(Math.floor(parsed), 1), 50);
|
|
13
13
|
}
|
|
14
|
-
function truncate(value, maxLength) {
|
|
15
|
-
const text = (value ?? "").replace(/\s+/g, " ").trim();
|
|
16
|
-
if (text.length <= maxLength)
|
|
17
|
-
return text;
|
|
18
|
-
return `${text.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
|
|
19
|
-
}
|
|
20
14
|
function contextFor(item) {
|
|
21
15
|
const owner = item.ownerProfile;
|
|
22
16
|
if (owner?.username)
|
|
@@ -47,8 +41,8 @@ function parseSearchInput(opts) {
|
|
|
47
41
|
function rowsFor(items) {
|
|
48
42
|
return items.map((item) => ({
|
|
49
43
|
type: item.type,
|
|
50
|
-
title:
|
|
51
|
-
context:
|
|
44
|
+
title: truncateText(item.title, MAX_TITLE_LENGTH),
|
|
45
|
+
context: truncateText(contextFor(item), MAX_CONTEXT_LENGTH),
|
|
52
46
|
match: item.matchedField,
|
|
53
47
|
updated: item.updatedAt ? formatLocalDateTime(item.updatedAt).slice(0, 10) : "",
|
|
54
48
|
href: item.href,
|
package/dist/commands/spaces.js
CHANGED
|
@@ -5,7 +5,7 @@ import { resolveCohubEnvironment } from "@neta-art/cohub";
|
|
|
5
5
|
import { uploadAvatarAsset, uploadChatImageAsset } from "../avatar.js";
|
|
6
6
|
import { createClient } from "../client.js";
|
|
7
7
|
import { putLocalFile } from "../http-put.js";
|
|
8
|
-
import { table, json as outJson, jsonRequested, ok, error, handleHttp, formatEpochMs } from "../output.js";
|
|
8
|
+
import { table, json as outJson, jsonRequested, ok, error, handleHttp, formatEpochMs, truncateText } from "../output.js";
|
|
9
9
|
import { resolveSpace } from "../space.js";
|
|
10
10
|
import { registerSpaceCommerce } from "./space-commerce.js";
|
|
11
11
|
import { registerSpaceActivity } from "./space-activity.js";
|
|
@@ -17,6 +17,8 @@ const defaultIdleTtlSeconds = cliEnv === "prod" ? 12 * 60 * 60 : 10 * 60;
|
|
|
17
17
|
const SPACE_ROLES = ["host", "builder", "guest"];
|
|
18
18
|
const SANDBOX_SPEC_IDS = ["standard", "boost", "ultra"];
|
|
19
19
|
const LABEL_RESOURCE_TYPES = ["session", "checkpoint", "file"];
|
|
20
|
+
/** Table cell width for space descriptions; `--json` keeps the full value. */
|
|
21
|
+
const SPACE_DESCRIPTION_COLUMN_WIDTH = 40;
|
|
20
22
|
function parseInteger(value, name, options = {}) {
|
|
21
23
|
if (!/^-?\d+$/.test(value.trim()))
|
|
22
24
|
return error(`Invalid ${name}`, `${name} must be an integer`);
|
|
@@ -476,6 +478,7 @@ export function registerSpaces(program) {
|
|
|
476
478
|
table(filtered, [
|
|
477
479
|
{ key: "id", label: "ID" },
|
|
478
480
|
{ key: "name", label: "Name" },
|
|
481
|
+
{ key: "description", label: "Description", format: (value) => truncateText(value, SPACE_DESCRIPTION_COLUMN_WIDTH) },
|
|
479
482
|
{ key: "isPinned", label: "Pinned" },
|
|
480
483
|
{ key: "createdAt", label: "Created" },
|
|
481
484
|
]);
|
|
@@ -500,7 +503,7 @@ export function registerSpaces(program) {
|
|
|
500
503
|
{ key: "id", label: "ID" },
|
|
501
504
|
{ key: "name", label: "Name" },
|
|
502
505
|
{ key: "slug", label: "Slug" },
|
|
503
|
-
{ key: "description", label: "Description" },
|
|
506
|
+
{ key: "description", label: "Description", format: (value) => truncateText(value, SPACE_DESCRIPTION_COLUMN_WIDTH) },
|
|
504
507
|
{ key: "status", label: "Status" },
|
|
505
508
|
{ key: "createdAt", label: "Created" },
|
|
506
509
|
]);
|
package/dist/output.d.ts
CHANGED
|
@@ -24,6 +24,12 @@ export declare function formatLocalDateTime(value: unknown): string;
|
|
|
24
24
|
* Unix epoch for a timestamp the server never sent.
|
|
25
25
|
*/
|
|
26
26
|
export declare function formatEpochMs(value: unknown): string;
|
|
27
|
+
/**
|
|
28
|
+
* Collapse whitespace and clip long text for a single-line table cell, so
|
|
29
|
+
* multi-line values cannot break the row layout. `--json` output keeps the
|
|
30
|
+
* raw value; only the human-facing table is shortened.
|
|
31
|
+
*/
|
|
32
|
+
export declare function truncateText(value: unknown, maxLength: number): string;
|
|
27
33
|
export declare function table(rows: Row[], columns: Column[]): void;
|
|
28
34
|
export declare function json(data: unknown): void;
|
|
29
35
|
export declare function jsonRequested(opts?: {
|
package/dist/output.js
CHANGED
|
@@ -40,6 +40,17 @@ export function formatEpochMs(value) {
|
|
|
40
40
|
return "";
|
|
41
41
|
return formatLocalDateTime(ms);
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Collapse whitespace and clip long text for a single-line table cell, so
|
|
45
|
+
* multi-line values cannot break the row layout. `--json` output keeps the
|
|
46
|
+
* raw value; only the human-facing table is shortened.
|
|
47
|
+
*/
|
|
48
|
+
export function truncateText(value, maxLength) {
|
|
49
|
+
const text = typeof value === "string" ? value.replace(/\s+/g, " ").trim() : "";
|
|
50
|
+
if (text.length <= maxLength)
|
|
51
|
+
return text;
|
|
52
|
+
return `${text.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
|
|
53
|
+
}
|
|
43
54
|
function cellText(column, row) {
|
|
44
55
|
const raw = row[column.key];
|
|
45
56
|
if (column.format)
|
|
@@ -15,6 +15,10 @@ export type ArchiveTransport = {
|
|
|
15
15
|
signal?: AbortSignal;
|
|
16
16
|
}): Promise<RuntimeArchivePage>;
|
|
17
17
|
};
|
|
18
|
+
/** Raised when local capture bytes fail integrity checks (checksum, truncation, boundary); retryable via cache rebuild. */
|
|
19
|
+
export declare class ArchiveCaptureIntegrityError extends Error {
|
|
20
|
+
constructor(message: string);
|
|
21
|
+
}
|
|
18
22
|
export declare function checksumNativeFile(path: string): Promise<string>;
|
|
19
23
|
export declare function atomicRuntimeJson(path: string, value: unknown): Promise<void>;
|
|
20
24
|
/** Immutable local segments are the outbox. Model acknowledgements never remove them. */
|
|
@@ -4,6 +4,10 @@ import { mkdir, open, readFile, readdir, rename, rm, stat, link } from "node:fs/
|
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { RUNTIME_ARCHIVE_SEGMENT_BYTES, harnessArchiveIndexSchema, validateArchiveBoundary, } from "@neta-art/cohub";
|
|
6
6
|
const missing = (error) => error?.code === "ENOENT";
|
|
7
|
+
/** Raised when local capture bytes fail integrity checks (checksum, truncation, boundary); retryable via cache rebuild. */
|
|
8
|
+
export class ArchiveCaptureIntegrityError extends Error {
|
|
9
|
+
constructor(message) { super(message); this.name = "ArchiveCaptureIntegrityError"; }
|
|
10
|
+
}
|
|
7
11
|
const hash = (bytes, algorithm = "sha256") => createHash(algorithm).update(bytes).digest("hex");
|
|
8
12
|
export async function checksumNativeFile(path) {
|
|
9
13
|
const digest = createHash("sha256");
|
|
@@ -118,7 +122,7 @@ export class RuntimeArchiveStore {
|
|
|
118
122
|
if (saved.sessionId !== state.sessionId || saved.harness !== state.harness)
|
|
119
123
|
throw new Error("Archive identity mismatch");
|
|
120
124
|
if (state.expectedChecksum && state.expectedChecksum !== saved.sha256)
|
|
121
|
-
throw new
|
|
125
|
+
throw new ArchiveCaptureIntegrityError("Native Turn bytes changed; original archive retained");
|
|
122
126
|
const committed = await stat(join(this.root, "ready", `${turnId}.json`)).catch((error) => { if (missing(error))
|
|
123
127
|
return null; throw error; });
|
|
124
128
|
if (!committed)
|
|
@@ -133,10 +137,10 @@ export class RuntimeArchiveStore {
|
|
|
133
137
|
try {
|
|
134
138
|
const before = await file.stat();
|
|
135
139
|
if (!before.isFile() || !before.size)
|
|
136
|
-
throw new
|
|
140
|
+
throw new ArchiveCaptureIntegrityError("Native archive is empty");
|
|
137
141
|
const sizeBytes = state.sizeBytes ?? before.size;
|
|
138
142
|
if (!Number.isSafeInteger(sizeBytes) || sizeBytes < 1 || sizeBytes > before.size)
|
|
139
|
-
throw new
|
|
143
|
+
throw new ArchiveCaptureIntegrityError("Native archive boundary is unavailable");
|
|
140
144
|
const buffer = Buffer.alloc(RUNTIME_ARCHIVE_SEGMENT_BYTES);
|
|
141
145
|
let offset = 0;
|
|
142
146
|
let digest = createHash("sha256");
|
|
@@ -172,14 +176,14 @@ export class RuntimeArchiveStore {
|
|
|
172
176
|
}
|
|
173
177
|
const after = await stat(state.path);
|
|
174
178
|
if (before.ino !== after.ino || after.size < sizeBytes || state.sizeBytes === undefined && (before.size !== after.size || before.mtimeMs !== after.mtimeMs))
|
|
175
|
-
throw new
|
|
179
|
+
throw new ArchiveCaptureIntegrityError("Native file changed during capture");
|
|
176
180
|
if (state.sizeBytes !== undefined) {
|
|
177
181
|
// Native clients may append while an earlier Turn is captured. Validate the exact prefix twice.
|
|
178
182
|
const verified = createHash("sha256");
|
|
179
183
|
for await (const bytes of createReadStream(state.path, { end: sizeBytes - 1 }))
|
|
180
184
|
verified.update(bytes);
|
|
181
185
|
if (verified.digest("hex") !== digest.copy().digest("hex"))
|
|
182
|
-
throw new
|
|
186
|
+
throw new ArchiveCaptureIntegrityError("Native prefix changed during capture");
|
|
183
187
|
}
|
|
184
188
|
if (process.platform !== "win32") {
|
|
185
189
|
const directory = await open(join(this.root, "objects"), "r");
|
|
@@ -195,7 +199,7 @@ export class RuntimeArchiveStore {
|
|
|
195
199
|
sizeBytes, sha256: digest.digest("hex"), segments });
|
|
196
200
|
validateArchiveBoundary(index, parent);
|
|
197
201
|
if (state.expectedChecksum && index.sha256 !== state.expectedChecksum)
|
|
198
|
-
throw new
|
|
202
|
+
throw new ArchiveCaptureIntegrityError("Native Turn bytes changed during capture; original retained");
|
|
199
203
|
}
|
|
200
204
|
finally {
|
|
201
205
|
await file.close();
|
|
@@ -4,6 +4,4 @@ export type CodexTokenTotals = Record<typeof fields[number], number>;
|
|
|
4
4
|
export declare const codexTokenTotals: (value: unknown) => CodexTokenTotals;
|
|
5
5
|
export declare const subtractCodexTokens: (total: CodexTokenTotals, base: CodexTokenTotals) => CodexTokenTotals;
|
|
6
6
|
export declare function codexUsage(total: CodexTokenTotals): NonNullable<RuntimeMessage["usage"]>;
|
|
7
|
-
/** Seed portable imports from the original native counters, not a prior turn's `last`. */
|
|
8
|
-
export declare function codexArchiveTotals(records: Record<string, unknown>[]): CodexTokenTotals | undefined;
|
|
9
7
|
export {};
|
|
@@ -9,15 +9,3 @@ export const subtractCodexTokens = (total, base) => Object.fromEntries(fields.ma
|
|
|
9
9
|
export function codexUsage(total) {
|
|
10
10
|
return { input: Math.max(0, total.inputTokens - total.cachedInputTokens - total.cacheWriteInputTokens), output: total.outputTokens, cacheRead: total.cachedInputTokens, cacheWrite: total.cacheWriteInputTokens, totalTokens: total.totalTokens };
|
|
11
11
|
}
|
|
12
|
-
/** Seed portable imports from the original native counters, not a prior turn's `last`. */
|
|
13
|
-
export function codexArchiveTotals(records) {
|
|
14
|
-
for (let i = records.length - 1; i >= 0; i--) {
|
|
15
|
-
const entry = records[i];
|
|
16
|
-
if (entry?.type !== "token_usage_record")
|
|
17
|
-
continue;
|
|
18
|
-
const value = record(record(entry.payload).thread_token_usage);
|
|
19
|
-
if (typeof value.total_tokens !== "number")
|
|
20
|
-
continue;
|
|
21
|
-
return codexTokenTotals({ inputTokens: value.input_tokens, outputTokens: value.output_tokens, cachedInputTokens: value.cached_input_tokens, cacheWriteInputTokens: value.cache_write_input_tokens, totalTokens: value.total_tokens });
|
|
22
|
-
}
|
|
23
|
-
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { type RuntimeCapabilities, type NativeRuntimeEvent } from "@neta-art/cohub";
|
|
2
|
-
import { type
|
|
3
|
-
import { type RuntimeSessionStore } from "./session-store.js";
|
|
1
|
+
import { type RuntimeNativeStop, type RuntimeCapabilities, type NativeRuntimeEvent } from "@neta-art/cohub";
|
|
2
|
+
import { type Executor } from "./native/execution.js";
|
|
4
3
|
import { type RuntimeDiagnostics } from "./diagnostics.js";
|
|
5
4
|
export type RuntimeConnectionOptions = {
|
|
6
5
|
spaceId: string;
|
|
7
6
|
cwd: string;
|
|
8
7
|
url: string;
|
|
9
8
|
capabilities: RuntimeCapabilities;
|
|
10
|
-
harnesses: HarnessOptions;
|
|
11
9
|
token: (forceRefresh?: boolean) => Promise<string>;
|
|
12
10
|
signal: AbortSignal;
|
|
13
|
-
|
|
11
|
+
executor: Executor;
|
|
14
12
|
onReady: () => void;
|
|
15
13
|
onDisconnected?: () => void;
|
|
16
14
|
runtimeId?: string;
|
|
17
15
|
diagnostics?: RuntimeDiagnostics;
|
|
18
16
|
leaseConflictTimeoutMs?: number;
|
|
19
17
|
onNativeChannel?: (send: (event: NativeRuntimeEvent) => Promise<unknown>) => void;
|
|
18
|
+
/** The web asked to stop a Turn; one running in a native client is stopped there. */
|
|
19
|
+
onNativeStop?: (stop: RuntimeNativeStop) => void;
|
|
20
20
|
};
|
|
21
21
|
export declare function serveRuntime(options: RuntimeConnectionOptions): Promise<void>;
|