@neta-art/cohub-cli 6.12.0 → 7.0.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.
@@ -0,0 +1,88 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+ import { link, mkdir, open, rm } from "node:fs/promises";
4
+ import { dirname } from "node:path";
5
+ import { RUNTIME_MAX_FRAME_BYTES } from "@neta-art/cohub";
6
+ import { JsonLineDecoder } from "./json-rpc.js";
7
+ import { codexArchiveTotals } from "./codex-usage.js";
8
+ export async function readCodexArchiveTotals(path) {
9
+ let totals;
10
+ const decoder = new JsonLineDecoder((row) => { totals = codexArchiveTotals([row]) ?? totals; });
11
+ for await (const bytes of createReadStream(path))
12
+ decoder.push(bytes);
13
+ decoder.end();
14
+ return totals;
15
+ }
16
+ /** Change only the header of a working copy. Raw archive bytes remain untouched. */
17
+ export async function importNativeArchive(input) {
18
+ await mkdir(dirname(input.target), { recursive: true, mode: 0o700 });
19
+ const temporary = `${input.target}.${randomUUID()}.importing`;
20
+ const file = await open(temporary, "wx", 0o600);
21
+ const digest = createHash("sha256");
22
+ let headerReady = false, headerBytes = 0;
23
+ const fragments = [];
24
+ let totals;
25
+ const decoder = input.harness === "codex" ? new JsonLineDecoder((row) => { totals = codexArchiveTotals([row]) ?? totals; }) : null;
26
+ const write = async (bytes) => { digest.update(bytes); await file.writeFile(bytes); };
27
+ const writeHeader = async () => {
28
+ const header = JSON.parse(Buffer.concat(fragments).toString("utf8"));
29
+ if (input.harness === "pi") {
30
+ if (header?.type !== "session" || header.id !== input.nativeSessionId)
31
+ throw new Error("Pi archive identity mismatch");
32
+ header.cwd = input.cwd;
33
+ delete header.parentSession;
34
+ }
35
+ else {
36
+ if (header?.type !== "session_meta" || header.payload?.id !== input.nativeSessionId)
37
+ throw new Error("Codex archive identity mismatch");
38
+ header.payload.id = input.id;
39
+ if (header.payload.session_id != null)
40
+ header.payload.session_id = input.id;
41
+ header.payload.history_mode = "legacy";
42
+ header.payload.cwd = input.cwd;
43
+ }
44
+ await write(Buffer.from(`${JSON.stringify(header)}\n`));
45
+ fragments.length = 0;
46
+ headerReady = true;
47
+ };
48
+ try {
49
+ for await (const bytes of createReadStream(input.source, { signal: input.signal })) {
50
+ decoder?.push(bytes);
51
+ if (headerReady) {
52
+ await write(bytes);
53
+ continue;
54
+ }
55
+ const newline = bytes.indexOf(10);
56
+ const prefix = newline < 0 ? bytes : bytes.subarray(0, newline);
57
+ headerBytes += prefix.length;
58
+ if (headerBytes > RUNTIME_MAX_FRAME_BYTES)
59
+ throw new Error("Native header is too large");
60
+ fragments.push(prefix);
61
+ if (newline >= 0) {
62
+ await writeHeader();
63
+ await write(bytes.subarray(newline + 1));
64
+ }
65
+ }
66
+ if (!headerReady)
67
+ await writeHeader();
68
+ decoder?.end();
69
+ input.signal?.throwIfAborted();
70
+ await file.sync();
71
+ await file.close();
72
+ await link(temporary, input.target);
73
+ if (process.platform !== "win32") {
74
+ const directory = await open(dirname(input.target), "r");
75
+ try {
76
+ await directory.sync();
77
+ }
78
+ finally {
79
+ await directory.close();
80
+ }
81
+ }
82
+ return { checksum: digest.digest("hex"), nativeSessionId: input.harness === "pi" ? input.nativeSessionId : input.id, codexTokenTotals: totals };
83
+ }
84
+ finally {
85
+ await file.close();
86
+ await rm(temporary, { force: true });
87
+ }
88
+ }
@@ -0,0 +1,3 @@
1
+ export declare class ProcessCleanupUncertainError extends Error {
2
+ }
3
+ export declare function stopProcessGroup(pid: number): Promise<void>;
@@ -0,0 +1,68 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { setTimeout as delay } from "node:timers/promises";
3
+ import { spawn } from "node:child_process";
4
+ export class ProcessCleanupUncertainError extends Error {
5
+ }
6
+ async function groupAlive(pid) {
7
+ try {
8
+ process.kill(-pid, 0);
9
+ }
10
+ catch (error) {
11
+ if (error.code === "ESRCH")
12
+ return false;
13
+ throw error;
14
+ }
15
+ if (process.platform !== "linux")
16
+ return true;
17
+ // Linux can retain reparented zombies: they cannot execute, but kill(0) still sees them.
18
+ for (const entry of await readdir("/proc")) {
19
+ if (!/^\d+$/.test(entry))
20
+ continue;
21
+ try {
22
+ const stat = await readFile(`/proc/${entry}/stat`, "utf8");
23
+ // After removing `pid (comm)`: state, ppid, pgrp, session, ...
24
+ const [state, _parentPid, processGroupId] = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
25
+ if (Number(processGroupId) === pid && state !== "Z" && state !== "X")
26
+ return true;
27
+ }
28
+ catch (error) {
29
+ if (error.code !== "ENOENT" && error.code !== "ESRCH")
30
+ throw error;
31
+ }
32
+ }
33
+ return false;
34
+ }
35
+ export async function stopProcessGroup(pid) {
36
+ try {
37
+ if (process.platform === "win32") {
38
+ await new Promise((resolve, reject) => {
39
+ const task = spawn("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore", timeout: 5000 });
40
+ task.once("error", reject);
41
+ task.once("exit", (code) => code === 0 ? resolve() : reject(new Error(`taskkill exited ${code}`)));
42
+ });
43
+ return;
44
+ }
45
+ const signal = (value) => {
46
+ try {
47
+ process.kill(-pid, value);
48
+ }
49
+ catch (error) {
50
+ if (error.code !== "ESRCH")
51
+ throw error;
52
+ }
53
+ };
54
+ signal("SIGTERM");
55
+ const started = Date.now();
56
+ while (await groupAlive(pid)) {
57
+ const elapsed = Date.now() - started;
58
+ if (elapsed >= 5000)
59
+ throw new Error("Process group is still running");
60
+ if (elapsed >= 2000)
61
+ signal("SIGKILL");
62
+ await delay(25);
63
+ }
64
+ }
65
+ catch (cause) {
66
+ throw new ProcessCleanupUncertainError("Tool process cleanup could not be confirmed; execution remains unresolved", { cause });
67
+ }
68
+ }
@@ -0,0 +1,47 @@
1
+ import { type RuntimeExecutionEvent, type HarnessArchive, type RuntimePendingExecution, type RuntimeTurnInput } from "@neta-art/cohub";
2
+ import { RuntimeArchiveStore, type ArchiveTransport } from "./archive-store.js";
3
+ import type { CodexTokenTotals } from "./codex-usage.js";
4
+ export declare class ContextRequiredError extends Error {
5
+ readonly historyOnly: boolean;
6
+ constructor(message: string, historyOnly?: boolean);
7
+ }
8
+ export type NativeSession = {
9
+ version: 1;
10
+ sessionId: string;
11
+ harness: "pi" | "codex";
12
+ nativeSessionId: string;
13
+ path: string;
14
+ cwd?: string;
15
+ throughTurnId: string | null;
16
+ revision: string;
17
+ checksum: string;
18
+ pendingTurnId: string | null;
19
+ resultChecksum?: string;
20
+ archivePendingTurnId?: string;
21
+ codexTokenTotals?: CodexTokenTotals;
22
+ };
23
+ /** Local references are hints validated against actual native files, never cloud existence claims. */
24
+ export declare class RuntimeSessionStore {
25
+ readonly root: string;
26
+ readonly archives: RuntimeArchiveStore;
27
+ private archiveFlush;
28
+ constructor(spaceId: string, stateRoot?: string, transport?: ArchiveTransport);
29
+ private statePath;
30
+ pendingExecutionBatches(): AsyncGenerator<RuntimePendingExecution[]>;
31
+ flushArchives(signal: AbortSignal): Promise<void>;
32
+ private flushArchiveOutbox;
33
+ pendingTurnIds(sessionId: string): Promise<string[]>;
34
+ prepare(input: RuntimeTurnInput, cwd: string, signal?: AbortSignal): Promise<{
35
+ state: NativeSession;
36
+ resume: "native" | "restored" | "handoff" | "new";
37
+ }>;
38
+ started(state: NativeSession, turnId: string): Promise<void>;
39
+ private resultPath;
40
+ recordResult(state: NativeSession, requestId: string, events: RuntimeExecutionEvent[]): Promise<void>;
41
+ recoverResult(input: Pick<RuntimeTurnInput, "sessionId" | "harness" | "turnId">, requestId?: string): Promise<{
42
+ state: NativeSession;
43
+ events: RuntimeExecutionEvent[];
44
+ } | null>;
45
+ archive(state: NativeSession, turnId: string): Promise<HarnessArchive | null>;
46
+ acknowledge(state: NativeSession, turnId: string, revision: string): Promise<void>;
47
+ }
@@ -0,0 +1,300 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { mkdir, open, readFile, readdir, rm } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { contextToPiMessages, RUNTIME_RECOVERY_BATCH_SIZE, selectRuntimeContextMessages, runtimeEventSchema } from "@neta-art/cohub";
6
+ import { RuntimeArchiveStore, checksumNativeFile, atomicRuntimeJson as atomicJson } from "./archive-store.js";
7
+ import { importNativeArchive, readCodexArchiveTotals } from "./native-archive.js";
8
+ export class ContextRequiredError extends Error {
9
+ historyOnly;
10
+ constructor(message, historyOnly = false) {
11
+ super(message);
12
+ this.historyOnly = historyOnly;
13
+ }
14
+ }
15
+ const checksum = (data) => createHash("sha256").update(data).digest("hex");
16
+ const missing = (error) => error?.code === "ENOENT";
17
+ class CaptureUnavailableError extends Error {
18
+ }
19
+ /** Local references are hints validated against actual native files, never cloud existence claims. */
20
+ export class RuntimeSessionStore {
21
+ root;
22
+ archives;
23
+ archiveFlush = null;
24
+ constructor(spaceId, stateRoot = join(homedir(), ".local", "state", "cohub", "runtime"), transport) {
25
+ this.root = join(stateRoot, spaceId);
26
+ this.archives = new RuntimeArchiveStore(join(this.root, "archives"), transport);
27
+ }
28
+ statePath(input) { return join(this.root, input.harness, `${input.sessionId}.json`); }
29
+ async *pendingExecutionBatches() {
30
+ let batch = [];
31
+ for (const harness of ["pi", "codex"]) {
32
+ const directory = join(this.root, harness);
33
+ const names = await readdir(directory).catch((error) => { if (missing(error))
34
+ return []; throw error; });
35
+ for (const name of names) {
36
+ if (!name.endsWith(".json"))
37
+ continue;
38
+ try {
39
+ const state = JSON.parse(await readFile(join(directory, name), "utf8"));
40
+ if (state.version !== 1 || state.harness !== harness || !state.sessionId || !state.pendingTurnId)
41
+ continue;
42
+ batch.push({ sessionId: state.sessionId, turnId: state.pendingTurnId, harness });
43
+ if (batch.length >= RUNTIME_RECOVERY_BATCH_SIZE) {
44
+ yield batch;
45
+ batch = [];
46
+ }
47
+ }
48
+ catch (error) {
49
+ console.error(`Runtime session state unreadable: ${join(directory, name)}`, error);
50
+ }
51
+ }
52
+ }
53
+ if (batch.length)
54
+ yield batch;
55
+ }
56
+ async flushArchives(signal) {
57
+ this.archiveFlush ??= this.flushArchiveOutbox(signal).finally(() => { this.archiveFlush = null; });
58
+ return this.archiveFlush;
59
+ }
60
+ async flushArchiveOutbox(signal) {
61
+ const captures = join(this.archives.root, "captures");
62
+ const names = await readdir(captures).catch((error) => { if (missing(error))
63
+ return []; throw error; });
64
+ for (const name of names) {
65
+ signal.throwIfAborted();
66
+ if (!name.endsWith(".json"))
67
+ continue;
68
+ let receipt;
69
+ try {
70
+ receipt = await readFile(join(captures, name), "utf8");
71
+ let state;
72
+ try {
73
+ state = JSON.parse(receipt);
74
+ }
75
+ catch {
76
+ throw new CaptureUnavailableError("Invalid capture receipt");
77
+ }
78
+ const turnId = state?.archivePendingTurnId;
79
+ if (typeof turnId !== "string" || typeof state?.path !== "string" || typeof state.resultChecksum !== "string") {
80
+ throw new CaptureUnavailableError("Invalid capture receipt");
81
+ }
82
+ if (!await this.archives.hasCapture(turnId)) {
83
+ const digest = await checksumNativeFile(state.path).catch((error) => {
84
+ if (missing(error))
85
+ throw new CaptureUnavailableError("Native session missing");
86
+ throw error;
87
+ });
88
+ if (digest !== state.resultChecksum)
89
+ throw new CaptureUnavailableError("Native session changed; original files retained");
90
+ signal.throwIfAborted();
91
+ await this.archives.stage(state, turnId);
92
+ }
93
+ await rm(join(captures, name), { force: true });
94
+ }
95
+ catch (error) {
96
+ signal.throwIfAborted();
97
+ if (error instanceof CaptureUnavailableError && receipt !== undefined) {
98
+ // Preserve the exact receipt before retiring it from the retry queue. Never alter native files.
99
+ await atomicJson(join(this.archives.root, "failed", "captures", `${name}.${checksum(receipt)}.json`), {
100
+ receipt, reason: error.message, failedAt: new Date().toISOString(),
101
+ });
102
+ await rm(join(captures, name), { force: true });
103
+ console.error("Archive capture unavailable; receipt retained:", error.message);
104
+ }
105
+ else
106
+ console.error("Archive capture pending:", error);
107
+ }
108
+ }
109
+ await this.archives.flush(signal);
110
+ }
111
+ async pendingTurnIds(sessionId) {
112
+ const ids = [];
113
+ for (const harness of ["pi", "codex"]) {
114
+ try {
115
+ const state = JSON.parse(await readFile(this.statePath({ sessionId, harness }), "utf8"));
116
+ if (state.sessionId !== sessionId || state.harness !== harness)
117
+ throw new Error("Local session identity mismatch");
118
+ if (state.pendingTurnId)
119
+ ids.push(state.pendingTurnId);
120
+ }
121
+ catch (error) {
122
+ if (!missing(error))
123
+ throw error;
124
+ }
125
+ }
126
+ return ids;
127
+ }
128
+ async prepare(input, cwd, signal) {
129
+ let previous = null;
130
+ try {
131
+ previous = JSON.parse(await readFile(this.statePath(input), "utf8"));
132
+ }
133
+ catch (error) {
134
+ if (!missing(error))
135
+ throw new Error("Local session state is unreadable; original files were preserved", { cause: error });
136
+ }
137
+ if (previous) {
138
+ if (previous.sessionId !== input.sessionId || previous.harness !== input.harness)
139
+ throw new Error("Local session identity mismatch");
140
+ const pending = previous.pendingTurnId;
141
+ if (pending) {
142
+ const resolved = input.context.resolvedTurnIds?.includes(pending) === true;
143
+ const settled = input.context.settledTurnIds?.includes(pending) === true;
144
+ const lostAcknowledgement = Boolean(previous.resultChecksum) && pending === input.context.throughTurnId && pending !== input.turnId;
145
+ // A human-confirmed stop is authoritative: rebuild from durable history instead of
146
+ // resuming a native projection whose outcome the server never recorded.
147
+ const retire = resolved || (settled && !lostAcknowledgement);
148
+ const nativeChecksum = await checksumNativeFile(previous.path).catch((error) => { if (missing(error))
149
+ return null; throw error; });
150
+ if (previous.resultChecksum && (settled || resolved) && nativeChecksum && nativeChecksum !== previous.resultChecksum) {
151
+ throw new Error("Native session changed outside Cohub; original data was preserved");
152
+ }
153
+ if (retire) {
154
+ // The server reached a terminal state for this turn. Archive the local projection and
155
+ // rebuild from durable context; native files are never deleted, so nothing is lost.
156
+ const receipt = await readFile(this.resultPath(previous), "utf8").catch((error) => { if (missing(error))
157
+ return null; throw error; });
158
+ const retired = { state: previous, receipt };
159
+ await atomicJson(join(this.root, "retired", `${pending}.${checksum(JSON.stringify(retired))}.json`), retired);
160
+ previous = null;
161
+ }
162
+ else if (lostAcknowledgement) {
163
+ // The server already persisted this turn; only our acknowledgement was lost.
164
+ await this.acknowledge(previous, pending, input.context.revision);
165
+ }
166
+ else if (input.context.complete === false) {
167
+ throw new ContextRequiredError("Server resolution is required for the pending native execution");
168
+ }
169
+ else {
170
+ throw new Error(`Local turn ${pending} has unconfirmed results; reconcile it before continuing`);
171
+ }
172
+ }
173
+ }
174
+ if (previous) {
175
+ try {
176
+ if (await checksumNativeFile(previous.path) !== previous.checksum)
177
+ throw new Error("Native session changed outside Cohub; original data was preserved");
178
+ if (previous.archivePendingTurnId) {
179
+ await this.archives.stage(previous, previous.archivePendingTurnId);
180
+ previous.archivePendingTurnId = undefined;
181
+ await atomicJson(this.statePath(previous), previous);
182
+ }
183
+ if (input.harness === "codex" && !previous.codexTokenTotals)
184
+ previous.codexTokenTotals = await readCodexArchiveTotals(previous.path);
185
+ if (previous.cwd === cwd && previous.throughTurnId === input.context.throughTurnId && previous.revision === input.context.revision)
186
+ return { state: previous, resume: "native" };
187
+ }
188
+ catch (error) {
189
+ if (!missing(error))
190
+ throw error;
191
+ }
192
+ }
193
+ if (input.context.complete === false && !input.context.archive)
194
+ throw new ContextRequiredError("Full context is required to materialize this session");
195
+ const id = randomUUID();
196
+ const path = join(this.root, input.harness, `${id}.jsonl`);
197
+ const archive = input.context.archive;
198
+ const restore = archive?.harness === input.harness && archive.sessionId === input.sessionId && archive.turnId === input.context.throughTurnId;
199
+ const rawPath = join(this.root, "archives", "restored", `${id}.jsonl`);
200
+ const state = {
201
+ version: 1, harness: input.harness, sessionId: input.sessionId, nativeSessionId: id,
202
+ path, cwd, throughTurnId: input.context.throughTurnId, revision: input.context.revision, checksum: "", pendingTurnId: null,
203
+ };
204
+ if (restore) {
205
+ try {
206
+ const restored = await this.archives.restore(archive, rawPath, signal);
207
+ Object.assign(state, await importNativeArchive({ source: rawPath, target: path, harness: input.harness, nativeSessionId: restored.nativeSessionId, id, cwd, signal }));
208
+ return { state, resume: "restored" };
209
+ }
210
+ catch (error) {
211
+ signal?.throwIfAborted();
212
+ console.error("Native archive unavailable; rebuilding from durable history:", error);
213
+ }
214
+ }
215
+ if (input.context.complete === false)
216
+ throw new ContextRequiredError("Database history is required after archive recovery failed", true);
217
+ let data = null;
218
+ if (input.harness === "pi") {
219
+ const entries = [{ type: "session", version: 3, id, cwd, timestamp: new Date().toISOString() }];
220
+ let parentId = null;
221
+ const history = selectRuntimeContextMessages(input.context.messages);
222
+ const summary = history[0]?.role === "system" ? history[0].content.find((block) => block.type === "system_note" && block.note_type === "compacted") : undefined;
223
+ let compaction;
224
+ if (summary?.type === "system_note") {
225
+ parentId = randomUUID().slice(0, 8);
226
+ compaction = { type: "compaction", id: parentId, parentId: null, timestamp: new Date().toISOString(), summary: summary.text,
227
+ firstKeptEntryId: "", tokensBefore: history[0]?.meta?.compaction?.tokensBefore ?? 0 };
228
+ entries.push(compaction);
229
+ }
230
+ for (const message of contextToPiMessages(history)) {
231
+ const entryId = randomUUID().slice(0, 8);
232
+ if (compaction && !compaction.firstKeptEntryId)
233
+ compaction.firstKeptEntryId = entryId;
234
+ entries.push({ type: "message", id: entryId, parentId, timestamp: new Date().toISOString(), message });
235
+ parentId = entryId;
236
+ }
237
+ data = `${entries.map((entry) => JSON.stringify(entry)).join("\n")}\n`;
238
+ }
239
+ if (data != null) {
240
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
241
+ const file = await open(path, "wx", 0o600);
242
+ try {
243
+ await file.writeFile(data);
244
+ await file.sync();
245
+ }
246
+ finally {
247
+ await file.close();
248
+ }
249
+ state.checksum = checksum(data);
250
+ }
251
+ return { state, resume: input.context.messages.length ? "handoff" : "new" };
252
+ }
253
+ async started(state, turnId) { state.pendingTurnId = turnId; state.resultChecksum = undefined; await atomicJson(this.statePath(state), state); }
254
+ resultPath(state) { return join(this.root, "results", `${state.sessionId}.${state.harness}.json`); }
255
+ async recordResult(state, requestId, events) {
256
+ if (!state.pendingTurnId)
257
+ throw new Error("Cannot record a result for an idle native session");
258
+ state.resultChecksum = await checksumNativeFile(state.path);
259
+ if (state.archivePendingTurnId)
260
+ await atomicJson(join(this.archives.root, "captures", `${state.archivePendingTurnId}.json`), state);
261
+ await atomicJson(this.resultPath(state), { requestId, state, events });
262
+ await atomicJson(this.statePath(state), state);
263
+ }
264
+ async recoverResult(input, requestId) {
265
+ try {
266
+ const saved = JSON.parse(await readFile(this.resultPath(input), "utf8"));
267
+ if (saved.state.sessionId !== input.sessionId || saved.state.harness !== input.harness)
268
+ throw new Error("Runtime result identity mismatch");
269
+ if (saved.state.pendingTurnId !== input.turnId)
270
+ return null;
271
+ if (requestId && saved.requestId !== requestId)
272
+ throw new Error("Runtime result execution identity mismatch");
273
+ return { state: saved.state, events: saved.events.map((event) => runtimeEventSchema.parse(event)) };
274
+ }
275
+ catch (error) {
276
+ if (missing(error))
277
+ return null;
278
+ throw error;
279
+ }
280
+ }
281
+ async archive(state, turnId) {
282
+ if (!state.path)
283
+ return null;
284
+ state.archivePendingTurnId = turnId;
285
+ const reference = await this.archives.stage(state, turnId);
286
+ state.archivePendingTurnId = undefined;
287
+ return reference;
288
+ }
289
+ async acknowledge(state, turnId, revision) {
290
+ if (state.pendingTurnId !== turnId)
291
+ throw new Error("Runtime acknowledgement identity mismatch");
292
+ const currentChecksum = await checksumNativeFile(state.path).catch((error) => { if (missing(error))
293
+ return state.resultChecksum ?? state.checksum; throw error; });
294
+ if (state.resultChecksum && currentChecksum !== state.resultChecksum)
295
+ throw new Error("Native data changed before acknowledgement; files preserved");
296
+ const acknowledged = { ...state, checksum: currentChecksum, pendingTurnId: null, resultChecksum: undefined, throughTurnId: turnId, revision };
297
+ await atomicJson(this.statePath(state), acknowledged);
298
+ Object.assign(state, acknowledged);
299
+ }
300
+ }
package/package.json CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "6.12.0",
3
+ "version": "7.0.1",
4
4
  "description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
5
5
  "type": "module",
6
+ "engines": {
7
+ "node": ">=24"
8
+ },
6
9
  "license": "Apache-2.0",
7
10
  "bin": {
8
11
  "cohub": "./bin/cohub.js"
@@ -15,18 +18,20 @@
15
18
  "NOTICE"
16
19
  ],
17
20
  "dependencies": {
18
- "@neta-art/generation": "^0.1.28",
21
+ "@neta-art/generation": "^0.1.30",
19
22
  "commander": "^15.0.0",
20
23
  "pixi.js": "^8.20.1",
21
24
  "sharp": "^0.35.4",
22
- "@neta-art/cohub": "8.17.0"
25
+ "@neta-art/cohub": "8.19.0"
23
26
  },
24
27
  "publishConfig": {
25
28
  "access": "public"
26
29
  },
27
30
  "devDependencies": {
28
31
  "@types/node": "^26.4.0",
29
- "typescript": "^7.0.2"
32
+ "@types/ws": "^8.18.1",
33
+ "typescript": "^7.0.2",
34
+ "ws": "^8.21.3"
30
35
  },
31
36
  "optionalDependencies": {
32
37
  "@napi-rs/canvas": "^1.0.8"
@@ -34,6 +39,8 @@
34
39
  "scripts": {
35
40
  "build": "tsc -p tsconfig.build.json",
36
41
  "test": "node ../../scripts/test/run.mjs 'tests/**/*.test.ts'",
42
+ "test:runtime": "node --import tsx --test tests/runtime-*.integration.ts",
43
+ "test:runtime:native": "node --import tsx tests/runtime-native.smoke.ts",
37
44
  "typecheck": "tsc -p tsconfig.json --noEmit"
38
45
  }
39
46
  }
@@ -1,3 +0,0 @@
1
- import type { Command } from "commander";
2
- export declare const resolveLocalSpaceName: (rootDir: string, requestedName?: string) => string;
3
- export declare function registerSandbox(program: Command): void;