@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.
- package/dist/commands/runtime.d.ts +0 -12
- package/dist/commands/runtime.js +129 -144
- package/dist/commands/search.js +3 -9
- package/dist/commands/spaces.js +5 -2
- package/dist/output.d.ts +6 -0
- package/dist/output.js +11 -0
- package/dist/runtime/codex-usage.d.ts +0 -2
- package/dist/runtime/codex-usage.js +0 -12
- package/dist/runtime/connection.d.ts +5 -5
- package/dist/runtime/connection.js +27 -16
- package/dist/runtime/harness.d.ts +8 -16
- package/dist/runtime/harness.js +1 -366
- package/dist/runtime/instance.d.ts +8 -1
- package/dist/runtime/instance.js +15 -8
- package/dist/runtime/launch.js +10 -5
- package/dist/runtime/native/adapters.d.ts +32 -0
- package/dist/runtime/native/adapters.js +79 -0
- package/dist/runtime/{native-archive.d.ts → native/archive.d.ts} +0 -3
- package/dist/runtime/{native-archive.js → native/archive.js} +2 -17
- package/dist/runtime/native/attach.d.ts +33 -0
- package/dist/runtime/native/attach.js +103 -0
- package/dist/runtime/native/codex-link.d.ts +40 -0
- package/dist/runtime/native/codex-link.js +249 -0
- package/dist/runtime/native/config.d.ts +16 -0
- package/dist/runtime/native/config.js +24 -0
- package/dist/runtime/native/daemon.d.ts +88 -0
- package/dist/runtime/native/daemon.js +367 -0
- package/dist/runtime/native/execution.d.ts +45 -0
- package/dist/runtime/native/execution.js +337 -0
- package/dist/runtime/native/identity.d.ts +9 -0
- package/dist/runtime/native/identity.js +15 -0
- package/dist/runtime/native/ingest.d.ts +241 -0
- package/dist/runtime/native/ingest.js +823 -0
- package/dist/runtime/native/install.d.ts +9 -0
- package/dist/runtime/native/install.js +105 -0
- package/dist/runtime/native/pi-extension.d.ts +35 -0
- package/dist/runtime/native/pi-extension.js +160 -0
- package/dist/runtime/native/pi-link.d.ts +67 -0
- package/dist/runtime/native/pi-link.js +238 -0
- package/dist/runtime/native/results.d.ts +41 -0
- package/dist/runtime/native/results.js +128 -0
- package/dist/runtime/native/sessions.d.ts +45 -0
- package/dist/runtime/native/sessions.js +92 -0
- package/dist/runtime/{native-transcript.d.ts → native/transcript.d.ts} +5 -5
- package/dist/runtime/{native-transcript.js → native/transcript.js} +37 -11
- package/dist/runtime/native/translate.d.ts +44 -0
- package/dist/runtime/native/translate.js +263 -0
- package/dist/runtime/native/transport.d.ts +7 -0
- package/dist/runtime/native/transport.js +16 -0
- package/dist/runtime/native/version.d.ts +5 -0
- package/dist/runtime/native/version.js +18 -0
- package/dist/runtime/presentation.d.ts +7 -10
- package/dist/runtime/presentation.js +35 -10
- package/dist/runtime/projection-store.d.ts +6 -13
- package/dist/runtime/projection-store.js +11 -74
- package/dist/runtime/supervisor.d.ts +2 -0
- package/dist/runtime/supervisor.js +36 -110
- package/dist/runtime/turn-projection.d.ts +1 -3
- package/dist/runtime/turn-projection.js +3 -10
- package/package.json +4 -5
- package/dist/runtime/native-attach.d.ts +0 -28
- package/dist/runtime/native-attach.js +0 -47
- package/dist/runtime/native-codex-hook.d.ts +0 -1
- package/dist/runtime/native-codex-hook.js +0 -28
- package/dist/runtime/native-install.d.ts +0 -21
- package/dist/runtime/native-install.js +0 -130
- package/dist/runtime/native-ipc.d.ts +0 -37
- package/dist/runtime/native-ipc.js +0 -118
- package/dist/runtime/native-pi-extension.d.ts +0 -20
- package/dist/runtime/native-pi-extension.js +0 -47
- package/dist/runtime/native-sync-store.d.ts +0 -116
- package/dist/runtime/native-sync-store.js +0 -495
- package/dist/runtime/native-sync.d.ts +0 -65
- package/dist/runtime/native-sync.js +0 -326
- package/dist/runtime/session-store.d.ts +0 -84
- 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 {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
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
|
|
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:
|
|
37
|
+
root: stateRoot, spaceId: config.spaceId, runtimeId: randomUUID(),
|
|
36
38
|
onEvent: (event) => { consoleSink?.(event); onDiagnostic?.(event); },
|
|
37
39
|
});
|
|
38
|
-
|
|
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
|
|
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),
|
|
88
|
+
closeInstance = await ownRuntimeInstance(runtimeInstanceDirectory(config.identity, config.spaceId), summary, async (force) => {
|
|
81
89
|
if (!force)
|
|
82
|
-
for await (const batch of
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
let
|
|
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
|
-
|
|
236
|
+
runtimeId: status.runtimeId, diagnostics, token, signal, executor: native.executor,
|
|
319
237
|
onReady: () => update({ harnessConnected: true }),
|
|
320
|
-
onDisconnected: () => {
|
|
321
|
-
|
|
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
|
|
335
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
119
|
-
|
|
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.
|
|
3
|
+
"version": "8.3.0",
|
|
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
|
-
"
|
|
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 {};
|