@neta-art/cohub-cli 8.2.0 → 8.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +11 -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 +38 -0
  21. package/dist/runtime/native/attach.js +113 -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 +113 -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
@@ -0,0 +1,249 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import WebSocket from "ws";
6
+ import { harnessEnvironment, record } from "../json-rpc.js";
7
+ import { atLeast, harnessVersion } from "./version.js";
8
+ /** First Codex release whose terminal client attaches to a shared app-server by default. */
9
+ export const CODEX_SHARED_MIN_VERSION = [0, 156, 0];
10
+ const REQUEST_TIMEOUT_MS = 60_000;
11
+ const JOIN_ATTEMPTS = 20;
12
+ const JOIN_RETRY_MS = 100;
13
+ /** `codex app-server daemon start` returns once the server listens; anything slower has failed. */
14
+ const DAEMON_START_TIMEOUT_MS = 30_000;
15
+ const codexHome = () => process.env.CODEX_HOME?.trim() || join(homedir(), ".codex");
16
+ /** The socket every Codex client on this machine discovers; a terminal `codex` attaches to it. */
17
+ const codexControlSocket = () => join(codexHome(), "app-server-control", "app-server-control.sock");
18
+ export const supportsSharedServer = (version) => atLeast(version, CODEX_SHARED_MIN_VERSION);
19
+ /** Start the shared app-server when none is running. */
20
+ async function ensureSharedServer(binary, cwd) {
21
+ if (existsSync(codexControlSocket()) && await probe())
22
+ return true;
23
+ const child = spawn(binary, ["app-server", "daemon", "start"], { cwd, env: harnessEnvironment(), stdio: ["ignore", "pipe", "ignore"], detached: true });
24
+ let output = "";
25
+ child.stdout.on("data", (bytes) => { output += bytes.toString("utf8"); });
26
+ // `exit`, not `close`: the server it forks may inherit stdout and keep the pipe open for good.
27
+ const timer = setTimeout(() => child.kill(), DAEMON_START_TIMEOUT_MS);
28
+ const ended = new Promise((resolve) => child.stdout.once("end", resolve));
29
+ const code = await new Promise((resolve) => { child.once("exit", resolve); child.once("error", () => resolve(null)); })
30
+ .finally(() => clearTimeout(timer));
31
+ // Its last output may still be buffered when it exits; a pipe that stays open is let go.
32
+ await Promise.race([ended, new Promise((resolve) => setTimeout(resolve, 1_000).unref())]);
33
+ child.stdout.destroy();
34
+ if (code !== 0)
35
+ return false;
36
+ try {
37
+ return ["started", "alreadyRunning", "already_running"].includes(String(JSON.parse(output.trim().split("\n").at(-1) ?? "{}").status)) && await probe();
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ }
43
+ async function probe() {
44
+ try {
45
+ const channel = await openShared();
46
+ await channel.close();
47
+ return true;
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ async function openShared() {
54
+ const socket = new WebSocket(`ws+unix://${codexControlSocket()}:/`);
55
+ await new Promise((resolve, reject) => {
56
+ socket.once("open", () => resolve());
57
+ socket.once("error", reject);
58
+ });
59
+ const channel = new SocketChannel(socket);
60
+ await initialize(channel);
61
+ return channel;
62
+ }
63
+ /** A private app-server over stdio, used when no shared one is available; it lives for one Turn. */
64
+ export async function openPrivateServer(rpc) {
65
+ const channel = new StdioChannel(rpc);
66
+ await initialize(channel);
67
+ return channel;
68
+ }
69
+ /** The Runtime's connection to the shared app-server. */
70
+ export class CodexShared {
71
+ binary;
72
+ cwd;
73
+ enabled;
74
+ channel = null;
75
+ opening = null;
76
+ ensured = false;
77
+ connectListeners = new Set();
78
+ driving = new Set();
79
+ constructor(binary, cwd, enabled) {
80
+ this.binary = binary;
81
+ this.cwd = cwd;
82
+ this.enabled = enabled;
83
+ }
84
+ async get() {
85
+ if (!this.enabled)
86
+ return null;
87
+ if (this.channel && !this.channel.closed)
88
+ return this.channel;
89
+ this.opening ??= (async () => {
90
+ if (!this.ensured) {
91
+ this.ensured = true;
92
+ if (!supportsSharedServer(await harnessVersion(this.binary)) || !await ensureSharedServer(this.binary, this.cwd))
93
+ return null;
94
+ }
95
+ const channel = await openShared();
96
+ this.channel = channel;
97
+ for (const listener of this.connectListeners)
98
+ listener(channel);
99
+ return channel;
100
+ })().catch(() => null).finally(() => { this.opening = null; });
101
+ return await this.opening;
102
+ }
103
+ onConnect(listener) {
104
+ this.connectListeners.add(listener);
105
+ return () => { this.connectListeners.delete(listener); };
106
+ }
107
+ async join(threadId) {
108
+ const channel = await this.get();
109
+ return channel ? await joinThread(channel, threadId) : null;
110
+ }
111
+ async interrupt(threadId, turnId) {
112
+ const channel = await this.get();
113
+ if (!channel)
114
+ return false;
115
+ await joinThread(channel, threadId);
116
+ await channel.request("turn/interrupt", { threadId, turnId });
117
+ return true;
118
+ }
119
+ async close() {
120
+ await this.channel?.close();
121
+ this.channel = null;
122
+ }
123
+ }
124
+ async function initialize(channel) {
125
+ await channel.request("initialize", { clientInfo: { name: "cohub-runtime", title: "Cohub", version: "1" }, capabilities: { experimentalApi: true } });
126
+ // The notification completes the handshake; responses are not expected.
127
+ channel.notify("initialized");
128
+ }
129
+ class SocketChannel {
130
+ socket;
131
+ shared = true;
132
+ pending = new Map();
133
+ listeners = new Set();
134
+ closeListeners = new Set();
135
+ counter = 0;
136
+ closedError = null;
137
+ constructor(socket) {
138
+ this.socket = socket;
139
+ socket.on("message", (data) => {
140
+ let message;
141
+ try {
142
+ message = record(JSON.parse(String(data)));
143
+ }
144
+ catch {
145
+ return;
146
+ }
147
+ const id = message.id;
148
+ if (id != null && ("result" in message || "error" in message)) {
149
+ const pending = this.pending.get(String(id));
150
+ if (!pending)
151
+ return;
152
+ this.pending.delete(String(id));
153
+ clearTimeout(pending.timer);
154
+ if (message.error)
155
+ pending.reject(new Error(String(record(message.error).message ?? "Codex request failed")));
156
+ else
157
+ pending.resolve(record(message.result));
158
+ return;
159
+ }
160
+ if (typeof message.method !== "string")
161
+ return;
162
+ const event = { method: message.method, params: record(message.params), ...(id != null ? { id: id } : {}) };
163
+ for (const listener of this.listeners)
164
+ listener(event);
165
+ });
166
+ socket.on("close", () => this.fail(new Error("Codex app-server disconnected")));
167
+ socket.on("error", (error) => this.fail(error instanceof Error ? error : new Error(String(error))));
168
+ }
169
+ get closed() { return this.closedError !== null; }
170
+ fail(error) {
171
+ if (this.closedError)
172
+ return;
173
+ this.closedError = error;
174
+ for (const pending of this.pending.values()) {
175
+ clearTimeout(pending.timer);
176
+ pending.reject(error);
177
+ }
178
+ this.pending.clear();
179
+ for (const listener of this.closeListeners)
180
+ listener(error);
181
+ this.closeListeners.clear();
182
+ }
183
+ request(method, params = {}, timeoutMs = REQUEST_TIMEOUT_MS) {
184
+ if (this.closedError)
185
+ return Promise.reject(this.closedError);
186
+ const id = `cohub-${++this.counter}`;
187
+ return new Promise((resolve, reject) => {
188
+ const timer = setTimeout(() => { this.pending.delete(id); reject(new Error(`Codex ${method} timed out`)); }, timeoutMs);
189
+ this.pending.set(id, { resolve, reject, timer });
190
+ this.socket.send(JSON.stringify({ id, method, params }));
191
+ });
192
+ }
193
+ notify(method, params = {}) { this.socket.send(JSON.stringify({ method, params })); }
194
+ refuse(id, message) { if (!this.closedError)
195
+ this.socket.send(JSON.stringify({ id, error: { code: -32000, message } })); }
196
+ onEvent(listener) { this.listeners.add(listener); return () => { this.listeners.delete(listener); }; }
197
+ onClose(listener) {
198
+ if (this.closedError) {
199
+ const error = this.closedError;
200
+ queueMicrotask(() => listener(error));
201
+ return () => undefined;
202
+ }
203
+ this.closeListeners.add(listener);
204
+ return () => { this.closeListeners.delete(listener); };
205
+ }
206
+ async close() { this.fail(new Error("Codex app-server connection closed")); this.socket.close(); }
207
+ }
208
+ class StdioChannel {
209
+ rpc;
210
+ shared = false;
211
+ failed = false;
212
+ constructor(rpc) {
213
+ this.rpc = rpc;
214
+ rpc.onFailure(() => { this.failed = true; });
215
+ }
216
+ get closed() { return this.failed; }
217
+ request(method, params = {}, timeoutMs) { return this.rpc.request(method, params, timeoutMs); }
218
+ notify(method, params = {}) { this.rpc.write({ method, params }); }
219
+ refuse(id, message) { try {
220
+ this.rpc.write({ id, error: { code: -32000, message } });
221
+ }
222
+ catch { /* The process is gone. */ } }
223
+ onClose(listener) { return this.rpc.onFailure(listener); }
224
+ onEvent(listener) {
225
+ return this.rpc.onEvent((event) => {
226
+ if (typeof event.method !== "string")
227
+ return;
228
+ listener({ method: event.method, params: record(event.params), ...(event.id != null ? { id: event.id } : {}) });
229
+ });
230
+ }
231
+ async close() { await this.rpc.close(); }
232
+ }
233
+ /**
234
+ * Join a thread that another client runs. A thread's rollout is written when its first Turn starts,
235
+ * so a join that arrives a moment early is retried briefly instead of failing the observation.
236
+ */
237
+ async function joinThread(channel, threadId) {
238
+ let lastError;
239
+ for (let attempt = 0; attempt < JOIN_ATTEMPTS; attempt += 1) {
240
+ try {
241
+ return await channel.request("thread/resume", { threadId });
242
+ }
243
+ catch (error) {
244
+ lastError = error;
245
+ await new Promise((resolve) => setTimeout(resolve, JOIN_RETRY_MS));
246
+ }
247
+ }
248
+ throw lastError instanceof Error ? lastError : new Error("Cannot join the Codex thread");
249
+ }
@@ -0,0 +1,16 @@
1
+ import type { Harness } from "./adapters.js";
2
+ /** Private state of one Space's Runtime on this machine. */
3
+ export declare const runtimeStateRoot: (spaceId: string, base?: string) => string;
4
+ /** What the user agreed to for native sessions in the bound directory. */
5
+ export type NativeConfig = {
6
+ version: 2;
7
+ identity: string;
8
+ spaceId: string;
9
+ root: string;
10
+ harnesses: Harness[];
11
+ enabledAt: string;
12
+ codexShared: boolean;
13
+ };
14
+ export declare const nativeConfigPath: (stateRoot: string) => string;
15
+ export declare function readNativeConfig(stateRoot: string, identity: string): Promise<NativeConfig | null>;
16
+ export declare function writeNativeConfig(stateRoot: string, config: NativeConfig): Promise<void>;
@@ -0,0 +1,24 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { atomicRuntimeJson } from "../archive-store.js";
5
+ /** Private state of one Space's Runtime on this machine. */
6
+ export const runtimeStateRoot = (spaceId, base = join(homedir(), ".local", "state", "cohub", "runtime")) => join(base, spaceId);
7
+ export const nativeConfigPath = (stateRoot) => join(stateRoot, "native", "config.json");
8
+ export async function readNativeConfig(stateRoot, identity) {
9
+ try {
10
+ const config = JSON.parse(await readFile(nativeConfigPath(stateRoot), "utf8"));
11
+ if (config.version !== 2 || !Array.isArray(config.harnesses) || typeof config.enabledAt !== "string")
12
+ throw new Error("Invalid native sync configuration");
13
+ // Another account's consent never applies.
14
+ return config.identity === identity ? config : null;
15
+ }
16
+ catch (error) {
17
+ if (error.code === "ENOENT")
18
+ return null;
19
+ throw error;
20
+ }
21
+ }
22
+ export async function writeNativeConfig(stateRoot, config) {
23
+ await atomicRuntimeJson(nativeConfigPath(stateRoot), config);
24
+ }
@@ -0,0 +1,88 @@
1
+ import type { NativeRuntimeEvent } from "@neta-art/cohub";
2
+ import type { RuntimeArchiveStore } from "../archive-store.js";
3
+ import { type RuntimeDiagnostics } from "../diagnostics.js";
4
+ import type { HarnessOptions } from "../harness.js";
5
+ import type { SessionTurnProjectionClient } from "../turn-projection.js";
6
+ import { type Harness } from "./adapters.js";
7
+ import { type NativeConfig } from "./config.js";
8
+ import type { Executor } from "./execution.js";
9
+ import { NativeIngest, type ImportJob } from "./ingest.js";
10
+ import { type PiExtensionState } from "./install.js";
11
+ export type NativeStatus = {
12
+ synced: Harness[];
13
+ transcripts: number;
14
+ running: number;
15
+ pi: {
16
+ extension: PiExtensionState;
17
+ connected: number;
18
+ unavailable: string | null;
19
+ } | null;
20
+ codex: {
21
+ control: "shared" | "private";
22
+ watching: number;
23
+ } | null;
24
+ import: ImportJob & {
25
+ queued: number;
26
+ running: number;
27
+ };
28
+ };
29
+ /**
30
+ * Everything the Runtime does with native sessions: the ingest engine (data plane), the Pi and
31
+ * Codex links (control plane), and the executor that runs Cohub Turns through them.
32
+ */
33
+ export declare class NativeRuntime {
34
+ private readonly options;
35
+ readonly ingest: NativeIngest;
36
+ readonly executor: Executor;
37
+ private readonly pi;
38
+ private readonly codex;
39
+ private readonly streams;
40
+ private readonly filePreviews;
41
+ private readonly watched;
42
+ private readonly active;
43
+ private send;
44
+ private extension;
45
+ constructor(options: {
46
+ spaceId: string;
47
+ root: string;
48
+ stateRoot: string;
49
+ harnesses: Harness[];
50
+ executables: HarnessOptions;
51
+ identity: string;
52
+ config: NativeConfig | null;
53
+ archives: RuntimeArchiveStore;
54
+ projectionSource: SessionTurnProjectionClient;
55
+ diagnostics: RuntimeDiagnostics;
56
+ });
57
+ private get synced();
58
+ start(signal: AbortSignal): Promise<void>;
59
+ connect(send: ((event: NativeRuntimeEvent) => Promise<unknown>) | null): void;
60
+ stop(sessionId: string, turnId: string): boolean;
61
+ status(): NativeStatus;
62
+ control(action: string, message: Record<string, unknown>): Promise<unknown>;
63
+ private reload;
64
+ private refreshScope;
65
+ close(signal: AbortSignal): Promise<void>;
66
+ private sink;
67
+ /**
68
+ * A connected Pi. A user prompt starting and the agent settling are the two moments its file has
69
+ * something to persist; everything between streams as a preview of a Turn someone runs there.
70
+ */
71
+ private onPiEvent;
72
+ private stream;
73
+ private endStream;
74
+ private endFilePreview;
75
+ private observeCodex;
76
+ private watchCodexThread;
77
+ private onCodexEvent;
78
+ /**
79
+ * A terminal Pi runs many Turns in one process; once the ledger knows which one is running, its
80
+ * tools are told, so a `cohub` call from a tool names the right Space, Session and Turn.
81
+ */
82
+ private onRunning;
83
+ /**
84
+ * A running Turn's file grew. A connected client streams it already; otherwise the preview is
85
+ * read from the file, at a pace set by a timer, not by every write.
86
+ */
87
+ private onGrowth;
88
+ }