@neta-art/cohub-cli 8.2.0 → 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.
Files changed (76) hide show
  1. package/dist/commands/runtime.d.ts +0 -12
  2. package/dist/commands/runtime.js +129 -144
  3. package/dist/commands/search.js +3 -9
  4. package/dist/commands/spaces.js +5 -2
  5. package/dist/output.d.ts +6 -0
  6. package/dist/output.js +11 -0
  7. package/dist/runtime/codex-usage.d.ts +0 -2
  8. package/dist/runtime/codex-usage.js +0 -12
  9. package/dist/runtime/connection.d.ts +5 -5
  10. package/dist/runtime/connection.js +27 -16
  11. package/dist/runtime/harness.d.ts +8 -16
  12. package/dist/runtime/harness.js +1 -366
  13. package/dist/runtime/instance.d.ts +8 -1
  14. package/dist/runtime/instance.js +15 -8
  15. package/dist/runtime/launch.js +10 -5
  16. package/dist/runtime/native/adapters.d.ts +32 -0
  17. package/dist/runtime/native/adapters.js +79 -0
  18. package/dist/runtime/{native-archive.d.ts → native/archive.d.ts} +0 -3
  19. package/dist/runtime/{native-archive.js → native/archive.js} +2 -17
  20. package/dist/runtime/native/attach.d.ts +33 -0
  21. package/dist/runtime/native/attach.js +103 -0
  22. package/dist/runtime/native/codex-link.d.ts +40 -0
  23. package/dist/runtime/native/codex-link.js +249 -0
  24. package/dist/runtime/native/config.d.ts +16 -0
  25. package/dist/runtime/native/config.js +24 -0
  26. package/dist/runtime/native/daemon.d.ts +88 -0
  27. package/dist/runtime/native/daemon.js +367 -0
  28. package/dist/runtime/native/execution.d.ts +45 -0
  29. package/dist/runtime/native/execution.js +337 -0
  30. package/dist/runtime/native/identity.d.ts +9 -0
  31. package/dist/runtime/native/identity.js +15 -0
  32. package/dist/runtime/native/ingest.d.ts +241 -0
  33. package/dist/runtime/native/ingest.js +823 -0
  34. package/dist/runtime/native/install.d.ts +9 -0
  35. package/dist/runtime/native/install.js +105 -0
  36. package/dist/runtime/native/pi-extension.d.ts +35 -0
  37. package/dist/runtime/native/pi-extension.js +160 -0
  38. package/dist/runtime/native/pi-link.d.ts +67 -0
  39. package/dist/runtime/native/pi-link.js +238 -0
  40. package/dist/runtime/native/results.d.ts +41 -0
  41. package/dist/runtime/native/results.js +128 -0
  42. package/dist/runtime/native/sessions.d.ts +45 -0
  43. package/dist/runtime/native/sessions.js +92 -0
  44. package/dist/runtime/{native-transcript.d.ts → native/transcript.d.ts} +5 -5
  45. package/dist/runtime/{native-transcript.js → native/transcript.js} +37 -11
  46. package/dist/runtime/native/translate.d.ts +44 -0
  47. package/dist/runtime/native/translate.js +263 -0
  48. package/dist/runtime/native/transport.d.ts +7 -0
  49. package/dist/runtime/native/transport.js +16 -0
  50. package/dist/runtime/native/version.d.ts +5 -0
  51. package/dist/runtime/native/version.js +18 -0
  52. package/dist/runtime/presentation.d.ts +7 -10
  53. package/dist/runtime/presentation.js +35 -10
  54. package/dist/runtime/projection-store.d.ts +6 -13
  55. package/dist/runtime/projection-store.js +11 -74
  56. package/dist/runtime/supervisor.d.ts +2 -0
  57. package/dist/runtime/supervisor.js +36 -110
  58. package/dist/runtime/turn-projection.d.ts +1 -3
  59. package/dist/runtime/turn-projection.js +3 -10
  60. package/package.json +4 -5
  61. package/dist/runtime/native-attach.d.ts +0 -28
  62. package/dist/runtime/native-attach.js +0 -47
  63. package/dist/runtime/native-codex-hook.d.ts +0 -1
  64. package/dist/runtime/native-codex-hook.js +0 -28
  65. package/dist/runtime/native-install.d.ts +0 -21
  66. package/dist/runtime/native-install.js +0 -130
  67. package/dist/runtime/native-ipc.d.ts +0 -37
  68. package/dist/runtime/native-ipc.js +0 -118
  69. package/dist/runtime/native-pi-extension.d.ts +0 -20
  70. package/dist/runtime/native-pi-extension.js +0 -47
  71. package/dist/runtime/native-sync-store.d.ts +0 -116
  72. package/dist/runtime/native-sync-store.js +0 -495
  73. package/dist/runtime/native-sync.d.ts +0 -65
  74. package/dist/runtime/native-sync.js +0 -326
  75. package/dist/runtime/session-store.d.ts +0 -84
  76. package/dist/runtime/session-store.js +0 -601
@@ -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;
@@ -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 { installNativeSync } from "../runtime/native-install.js";
8
- import { discoverNativeImportCandidates, nativeRuntimeRoot, readNativeSyncConfig } from "../runtime/native-sync.js";
9
- import { listNativeSyncStores } from "../runtime/native-sync-store.js";
10
- import { requestNativeDaemon } from "../runtime/native-ipc.js";
11
- import { requestRuntimeInstance, runtimeInstanceDirectory } from "../runtime/instance.js";
12
- import { atLeastLevel, diagnosticLevels, formatDiagnostic, formatNativeSync, printRuntimeSummary } from "../runtime/presentation.js";
13
- import { RuntimeSessionStore } from "../runtime/session-store.js";
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
- export function applyNativeImportResponse(result, path, response) {
21
- if (!response.ok) {
22
- if (response.skipped) {
23
- result.skipped.push({ path, message: response.message });
24
- return;
25
- }
26
- throw new Error(response.message);
27
- }
28
- result.imported += 1;
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("detach")
53
- .description("Pause native sync; retain all receipts")
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 or Codex; repeatable", (value, previous) => [...previous, value], [])
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 spaceId = await resolveRuntimeTarget(program, options.space);
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 (!identity)
62
- throw new Error("Sign in first");
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({ ...result, enabled: false });
74
+ outJson({ harness: "pi", extension: "installed", updated: before !== "installed" });
71
75
  else
72
- process.stdout.write("Native sync paused; all local records retained\n");
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,86 +122,46 @@ 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 identity = currentIdentityKey();
93
- if (!identity)
94
- throw new Error("Sign in first");
95
- const root = await canonicalRuntimeRoot(dir ?? process.cwd());
96
- const spaceId = await resolveRuntimeTarget(program, options.space);
97
- const binding = await getRuntimeSpaceBinding(root, identity);
98
- if (!binding || binding.spaceId !== spaceId)
99
- throw new Error("Bind this directory with runtime up first");
100
- const local = await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId));
101
- if (!local)
102
- throw new Error("Local Runtime is not running");
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 superseded = discovered.superseded.filter((skip) => !options.session || skip.nativeSessionId === options.session);
119
- const result = {
120
- spaceId, root, harnesses, candidates, errors: discovered.errors,
121
- imported: 0, pendingTurns: 0, failed: [], skipped: [], superseded, dryRun: Boolean(options.dryRun),
122
- complete: discovered.errors.length === 0 && superseded.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;
123
138
  };
124
- if (jsonRequested(options) && options.dryRun) {
125
- result.complete = result.complete && result.failed.length === 0;
126
- outJson(result);
127
- if (!result.complete)
128
- process.exitCode = 1;
129
- return;
130
- }
131
- if (!candidates.length) {
132
- if (!result.complete)
133
- process.exitCode = 1;
134
- if (jsonRequested(options))
135
- 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");
136
146
  else {
137
- process.stdout.write("No existing native conversations found\n");
138
- for (const error of discovered.errors)
139
- process.stdout.write(`Skipped ${error.path}: ${error.message}\n`);
140
- for (const skip of superseded)
141
- process.stdout.write(`Superseded ${skip.path}: ${skip.message}\n`);
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`);
142
152
  }
143
153
  return;
144
154
  }
145
- if (options.dryRun) {
146
- if (jsonRequested(options))
147
- outJson(result);
148
- else {
149
- process.stdout.write(`Found ${candidates.length} native conversation${candidates.length === 1 ? "" : "s"}\n`);
150
- for (const candidate of candidates)
151
- process.stdout.write(` ${candidate.harness} ${candidate.nativeSessionId} ${candidate.turnCount} Turn${candidate.turnCount === 1 ? "" : "s"} ${candidate.path}\n`);
152
- for (const error of discovered.errors)
153
- process.stdout.write(`Skipped ${error.path}: ${error.message}\n`);
154
- for (const skip of superseded)
155
- process.stdout.write(`Superseded ${skip.path}: ${skip.message}\n`);
156
- }
157
- if (!result.complete)
158
- process.exitCode = 1;
159
- return;
160
- }
161
155
  if (!options.yes) {
162
156
  if (!process.stdin.isTTY)
163
157
  throw new Error("Use --yes in non-interactive mode");
164
158
  const { createInterface } = await import("node:readline/promises");
165
159
  const rl = createInterface({ input: process.stdin, output: process.stderr });
166
160
  try {
167
- const answer = await rl.question(`Import ${candidates.length} native conversation${candidates.length === 1 ? "" : "s"} to Space ${spaceId}? [Y/n] `);
161
+ const answer = await rl.question(`Import ${plan.files} conversation${plan.files === 1 ? "" : "s"} (${megabytes(plan.bytes)}) to Space ${spaceId}? [Y/n] `);
168
162
  if (!/^(|y(es)?)$/i.test(answer.trim())) {
169
- if (jsonRequested(options))
170
- outJson({ ...result, cancelled: true });
163
+ if (asJson)
164
+ outJson({ ...summary, cancelled: true });
171
165
  else
172
166
  process.stdout.write("Import cancelled\n");
173
167
  return;
@@ -177,43 +171,31 @@ export function registerRuntime(program) {
177
171
  rl.close();
178
172
  }
179
173
  }
180
- let processed = 0;
181
- for (const candidate of candidates) {
182
- if (controller.signal.aborted)
183
- break;
184
- try {
185
- const response = await requestNativeDaemon({ harness: candidate.harness, cwd: root, path: candidate.path, nativeSessionId: candidate.nativeSessionId,
186
- sessionStartedAt: candidate.sessionStartedAt, origin: "local_import", settled: true });
187
- applyNativeImportResponse(result, candidate.path, response);
188
- if (response.ok)
189
- result.pendingTurns += response.pendingTurns;
190
- }
191
- catch (error) {
192
- if (!controller.signal.aborted)
193
- result.failed.push({ path: candidate.path, message: error instanceof Error ? error.message : String(error) });
194
- }
195
- finally {
196
- processed += 1;
197
- if (!jsonRequested(options) && !controller.signal.aborted) {
198
- process.stderr.write(`\rSubmitted ${result.imported}/${candidates.length} · pending ${result.pendingTurns} Turns`);
199
- }
200
- }
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;
201
183
  }
202
- const cancelled = controller.signal.aborted;
203
- result.complete = result.complete && result.failed.length === 0 && !cancelled;
204
- if (jsonRequested(options))
205
- outJson({ ...result, cancelled, processed });
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 });
206
189
  else {
207
- if (processed)
208
- process.stderr.write("\n");
209
- process.stdout.write(`Submitted ${result.imported}/${candidates.length} native conversation${candidates.length === 1 ? "" : "s"}${cancelled ? " (interrupted)" : ""}\n`);
210
- for (const error of [...discovered.errors, ...result.skipped, ...result.failed])
211
- process.stdout.write(`Skipped ${error.path}: ${error.message}\n`);
212
- for (const skip of result.superseded)
213
- process.stdout.write(`Superseded ${skip.path}: ${skip.message}\n`);
214
- process.stdout.write("Uploads continue in the local Runtime background; use runtime status to check confirmation\n");
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");
215
197
  }
216
- if (!result.complete && !cancelled)
198
+ if (job.failed.length)
217
199
  process.exitCode = 1;
218
200
  }
219
201
  catch (cause) {
@@ -233,16 +215,15 @@ export function registerRuntime(program) {
233
215
  const identity = currentIdentityKey();
234
216
  const local = identity ? await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId)) : null;
235
217
  const space = createClient().space(spaceId);
236
- const store = new RuntimeSessionStore(spaceId, { projectionSource: space, archiveTransport: null });
237
- const [remote, pendingLocalArchives, failedLocalArchives, nativeStores] = await Promise.all([
218
+ const stateRoot = runtimeStateRoot(spaceId);
219
+ const archives = new RuntimeArchiveStore(join(stateRoot, "archives"));
220
+ const [remote, pendingLocalArchives, failedLocalArchives] = await Promise.all([
238
221
  space.getRuntime(undefined, { signal: AbortSignal.timeout(5000) }).then((value) => ({ value, error: null })).catch((error) => ({ value: null, error: serializeDiagnosticError(error).message })),
239
- store.archives.pendingCount(), store.archives.failedCaptureCount(),
240
- identity ? listNativeSyncStores(store.root, spaceId, identity) : [],
222
+ archives.pendingCount(), archives.failedCaptureCount(),
241
223
  ]);
242
- const nativeSessions = await Promise.all(nativeStores.map((native) => native.status()));
243
224
  // A corrupt native sync config must never take down the whole status report.
244
- const nativeSync = identity ? await readNativeSyncConfig(store.root, identity).then((config) => ({ config, error: null }), (error) => ({ config: null, error: serializeDiagnosticError(error).message })) : { config: null, error: null };
245
- const result = { ...remote.value, spaceId, local, remote: remote.value, remoteError: remote.error, diagnosticsPath: runtimeDiagnosticsDirectory(store.root), pendingLocalArchives, failedLocalArchives, nativeSync: nativeSync.config, nativeSyncError: nativeSync.error, nativeSessions };
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 };
246
227
  if (jsonRequested(options))
247
228
  outJson(result);
248
229
  else {
@@ -251,7 +232,7 @@ export function registerRuntime(program) {
251
232
  else
252
233
  process.stdout.write(`Local process Not running\nSpace ${spaceId}\nLogs ${result.diagnosticsPath}\n`);
253
234
  process.stdout.write(`Server ${remote.error ? `Unknown — ${remote.error}` : remote.value?.online ? "Harness connected" : "Offline"}\nArchives ${pendingLocalArchives} pending · ${failedLocalArchives} failed\n`);
254
- process.stdout.write(formatNativeSync(nativeSync.config, nativeSessions, nativeSync.error));
235
+ process.stdout.write(formatNativeSync(nativeSync.config, local?.native, nativeSync.error));
255
236
  }
256
237
  }
257
238
  catch (cause) {
@@ -285,8 +266,12 @@ export function registerRuntime(program) {
285
266
  throw new Error("Runtime is still stopping; inspect logs");
286
267
  if (jsonRequested(options))
287
268
  outJson({ spaceId, stopped: true });
288
- else
269
+ else {
289
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
+ }
290
275
  }
291
276
  catch (cause) {
292
277
  reportFailure(cause);
@@ -303,18 +288,18 @@ export function registerRuntime(program) {
303
288
  const stop = () => controller.abort();
304
289
  try {
305
290
  const spaceId = await resolveRuntimeTarget(program, options.space);
306
- const store = new RuntimeSessionStore(spaceId, { projectionSource: createClient().space(spaceId), archiveTransport: null });
291
+ const stateRoot = runtimeStateRoot(spaceId);
307
292
  const limit = Number(options.limit);
308
293
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 10_000)
309
294
  throw new Error("Use a limit from 1 to 10000");
310
295
  if (!diagnosticLevels.includes(options.level))
311
296
  throw new Error("Use debug, info, warn or error");
312
297
  const asJson = jsonRequested(options);
313
- const reader = new RuntimeDiagnosticReader(store.root);
298
+ const reader = new RuntimeDiagnosticReader(stateRoot);
314
299
  process.once("SIGINT", stop);
315
300
  process.once("SIGTERM", stop);
316
301
  do {
317
- const events = (options.follow ? await reader.read({ limit }) : await readRuntimeDiagnosticEvents(store.root, { limit }))
302
+ const events = (options.follow ? await reader.read({ limit }) : await readRuntimeDiagnosticEvents(stateRoot, { limit }))
318
303
  .filter((event) => atLeastLevel(event.level, options.level));
319
304
  if (asJson && !options.follow)
320
305
  outJson(events);
@@ -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: truncate(item.title, MAX_TITLE_LENGTH),
51
- context: truncate(contextFor(item), MAX_CONTEXT_LENGTH),
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,
@@ -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)
@@ -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 HarnessOptions } from "./harness.js";
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
- store: RuntimeSessionStore;
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>;