@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
@@ -12,9 +12,10 @@ import { discoverHarnesses } from "./harness.js";
12
12
  import { RuntimeDiagnostics, serializeDiagnosticError } from "./diagnostics.js";
13
13
  import { ownRuntimeInstance, runtimeInstanceDirectory } from "./instance.js";
14
14
  import { createDiagnosticConsole } from "./presentation.js";
15
- import { RuntimeSessionStore } from "./session-store.js";
16
- import { captureNativeSession, flushNativeSessions, nativeWebSocketTransport, summarizeNativeSyncErrors } from "./native-sync.js";
17
- import { serveNativeDaemon } from "./native-ipc.js";
15
+ import { join } from "node:path";
16
+ import { RuntimeArchiveStore } from "./archive-store.js";
17
+ import { readNativeConfig, runtimeStateRoot } from "./native/config.js";
18
+ import { NativeRuntime } from "./native/daemon.js";
18
19
  export function sandboxOutputLevel(value, stream) {
19
20
  const level = typeof value === "string" ? value.toLowerCase() : "";
20
21
  if (["debug", "info", "warn", "error"].includes(level))
@@ -29,19 +30,28 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
29
30
  process.once("SIGTERM", stop);
30
31
  const client = createClient();
31
32
  const space = client.space(config.spaceId);
32
- const store = new RuntimeSessionStore(config.spaceId, { projectionSource: space, archiveTransport: space });
33
+ const stateRoot = runtimeStateRoot(config.spaceId);
34
+ const archives = new RuntimeArchiveStore(join(stateRoot, "archives"), space);
33
35
  const consoleSink = config.background ? undefined : createDiagnosticConsole(config.verbose);
34
36
  const diagnostics = new RuntimeDiagnostics({
35
- root: store.root, spaceId: config.spaceId, runtimeId: randomUUID(),
37
+ root: stateRoot, spaceId: config.spaceId, runtimeId: randomUUID(),
36
38
  onEvent: (event) => { consoleSink?.(event); onDiagnostic?.(event); },
37
39
  });
38
- store.setDiagnostics(diagnostics);
40
+ archives.setErrorReporter((error, index) => diagnostics.log("warn", "archive.upload_pending", { error: serializeDiagnosticError(error) }, {
41
+ component: "archive", sessionId: index?.sessionId, turnId: index?.turnId, harness: index?.harness,
42
+ }));
43
+ const native = new NativeRuntime({
44
+ spaceId: config.spaceId, root: config.root, stateRoot, harnesses: config.harnesses, executables: config.executables,
45
+ identity: config.identity, config: await readNativeConfig(stateRoot, config.identity).catch(() => null),
46
+ archives, projectionSource: space, diagnostics,
47
+ });
39
48
  let status = {
40
49
  spaceId: config.spaceId, root: config.root, runtimeId: diagnostics.runtimeId,
41
50
  pid: process.pid, harnesses: config.harnesses, background: config.background,
42
51
  state: "starting", harnessConnected: false, workspaceConnected: false,
43
52
  diagnosticsPath: diagnostics.directory,
44
53
  };
54
+ const summary = () => ({ ...status, native: native.status() });
45
55
  let hasBeenReady = false;
46
56
  const update = (patch) => {
47
57
  const next = { ...status, ...patch };
@@ -59,9 +69,7 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
59
69
  };
60
70
  let closeInstance;
61
71
  let bridgeTask;
62
- let nativeSyncTask;
63
- let closeNativeDaemon;
64
- let nativeSend = null;
72
+ let nativeStarted = false;
65
73
  let tokenInFlight = null;
66
74
  const token = (forceRefresh = false) => {
67
75
  tokenInFlight ??= (async () => {
@@ -77,15 +85,15 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
77
85
  return tokenInFlight;
78
86
  };
79
87
  try {
80
- closeInstance = await ownRuntimeInstance(runtimeInstanceDirectory(config.identity, config.spaceId), () => status, async (force) => {
88
+ closeInstance = await ownRuntimeInstance(runtimeInstanceDirectory(config.identity, config.spaceId), summary, async (force) => {
81
89
  if (!force)
82
- for await (const batch of store.pendingExecutionBatches()) {
90
+ for await (const batch of native.executor.results.pendingBatches()) {
83
91
  if (batch.length)
84
92
  throw new Error("Unconfirmed executions remain. Use down --yes to stop; results and files are retained");
85
93
  }
86
94
  update({ state: "stopping" });
87
95
  setTimeout(stop, 30);
88
- });
96
+ }, (action, message) => native.control(action, message));
89
97
  onState({ ...status });
90
98
  diagnostics.log("info", "runtime.cli_started", { platform: process.platform, node: process.versions.node, harnesses: config.harnesses });
91
99
  const [binary, capabilities] = await Promise.all([
@@ -220,105 +228,22 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
220
228
  }
221
229
  };
222
230
  bridgeTask = runBridge();
223
- // Native reconciliation is event-driven: captures and channel reconnects wake it, while failures
224
- // get bounded exponential retries so idle Runtimes do not scan every few seconds.
225
- let nativeWakePending = true;
226
- let nativeWake = null;
227
- let nativeRetryTimer;
228
- let nativeRetryDelay = 1000;
229
- const wakeNativeSync = () => {
230
- nativeWakePending = true;
231
- // A new capture may wake healthy work, but it must not cancel a failed receipt's backoff.
232
- if (!nativeRetryTimer) {
233
- nativeWake?.();
234
- nativeWake = null;
235
- }
236
- };
237
- const waitForNativeWake = async () => {
238
- if (nativeWakePending && !nativeRetryTimer) {
239
- nativeWakePending = false;
240
- return;
241
- }
242
- await new Promise((resolve) => {
243
- const finish = () => {
244
- signal.removeEventListener("abort", finish);
245
- if (nativeWake === finish)
246
- nativeWake = null;
247
- resolve();
248
- };
249
- nativeWake = finish;
250
- signal.addEventListener("abort", finish, { once: true });
251
- if (signal.aborted)
252
- finish();
253
- });
254
- nativeWakePending = false;
255
- };
256
- nativeSyncTask = (async () => {
257
- let lastFailureLogAt = 0;
258
- let failureStartedAt = 0;
259
- let accumulatedErrors = [];
260
- while (!signal.aborted) {
261
- await waitForNativeWake();
262
- if (signal.aborted)
263
- break;
264
- if (!nativeSend)
265
- continue;
266
- let failed = false;
267
- const cycleErrors = [];
268
- try {
269
- failed = await flushNativeSessions(config.spaceId, config.identity, signal, (error) => cycleErrors.push(error), nativeWebSocketTransport(config.spaceId, config.identity, nativeSend));
270
- }
271
- catch (error) {
272
- if (!signal.aborted) {
273
- cycleErrors.push(error);
274
- failed = true;
275
- }
276
- }
277
- if (cycleErrors.length && !signal.aborted) {
278
- const now = Date.now();
279
- failureStartedAt ||= now;
280
- accumulatedErrors.push(...cycleErrors);
281
- if (!lastFailureLogAt || now - lastFailureLogAt >= 60_000) {
282
- diagnostics.log("warn", "native.sync_pending", {
283
- failedStores: accumulatedErrors.length,
284
- retryInMs: nativeRetryDelay,
285
- errors: summarizeNativeSyncErrors(accumulatedErrors),
286
- });
287
- accumulatedErrors = [];
288
- lastFailureLogAt = now;
289
- }
290
- }
291
- else if (!signal.aborted && !failed && failureStartedAt) {
292
- diagnostics.log("info", "native.sync_recovered", { durationMs: Date.now() - failureStartedAt });
293
- failureStartedAt = 0;
294
- lastFailureLogAt = 0;
295
- accumulatedErrors = [];
296
- }
297
- if (failed && !signal.aborted) {
298
- nativeWakePending = false;
299
- if (!nativeRetryTimer)
300
- nativeRetryTimer = setTimeout(() => { nativeRetryTimer = undefined; wakeNativeSync(); }, nativeRetryDelay);
301
- nativeRetryDelay = Math.min(30_000, nativeRetryDelay * 2);
302
- }
303
- else if (!failed) {
304
- nativeRetryDelay = 1000;
305
- }
306
- }
307
- if (nativeRetryTimer)
308
- clearTimeout(nativeRetryTimer);
309
- })();
310
- closeNativeDaemon = await serveNativeDaemon({ runtimeRoot: store.root, handle: async (request) => {
311
- const result = { store: await captureNativeSession(request) };
312
- if (result.store)
313
- wakeNativeSync();
314
- return result;
315
- } });
231
+ await native.start(signal);
232
+ nativeStarted = true;
233
+ let importRequested = Boolean(config.importHistory);
316
234
  await serveRuntime({
317
235
  spaceId: config.spaceId, cwd: config.root, url: url.toString(), capabilities,
318
- harnesses: config.executables, runtimeId: status.runtimeId, diagnostics, token, signal, store,
236
+ runtimeId: status.runtimeId, diagnostics, token, signal, executor: native.executor,
319
237
  onReady: () => update({ harnessConnected: true }),
320
- onDisconnected: () => { nativeSend = null; update({ harnessConnected: false }); },
321
- onNativeChannel: (send) => { nativeSend = send; wakeNativeSync(); },
238
+ onDisconnected: () => { native.connect(null); update({ harnessConnected: false }); },
239
+ onNativeStop: (stop) => { native.stop(stop.sessionId, stop.turnId); },
240
+ onNativeChannel: (send) => {
241
+ native.connect(send);
242
+ if (!importRequested)
243
+ return;
244
+ importRequested = false;
245
+ void native.control("import", { command: "start" }).catch((error) => diagnostics.log("warn", "native.import_failed", { error: serializeDiagnosticError(error) }));
246
+ },
322
247
  });
323
248
  }
324
249
  catch (error) {
@@ -331,8 +256,9 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
331
256
  finally {
332
257
  controller.abort();
333
258
  try {
334
- await Promise.all([bridgeTask, nativeSyncTask]);
335
- await closeNativeDaemon?.();
259
+ await bridgeTask;
260
+ if (nativeStarted)
261
+ await native.close(AbortSignal.timeout(5_000));
336
262
  }
337
263
  finally {
338
264
  try {
@@ -30,14 +30,12 @@ export type SessionTurnProjectionClient = {
30
30
  };
31
31
  };
32
32
  export type ProjectionSourceTurn = CanonicalProjectionTurn;
33
- export declare function hydrateSessionTurn(turn: SessionTurnRecord, client: ReturnType<SessionTurnProjectionClient["session"]>, signal?: AbortSignal): Promise<CanonicalProjectionTurn>;
34
33
  export declare function listSessionProjectionTurns(source: SessionTurnProjectionClient, sessionId: string, options?: {
35
34
  afterSequence?: number | null;
36
35
  throughSequence?: number | null;
37
36
  excludeTurnId?: string | null;
38
37
  signal?: AbortSignal;
39
38
  }): Promise<ProjectionSourceTurn[]>;
40
- export declare function getSessionProjectionTurn(source: SessionTurnProjectionClient, sessionId: string, turnId: string, signal?: AbortSignal): Promise<CanonicalProjectionTurn>;
41
39
  export declare function projectTurnBatch(input: Omit<ProjectionInput, "turns"> & {
42
40
  turns: ProjectionSourceTurn[];
43
- }, target: ProjectionTarget, includeHeader?: boolean): import("@neta-art/cohub").NativeProjection;
41
+ }, target: ProjectionTarget): import("@neta-art/cohub").NativeProjection;
@@ -57,7 +57,7 @@ async function mapWithConcurrency(items, concurrency, mapper) {
57
57
  await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, worker));
58
58
  return results;
59
59
  }
60
- export async function hydrateSessionTurn(turn, client, signal) {
60
+ async function hydrateSessionTurn(turn, client, signal) {
61
61
  const sourceId = sourceSessionId(turn);
62
62
  const messages = [];
63
63
  if (turn.userContent.length) {
@@ -115,13 +115,6 @@ export async function listSessionProjectionTurns(source, sessionId, options = {}
115
115
  }
116
116
  return mapWithConcurrency(turns, 4, (turn) => hydrateSessionTurn(turn, client, options.signal));
117
117
  }
118
- export async function getSessionProjectionTurn(source, sessionId, turnId, signal) {
119
- const client = source.session(sessionId);
120
- return hydrateSessionTurn((await client.turns.get(turnId, { signal })).turn, client, signal);
121
- }
122
- export function projectTurnBatch(input, target, includeHeader = true) {
123
- const projection = projectNativeSession(input, target);
124
- if (includeHeader)
125
- return projection;
126
- return { ...projection, records: projection.records.filter((record) => record.sourceTurnId !== null) };
118
+ export function projectTurnBatch(input, target) {
119
+ return projectNativeSession(input, target);
127
120
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "8.2.0",
3
+ "version": "8.3.1",
4
4
  "description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -22,7 +22,8 @@
22
22
  "commander": "^15.0.0",
23
23
  "pixi.js": "^8.20.1",
24
24
  "sharp": "^0.35.4",
25
- "@neta-art/cohub": "8.21.1"
25
+ "ws": "^8.21.3",
26
+ "@neta-art/cohub": "8.22.0"
26
27
  },
27
28
  "publishConfig": {
28
29
  "access": "public"
@@ -30,8 +31,7 @@
30
31
  "devDependencies": {
31
32
  "@types/node": "^26.4.0",
32
33
  "@types/ws": "^8.18.1",
33
- "typescript": "^7.0.2",
34
- "ws": "^8.21.3"
34
+ "typescript": "^7.0.2"
35
35
  },
36
36
  "optionalDependencies": {
37
37
  "@napi-rs/canvas": "^1.0.8"
@@ -42,7 +42,6 @@
42
42
  "test": "node ../../scripts/test/run.mjs 'tests/**/*.test.ts'",
43
43
  "test:runtime": "node --import tsx --test tests/runtime-*.integration.ts",
44
44
  "test:runtime:native": "node --import tsx tests/runtime-native.smoke.ts",
45
- "test:runtime:plugins": "node scripts/test-native-plugins.mjs",
46
45
  "typecheck": "tsc -p tsconfig.json --noEmit"
47
46
  }
48
47
  }
@@ -1,28 +0,0 @@
1
- import { type NativeSyncConfig } from "./native-sync.js";
2
- type Harness = "pi" | "codex";
3
- export type NativeSyncReport = {
4
- enabled: boolean;
5
- harnesses: Harness[];
6
- note?: string;
7
- };
8
- /** Already enabled for this exact directory and every requested Harness: installation is idempotent. */
9
- export declare function nativeSyncSatisfied(config: NativeSyncConfig | null, input: {
10
- spaceId: string;
11
- root: string;
12
- harnesses: Harness[];
13
- }): boolean;
14
- /** One explicit consent prompt, defaulting to yes; native history may contain secrets. */
15
- export declare function askNativeSyncConsent(harnesses: Harness[], ask?: (question: string) => Promise<string>): Promise<boolean>;
16
- /** Enable native sync after consent; capability or write failures warn and never block the Runtime. */
17
- export declare function ensureNativeSync(input: {
18
- root: string;
19
- spaceId: string;
20
- identity: string;
21
- harnesses: Harness[];
22
- yes?: boolean;
23
- executables?: {
24
- pi?: string;
25
- codex?: string;
26
- };
27
- }, log?: (line: string) => void): Promise<NativeSyncReport>;
28
- export {};
@@ -1,47 +0,0 @@
1
- import { createInterface } from "node:readline/promises";
2
- import { installNativeSync } from "./native-install.js";
3
- import { nativeRuntimeRoot, readNativeSyncConfig } from "./native-sync.js";
4
- import { serializeDiagnosticError } from "./diagnostics.js";
5
- /** Already enabled for this exact directory and every requested Harness: installation is idempotent. */
6
- export function nativeSyncSatisfied(config, input) {
7
- return Boolean(config && config.spaceId === input.spaceId && config.root === input.root && input.harnesses.every((harness) => config.harnesses.includes(harness)));
8
- }
9
- /** One explicit consent prompt, defaulting to yes; native history may contain secrets. */
10
- export async function askNativeSyncConsent(harnesses, ask = defaultAsk) {
11
- const answer = await ask(`Sync native ${harnesses.join(" / ")} chats in this folder and upload their history (may contain secrets) to this Space? [Y/n] `);
12
- return /^(|y(es)?)$/i.test(answer.trim());
13
- }
14
- async function defaultAsk(question) {
15
- const rl = createInterface({ input: process.stdin, output: process.stderr });
16
- try {
17
- return await rl.question(question);
18
- }
19
- finally {
20
- rl.close();
21
- }
22
- }
23
- /** Enable native sync after consent; capability or write failures warn and never block the Runtime. */
24
- export async function ensureNativeSync(input, log = (line) => process.stderr.write(line)) {
25
- const config = await readNativeSyncConfig(nativeRuntimeRoot(input.spaceId), input.identity).catch(() => null);
26
- if (nativeSyncSatisfied(config, input))
27
- return { enabled: true, harnesses: config?.harnesses ?? [] };
28
- if (!input.yes) {
29
- // Non-interactive sessions cannot consent; they continue without sync instead of failing `up`.
30
- if (!process.stdin.isTTY) {
31
- log("Native sync off · rerun interactively or with --yes to enable\n");
32
- return { enabled: false, harnesses: input.harnesses, note: "non-interactive" };
33
- }
34
- if (!await askNativeSyncConsent(input.harnesses))
35
- return { enabled: false, harnesses: input.harnesses, note: "declined" };
36
- }
37
- try {
38
- const result = await installNativeSync(input);
39
- log(`Native sync enabled. Reload Pi or restart Codex and review its hook trust prompt.\n${result.configPath}`);
40
- return { enabled: true, harnesses: result.harnesses };
41
- }
42
- catch (error) {
43
- const message = serializeDiagnosticError(error).message;
44
- log(`Native sync unavailable; Runtime continues without it — ${message}\n`);
45
- return { enabled: false, harnesses: input.harnesses, note: message };
46
- }
47
- }
@@ -1 +0,0 @@
1
- export declare function runCodexNativeHook(payload: unknown): Promise<void>;
@@ -1,28 +0,0 @@
1
- import { pathToFileURL } from "node:url";
2
- import { requestNativeDaemon } from "./native-ipc.js";
3
- export async function runCodexNativeHook(payload) {
4
- if (process.env.COHUB_TURN_ID || process.env.COHUB_EXECUTION_TOKEN)
5
- return;
6
- const value = payload;
7
- if (!value || typeof value.cwd !== "string" || typeof value.session_id !== "string")
8
- throw new Error("Invalid Codex hook identity");
9
- if (typeof value.transcript_path !== "string" || !value.transcript_path)
10
- return;
11
- const result = await requestNativeDaemon({ harness: "codex", cwd: value.cwd, path: value.transcript_path, nativeSessionId: value.session_id });
12
- if (!result.ok && !result.skipped)
13
- throw new Error(result.message);
14
- }
15
- async function main() {
16
- let input = "";
17
- for await (const chunk of process.stdin) {
18
- input += chunk.toString();
19
- if (Buffer.byteLength(input) > 4 * 1024 * 1024)
20
- throw new Error("Codex hook input is too large");
21
- }
22
- await runCodexNativeHook(JSON.parse(input));
23
- }
24
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
25
- void main().catch((error) => {
26
- process.stderr.write(`Cohub sync pending; native execution continues: ${error instanceof Error ? error.message : String(error)}\n`);
27
- });
28
- }
@@ -1,21 +0,0 @@
1
- export declare function codexNativeHookBlock(node: string, hook: string): string;
2
- export declare function verifyNativeSyncSupport(harnesses: ("pi" | "codex")[], cwd: string, executables?: {
3
- pi?: string;
4
- codex?: string;
5
- }): Promise<void>;
6
- /** Install once in the user's native configuration; data collection remains explicitly project-scoped. */
7
- export declare function installNativeSync(input: {
8
- root: string;
9
- spaceId: string;
10
- identity: string;
11
- harnesses: ("pi" | "codex")[];
12
- disabled?: boolean;
13
- executables?: {
14
- pi?: string;
15
- codex?: string;
16
- };
17
- }): Promise<{
18
- configPath: string;
19
- harnesses: ("codex" | "pi")[];
20
- enabled: boolean;
21
- }>;
@@ -1,130 +0,0 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import { execFile } from "node:child_process";
3
- import { promisify } from "node:util";
4
- import { lstat, mkdir, open, readFile, rename, rm } from "node:fs/promises";
5
- import { dirname, join } from "node:path";
6
- import { homedir } from "node:os";
7
- import { fileURLToPath } from "node:url";
8
- import { atomicRuntimeJson } from "./archive-store.js";
9
- import { nativeRuntimeRoot, nativeSyncConfigPath, readNativeSyncConfig } from "./native-sync.js";
10
- import { withRuntimeSpaceBindingsLock } from "./space-binding.js";
11
- const START = "# BEGIN COHUB NATIVE SYNC";
12
- const END = "# END COHUB NATIVE SYNC";
13
- const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
14
- const missing = (error) => error.code === "ENOENT";
15
- export function codexNativeHookBlock(node, hook) {
16
- const command = `${shellQuote(node)} ${shellQuote(hook)}`;
17
- return `${START}\n${["SessionStart", "UserPromptSubmit", "PostToolUse", "Stop", "Interrupt", "SessionEnd"].map((event) => `[[hooks.${event}]]\n[[hooks.${event}.hooks]]\ntype = "command"\ncommand = ${JSON.stringify(command)}\ntimeout = ${event === "SessionEnd" || event === "Interrupt" ? 3 : 10}\n`).join("\n")}${END}\n`;
18
- }
19
- async function installText(path, update) {
20
- await withRuntimeSpaceBindingsLock(async () => {
21
- const info = await lstat(path).catch((error) => { if (missing(error))
22
- return null; throw error; });
23
- if (info && (!info.isFile() || info.isSymbolicLink()))
24
- throw new Error(`Refusing to replace a non-regular file: ${path}`);
25
- const original = info ? await readFile(path, "utf8") : null;
26
- const next = update(original);
27
- if (next === original)
28
- return;
29
- await mkdir(dirname(path), { recursive: true, mode: 0o700 });
30
- if (original !== null) {
31
- const backup = `${path}.cohub-backup-${createHash("sha256").update(original).digest("hex").slice(0, 16)}`;
32
- const file = await open(backup, "wx", 0o600).catch((error) => { if (error.code === "EEXIST")
33
- return null; throw error; });
34
- if (file) {
35
- try {
36
- await file.writeFile(original);
37
- await file.sync();
38
- }
39
- finally {
40
- await file.close();
41
- }
42
- }
43
- }
44
- const temporary = `${path}.${randomUUID()}.tmp`;
45
- try {
46
- const file = await open(temporary, "wx", info?.mode ?? 0o600);
47
- try {
48
- await file.writeFile(next);
49
- await file.sync();
50
- }
51
- finally {
52
- await file.close();
53
- }
54
- const current = await readFile(path, "utf8").catch((error) => { if (missing(error))
55
- return null; throw error; });
56
- if (current !== original)
57
- throw new Error(`Configuration changed during installation: ${path}`);
58
- await rename(temporary, path);
59
- const directory = await open(dirname(path), "r");
60
- try {
61
- await directory.sync();
62
- }
63
- finally {
64
- await directory.close();
65
- }
66
- }
67
- finally {
68
- await rm(temporary, { force: true });
69
- }
70
- }, { lockPath: `${path}.cohub-lock` });
71
- }
72
- export async function verifyNativeSyncSupport(harnesses, cwd, executables = {}) {
73
- for (const harness of harnesses) {
74
- const { stdout } = await promisify(execFile)(executables[harness] || harness, harness === "pi" ? ["--version"] : ["features", "list"], { cwd, encoding: "utf8", timeout: 15_000, maxBuffer: 1024 * 1024 });
75
- if (harness === "pi") {
76
- const version = /\b(\d+)\.(\d+)\.(\d+)\b/.exec(stdout);
77
- if (!version || Number(version[1]) === 0 && (Number(version[2]) < 85 || Number(version[2]) === 85 && Number(version[3]) < 1))
78
- throw new Error("Native sync requires Pi 0.85.1+");
79
- }
80
- else if (!/^hooks\s+stable\s+true\s*$/m.test(stdout))
81
- throw new Error("Install a Codex version with stable Hooks and enable hooks first");
82
- }
83
- }
84
- /** Install once in the user's native configuration; data collection remains explicitly project-scoped. */
85
- export async function installNativeSync(input) {
86
- if (!input.disabled)
87
- await verifyNativeSyncSupport(input.harnesses, input.root, input.executables);
88
- const runtimeRoot = nativeRuntimeRoot(input.spaceId);
89
- const configPath = nativeSyncConfigPath(runtimeRoot, input.identity);
90
- const extension = new URL(import.meta.url.endsWith(".ts") ? "./native-pi-extension.ts" : "./native-pi-extension.js", import.meta.url);
91
- const hook = fileURLToPath(new URL(import.meta.url.endsWith(".ts") ? "./native-codex-hook.ts" : "./native-codex-hook.js", import.meta.url));
92
- if (!input.disabled)
93
- for (const harness of input.harnesses) {
94
- if (harness === "pi") {
95
- const content = `// Cohub native Turn sync\nexport { default } from ${JSON.stringify(extension.href)};\n`;
96
- await installText(join(process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent"), "extensions", "cohub.ts"), (existing) => {
97
- if (existing !== null && existing !== content)
98
- throw new Error("Pi Cohub extension already exists; preserve it and review manually");
99
- return content;
100
- });
101
- }
102
- else {
103
- const block = codexNativeHookBlock(process.execPath, hook);
104
- await installText(join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "config.toml"), (existing) => {
105
- if (existing?.includes(block))
106
- return existing;
107
- if (existing?.includes(START) || existing?.includes(END))
108
- throw new Error("Codex Cohub hook block differs; preserve it and review manually");
109
- if (existing && /^\s*hooks\s*=/m.test(existing))
110
- throw new Error("Inline Codex hooks require manual merging");
111
- return `${existing ?? ""}${existing?.endsWith("\n") ? "\n" : "\n\n"}${block}`;
112
- });
113
- }
114
- }
115
- await withRuntimeSpaceBindingsLock(async () => {
116
- const previous = await readNativeSyncConfig(runtimeRoot, input.identity);
117
- if (previous && previous.root !== input.root)
118
- throw new Error("Space native sync belongs to another directory");
119
- const harnesses = new Set(previous?.harnesses ?? []);
120
- for (const harness of input.harnesses) {
121
- if (input.disabled)
122
- harnesses.delete(harness);
123
- else
124
- harnesses.add(harness);
125
- }
126
- const config = { version: 1, identity: input.identity, spaceId: input.spaceId, root: input.root, harnesses: [...harnesses] };
127
- await atomicRuntimeJson(configPath, config);
128
- }, { path: configPath });
129
- return { configPath, harnesses: input.harnesses, enabled: !input.disabled };
130
- }
@@ -1,37 +0,0 @@
1
- import type { NativeSyncStore } from "./native-sync-store.js";
2
- type NativeIpcRequest = {
3
- type: "native.capture";
4
- harness: "pi" | "codex";
5
- cwd: string;
6
- path: string;
7
- nativeSessionId: string;
8
- settled?: boolean;
9
- leafId?: string | null;
10
- sessionStartedAt?: string;
11
- origin?: "local_import";
12
- };
13
- export type NativeIpcResponse = {
14
- ok: true;
15
- pendingTurns: number;
16
- } | {
17
- ok: false;
18
- skipped?: true;
19
- message: string;
20
- };
21
- /**
22
- * `store: null` means the request was valid but nothing was applicable yet (an unbound
23
- * workspace, or a transcript a harness has not written). That is not a sync failure, so
24
- * clients stay silent instead of warning about it.
25
- */
26
- export declare function nativeCaptureResponse(result: {
27
- store: NativeSyncStore | null;
28
- }): Promise<NativeIpcResponse>;
29
- export declare function nativeDaemonSocketFor(cwd: string): Promise<string | null>;
30
- export declare function requestNativeDaemon(input: Omit<NativeIpcRequest, "type">): Promise<NativeIpcResponse>;
31
- export declare function serveNativeDaemon(input: {
32
- runtimeRoot: string;
33
- handle: (request: NativeIpcRequest) => Promise<{
34
- store: NativeSyncStore | null;
35
- }>;
36
- }): Promise<() => Promise<void>>;
37
- export {};