@neta-art/cohub-cli 8.0.1 → 8.1.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.
- package/README.md +7 -16
- package/dist/commands/app-commerce.js +2 -2
- package/dist/commands/me.js +2 -2
- package/dist/commands/references.js +2 -2
- package/dist/commands/runtime.d.ts +12 -0
- package/dist/commands/runtime.js +163 -52
- package/dist/commands/sandboxd-binary.d.ts +1 -1
- package/dist/commands/sandboxd-binary.js +4 -3
- package/dist/commands/search.js +2 -2
- package/dist/commands/space-commerce.js +2 -2
- package/dist/output.d.ts +9 -2
- package/dist/output.js +28 -4
- package/dist/runtime/diagnostics.js +8 -1
- package/dist/runtime/launch.js +8 -2
- package/dist/runtime/native-attach.d.ts +28 -0
- package/dist/runtime/native-attach.js +47 -0
- package/dist/runtime/native-codex-hook.js +1 -1
- package/dist/runtime/native-ipc.d.ts +12 -1
- package/dist/runtime/native-ipc.js +21 -4
- package/dist/runtime/native-pi-extension.js +9 -9
- package/dist/runtime/native-sync-store.d.ts +6 -1
- package/dist/runtime/native-sync-store.js +5 -2
- package/dist/runtime/native-sync.d.ts +26 -1
- package/dist/runtime/native-sync.js +107 -14
- package/dist/runtime/native-transcript.d.ts +8 -0
- package/dist/runtime/native-transcript.js +34 -5
- package/dist/runtime/presentation.d.ts +10 -1
- package/dist/runtime/presentation.js +16 -0
- package/dist/runtime/supervisor.js +60 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -85,26 +85,21 @@ COHUB_SPACE_ID=<spaceId> cohub spaces prompt "message" --json
|
|
|
85
85
|
Connect one local workspace to a Space and select Pi or Codex per turn.
|
|
86
86
|
|
|
87
87
|
To keep using native terminals with ordinary Cohub Chats / Turns:
|
|
88
|
-
使用原生终端,并同步到普通 Cohub Chat / Turn:
|
|
89
88
|
|
|
90
89
|
```bash
|
|
91
90
|
cohub runtime up -d --harness pi --harness codex
|
|
92
|
-
|
|
93
|
-
#
|
|
94
|
-
|
|
95
|
-
cohub runtime detach --harness pi --harness codex # Pause; retain data / 暂停,保留数据
|
|
91
|
+
# Answer the native sync consent (default yes), then reload Pi / restart Codex
|
|
92
|
+
# and approve its hook trust prompt.
|
|
93
|
+
cohub runtime detach --harness pi --harness codex # Pause; retain data
|
|
96
94
|
```
|
|
97
95
|
|
|
98
|
-
`
|
|
99
|
-
|
|
96
|
+
`up` installs native sync by default after one explicit consent (default yes); declining or a
|
|
97
|
+
capability failure keeps the Runtime running without it, and `up` is idempotent. The Runtime
|
|
98
|
+
Supervisor is the single local Daemon: Pi / Codex integrations use its private IPC,
|
|
100
99
|
while native Turn events use the existing Runtime WebSocket. Configuration is backed up, existing
|
|
101
100
|
Runtime bindings are reused, and competing continuations fork at complete Turns without blocking native work. Requires Pi 0.85.1+ or Codex with stable
|
|
102
101
|
Hooks enabled. User-level integrations remain inert outside opted-in directories. See
|
|
103
|
-
[boundaries and privacy](../../docs/local-runtime.md#native-clients
|
|
104
|
-
|
|
105
|
-
`attach` 明确授权上传当前项目打开的对话历史与原生归档;复用现有绑定,配置修改前备份。
|
|
106
|
-
冲突时按完整 Turn 分支,不阻塞本地执行。用户级集成在未授权目录中不会采集数据。
|
|
107
|
-
|
|
102
|
+
[boundaries and privacy](../../docs/local-runtime.md#native-clients).
|
|
108
103
|
|
|
109
104
|
```bash
|
|
110
105
|
cohub runtime up ./project --harness pi --harness codex
|
|
@@ -142,10 +137,6 @@ local process and remote component status. `down` retains all data and requires
|
|
|
142
137
|
when executions remain unconfirmed. `logs --level warn --follow` shows sensitive events;
|
|
143
138
|
foreground startup prints these automatically. Runtime commands never fall back to Home.
|
|
144
139
|
|
|
145
|
-
普通使用只需 `cohub runtime up`,首次提示新建,后续优先复用。`-n` 是 `--new` 的简写,
|
|
146
|
-
`--name` 单独指定名称。`-d` 后台运行并返回链接、PID 和日志位置;`down` 停止但保留所有数据。
|
|
147
|
-
断网及临时凭证错误自动重试;断连不代表任务已停止,也不会自动重跑工具。
|
|
148
|
-
|
|
149
140
|
## Chats and prompts
|
|
150
141
|
|
|
151
142
|
Use `spaces prompt` for immediate sends, delayed sends, one-time schedules, recurring schedules, new Chats, and existing Chats.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolveExecutionAppId, } from "@neta-art/cohub";
|
|
2
2
|
import { createClient } from "../client.js";
|
|
3
|
-
import { error, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
|
|
3
|
+
import { error, formatLocalDateTime, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
|
|
4
4
|
function collectOption(value, previous = []) {
|
|
5
5
|
return [...previous, value];
|
|
6
6
|
}
|
|
@@ -69,7 +69,7 @@ function printOrder(order) {
|
|
|
69
69
|
{ key: "status", label: "Status" },
|
|
70
70
|
{ key: "amount", label: "Amount" },
|
|
71
71
|
{ key: "paid", label: "Paid" },
|
|
72
|
-
{ key: "created", label: "Created" },
|
|
72
|
+
{ key: "created", label: "Created", format: formatLocalDateTime },
|
|
73
73
|
{ key: "paidAt", label: "Paid At" },
|
|
74
74
|
]);
|
|
75
75
|
}
|
package/dist/commands/me.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createClient } from "../client.js";
|
|
2
|
-
import { handleHttp, json as outJson, jsonRequested, table } from "../output.js";
|
|
2
|
+
import { formatLocalDateTime, handleHttp, json as outJson, jsonRequested, table } from "../output.js";
|
|
3
3
|
function parseInteger(value, name, min) {
|
|
4
4
|
const parsed = Number.parseInt(value, 10);
|
|
5
5
|
if (!Number.isSafeInteger(parsed) || parsed < min) {
|
|
@@ -70,7 +70,7 @@ export function registerMe(program) {
|
|
|
70
70
|
});
|
|
71
71
|
if (jsonRequested(opts))
|
|
72
72
|
return outJson(activity);
|
|
73
|
-
console.log(`\n ${activity.range.from} → ${activity.range.to}`);
|
|
73
|
+
console.log(`\n ${formatLocalDateTime(activity.range.from)} → ${formatLocalDateTime(activity.range.to)}`);
|
|
74
74
|
table([activity.summary], [
|
|
75
75
|
{ key: "totalTokens", label: "Tokens" },
|
|
76
76
|
{ key: "costTotal", label: "Cost ($)" },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isUuid, } from "@neta-art/cohub";
|
|
2
2
|
import { createClient } from "../client.js";
|
|
3
|
-
import { table, json as outJson, jsonRequested, error, handleHttp } from "../output.js";
|
|
3
|
+
import { formatLocalDateTime, table, json as outJson, jsonRequested, error, handleHttp } from "../output.js";
|
|
4
4
|
const RESOURCE_TYPES = new Set([
|
|
5
5
|
"turn",
|
|
6
6
|
"session",
|
|
@@ -104,7 +104,7 @@ Examples:
|
|
|
104
104
|
source: `${r.sourceType}:${r.sourceId.slice(0, 8)}`,
|
|
105
105
|
target: formatTarget(r.targetType, r.targetId),
|
|
106
106
|
count: r.count,
|
|
107
|
-
lastSeen: r.updatedAt.slice(0, 10),
|
|
107
|
+
lastSeen: formatLocalDateTime(r.updatedAt).slice(0, 10),
|
|
108
108
|
}));
|
|
109
109
|
table(rows, [
|
|
110
110
|
{ key: "kind", label: "Kind" },
|
|
@@ -1,3 +1,15 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
+
import { type NativeIpcResponse } from "../runtime/native-ipc.js";
|
|
2
3
|
export { resolveLocalSpaceName, parseRuntimeHarnesses } from "../runtime/launch.js";
|
|
4
|
+
export declare function applyNativeImportResponse(result: {
|
|
5
|
+
imported: number;
|
|
6
|
+
failed: Array<{
|
|
7
|
+
path: string;
|
|
8
|
+
message: string;
|
|
9
|
+
}>;
|
|
10
|
+
skipped: Array<{
|
|
11
|
+
path: string;
|
|
12
|
+
message: string;
|
|
13
|
+
}>;
|
|
14
|
+
}, path: string, response: NativeIpcResponse): void;
|
|
3
15
|
export declare function registerRuntime(program: Command): void;
|
package/dist/commands/runtime.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
-
import { createInterface } from "node:readline/promises";
|
|
3
2
|
import { createClient } from "../client.js";
|
|
4
3
|
import { json as outJson, jsonRequested } from "../output.js";
|
|
5
4
|
import { currentIdentityKey } from "../space.js";
|
|
6
5
|
import { resolveRuntimeTarget, runtimeUp, parseRuntimeHarnesses } from "../runtime/launch.js";
|
|
7
6
|
import { canonicalRuntimeRoot, getRuntimeSpaceBinding } from "../runtime/space-binding.js";
|
|
8
7
|
import { installNativeSync } from "../runtime/native-install.js";
|
|
8
|
+
import { discoverNativeImportCandidates, nativeRuntimeRoot, readNativeSyncConfig } from "../runtime/native-sync.js";
|
|
9
9
|
import { listNativeSyncStores } from "../runtime/native-sync-store.js";
|
|
10
|
+
import { requestNativeDaemon } from "../runtime/native-ipc.js";
|
|
10
11
|
import { requestRuntimeInstance, runtimeInstanceDirectory } from "../runtime/instance.js";
|
|
11
|
-
import { atLeastLevel, diagnosticLevels, formatDiagnostic, printRuntimeSummary } from "../runtime/presentation.js";
|
|
12
|
+
import { atLeastLevel, diagnosticLevels, formatDiagnostic, formatNativeSync, printRuntimeSummary } from "../runtime/presentation.js";
|
|
12
13
|
import { RuntimeSessionStore } from "../runtime/session-store.js";
|
|
13
14
|
import { readRuntimeDiagnosticEvents, RuntimeDiagnosticReader, runtimeDiagnosticsDirectory, serializeDiagnosticError } from "../runtime/diagnostics.js";
|
|
14
15
|
export { resolveLocalSpaceName, parseRuntimeHarnesses } from "../runtime/launch.js";
|
|
@@ -16,6 +17,16 @@ const reportFailure = (cause) => {
|
|
|
16
17
|
process.stderr.write(`Runtime failed: ${serializeDiagnosticError(cause).message}\n`);
|
|
17
18
|
process.exitCode = 1;
|
|
18
19
|
};
|
|
20
|
+
export function applyNativeImportResponse(result, path, response) {
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
if (response.skipped) {
|
|
23
|
+
result.skipped.push({ path, message: response.message });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
throw new Error(response.message);
|
|
27
|
+
}
|
|
28
|
+
result.imported += 1;
|
|
29
|
+
}
|
|
19
30
|
export function registerRuntime(program) {
|
|
20
31
|
const runtime = program.command("runtime").description("Connect a local workspace");
|
|
21
32
|
runtime.command("up [dir]")
|
|
@@ -27,7 +38,7 @@ export function registerRuntime(program) {
|
|
|
27
38
|
.option("--harness <name>", "Pi or Codex; repeatable", (value, previous) => [...previous, value], [])
|
|
28
39
|
.option("--pi <path>", "Pi executable")
|
|
29
40
|
.option("--codex <path>", "Codex executable")
|
|
30
|
-
.option("-y, --yes", "Accept defaults and authorize local execution")
|
|
41
|
+
.option("-y, --yes", "Accept defaults and authorize local execution and native sync")
|
|
31
42
|
.option("--verbose", "Show diagnostic details")
|
|
32
43
|
.option("--json", "JSON output")
|
|
33
44
|
.action(async (dir, options) => {
|
|
@@ -38,57 +49,156 @@ export function registerRuntime(program) {
|
|
|
38
49
|
reportFailure(cause);
|
|
39
50
|
}
|
|
40
51
|
});
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
52
|
+
runtime.command("detach")
|
|
53
|
+
.description("Pause native sync; retain all receipts")
|
|
54
|
+
.option("-s, --space <id>", "Target Space")
|
|
55
|
+
.option("--harness <name>", "Pi or Codex; repeatable", (value, previous) => [...previous, value], [])
|
|
56
|
+
.option("--json", "JSON output")
|
|
57
|
+
.action(async (options) => {
|
|
58
|
+
try {
|
|
59
|
+
const spaceId = await resolveRuntimeTarget(program, options.space);
|
|
60
|
+
const identity = currentIdentityKey();
|
|
61
|
+
if (!identity)
|
|
62
|
+
throw new Error("Sign in first");
|
|
63
|
+
const root = await canonicalRuntimeRoot(process.cwd());
|
|
64
|
+
if ((await getRuntimeSpaceBinding(root, identity))?.spaceId !== spaceId)
|
|
65
|
+
throw new Error("Bind this directory with runtime up first");
|
|
66
|
+
const instance = await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId));
|
|
67
|
+
const harnesses = parseRuntimeHarnesses(options.harness.length ? options.harness : instance?.harnesses ?? ["pi", "codex"]);
|
|
68
|
+
const result = await installNativeSync({ root, spaceId, identity, harnesses, disabled: true });
|
|
69
|
+
if (jsonRequested(options))
|
|
70
|
+
outJson({ ...result, enabled: false });
|
|
71
|
+
else
|
|
72
|
+
process.stdout.write("Native sync paused; all local records retained\n");
|
|
73
|
+
}
|
|
74
|
+
catch (cause) {
|
|
75
|
+
reportFailure(cause);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
runtime.command("import [dir]")
|
|
79
|
+
.description("Import local conversations")
|
|
80
|
+
.option("-s, --space <id>", "Target Space")
|
|
81
|
+
.option("--harness <name>", "Filter by harness; repeatable", (value, previous) => [...previous, value], [])
|
|
82
|
+
.option("--session <id>", "Filter by native session ID")
|
|
83
|
+
.option("--dry-run", "Preview without importing")
|
|
84
|
+
.option("-y, --yes", "Skip confirmation")
|
|
85
|
+
.option("--json", "Output as JSON")
|
|
86
|
+
.action(async (dir, options) => {
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
const stop = () => controller.abort();
|
|
89
|
+
process.once("SIGINT", stop);
|
|
90
|
+
process.once("SIGTERM", stop);
|
|
91
|
+
try {
|
|
92
|
+
const identity = currentIdentityKey();
|
|
93
|
+
if (!identity)
|
|
94
|
+
throw new Error("Sign in first");
|
|
95
|
+
const root = await canonicalRuntimeRoot(dir ?? process.cwd());
|
|
96
|
+
const spaceId = await resolveRuntimeTarget(program, options.space);
|
|
97
|
+
const binding = await getRuntimeSpaceBinding(root, identity);
|
|
98
|
+
if (!binding || binding.spaceId !== spaceId)
|
|
99
|
+
throw new Error("Bind this directory with runtime up first");
|
|
100
|
+
const local = await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId));
|
|
101
|
+
if (!local)
|
|
102
|
+
throw new Error("Local Runtime is not running");
|
|
103
|
+
const config = await readNativeSyncConfig(nativeRuntimeRoot(spaceId), identity);
|
|
104
|
+
if (!config || config.root !== root)
|
|
105
|
+
throw new Error("Native sync is not enabled for this directory");
|
|
106
|
+
const harnesses = parseRuntimeHarnesses(options.harness.length ? options.harness : config.harnesses);
|
|
107
|
+
let lastProgress = 0;
|
|
108
|
+
const discovered = await discoverNativeImportCandidates(root, harnesses, {
|
|
109
|
+
signal: controller.signal,
|
|
110
|
+
onProgress: ({ harness, scanned, candidates: found }) => {
|
|
111
|
+
if (jsonRequested(options) || scanned - lastProgress < 25)
|
|
112
|
+
return;
|
|
113
|
+
lastProgress = scanned;
|
|
114
|
+
process.stderr.write(`Scanned ${scanned} ${harness} transcript${scanned === 1 ? "" : "s"}; found ${found}\n`);
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
const candidates = discovered.candidates.filter((candidate) => !options.session || candidate.nativeSessionId === options.session);
|
|
118
|
+
const result = {
|
|
119
|
+
spaceId, root, harnesses, candidates, errors: discovered.errors,
|
|
120
|
+
imported: 0, failed: [], skipped: [], dryRun: Boolean(options.dryRun),
|
|
121
|
+
complete: discovered.errors.length === 0,
|
|
122
|
+
};
|
|
123
|
+
if (jsonRequested(options) && options.dryRun) {
|
|
124
|
+
result.complete = result.complete && result.failed.length === 0;
|
|
125
|
+
outJson(result);
|
|
126
|
+
if (!result.complete)
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!candidates.length) {
|
|
131
|
+
if (!result.complete)
|
|
132
|
+
process.exitCode = 1;
|
|
81
133
|
if (jsonRequested(options))
|
|
82
134
|
outJson(result);
|
|
83
135
|
else
|
|
84
|
-
process.stdout.write(
|
|
85
|
-
|
|
86
|
-
|
|
136
|
+
process.stdout.write("No existing native conversations found\n");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (options.dryRun) {
|
|
140
|
+
if (jsonRequested(options))
|
|
141
|
+
outJson(result);
|
|
142
|
+
else {
|
|
143
|
+
process.stdout.write(`Found ${candidates.length} native conversation${candidates.length === 1 ? "" : "s"}\n`);
|
|
144
|
+
for (const candidate of candidates)
|
|
145
|
+
process.stdout.write(` ${candidate.harness} ${candidate.nativeSessionId} ${candidate.turnCount} Turn${candidate.turnCount === 1 ? "" : "s"} ${candidate.path}\n`);
|
|
146
|
+
for (const error of discovered.errors)
|
|
147
|
+
process.stdout.write(`Skipped ${error.path}: ${error.message}\n`);
|
|
148
|
+
}
|
|
149
|
+
if (!result.complete)
|
|
150
|
+
process.exitCode = 1;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (!options.yes) {
|
|
154
|
+
if (!process.stdin.isTTY)
|
|
155
|
+
throw new Error("Use --yes in non-interactive mode");
|
|
156
|
+
const { createInterface } = await import("node:readline/promises");
|
|
157
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
158
|
+
try {
|
|
159
|
+
const answer = await rl.question(`Import ${candidates.length} native conversation${candidates.length === 1 ? "" : "s"} to Space ${spaceId}? [Y/n] `);
|
|
160
|
+
if (!/^(|y(es)?)$/i.test(answer.trim())) {
|
|
161
|
+
if (jsonRequested(options))
|
|
162
|
+
outJson({ ...result, cancelled: true });
|
|
163
|
+
else
|
|
164
|
+
process.stdout.write("Import cancelled\n");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
rl.close();
|
|
170
|
+
}
|
|
87
171
|
}
|
|
88
|
-
|
|
89
|
-
|
|
172
|
+
for (const candidate of candidates) {
|
|
173
|
+
try {
|
|
174
|
+
const response = await requestNativeDaemon({ harness: candidate.harness, cwd: root, path: candidate.path, nativeSessionId: candidate.nativeSessionId,
|
|
175
|
+
sessionStartedAt: candidate.sessionStartedAt, origin: "local_import", settled: true });
|
|
176
|
+
applyNativeImportResponse(result, candidate.path, response);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
result.failed.push({ path: candidate.path, message: error instanceof Error ? error.message : String(error) });
|
|
180
|
+
}
|
|
90
181
|
}
|
|
91
|
-
|
|
182
|
+
result.complete = result.complete && result.failed.length === 0;
|
|
183
|
+
if (jsonRequested(options))
|
|
184
|
+
outJson(result);
|
|
185
|
+
else {
|
|
186
|
+
process.stdout.write(`Imported ${result.imported}/${candidates.length} native conversation${candidates.length === 1 ? "" : "s"}\n`);
|
|
187
|
+
for (const error of [...discovered.errors, ...result.skipped, ...result.failed])
|
|
188
|
+
process.stdout.write(`Skipped ${error.path}: ${error.message}\n`);
|
|
189
|
+
process.stdout.write("Uploads continue in the local Runtime background\n");
|
|
190
|
+
}
|
|
191
|
+
if (!result.complete)
|
|
192
|
+
process.exitCode = 1;
|
|
193
|
+
}
|
|
194
|
+
catch (cause) {
|
|
195
|
+
reportFailure(cause);
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
process.removeListener("SIGINT", stop);
|
|
199
|
+
process.removeListener("SIGTERM", stop);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
92
202
|
runtime.command("status").description("Local and server status")
|
|
93
203
|
.option("-s, --space <id>", "Target Space")
|
|
94
204
|
.option("--json", "JSON output")
|
|
@@ -105,7 +215,9 @@ export function registerRuntime(program) {
|
|
|
105
215
|
identity ? listNativeSyncStores(store.root, spaceId, identity) : [],
|
|
106
216
|
]);
|
|
107
217
|
const nativeSessions = await Promise.all(nativeStores.map((native) => native.status()));
|
|
108
|
-
|
|
218
|
+
// A corrupt native sync config must never take down the whole status report.
|
|
219
|
+
const nativeSync = identity ? await readNativeSyncConfig(store.root, identity).then((config) => ({ config, error: null }), (error) => ({ config: null, error: serializeDiagnosticError(error).message })) : { config: null, error: null };
|
|
220
|
+
const result = { ...remote.value, spaceId, local, remote: remote.value, remoteError: remote.error, diagnosticsPath: runtimeDiagnosticsDirectory(store.root), pendingLocalArchives, failedLocalArchives, nativeSync: nativeSync.config, nativeSyncError: nativeSync.error, nativeSessions };
|
|
109
221
|
if (jsonRequested(options))
|
|
110
222
|
outJson(result);
|
|
111
223
|
else {
|
|
@@ -114,8 +226,7 @@ export function registerRuntime(program) {
|
|
|
114
226
|
else
|
|
115
227
|
process.stdout.write(`Local process Not running\nSpace ${spaceId}\nLogs ${result.diagnosticsPath}\n`);
|
|
116
228
|
process.stdout.write(`Server ${remote.error ? `Unknown — ${remote.error}` : remote.value?.online ? "Harness connected" : "Offline"}\nArchives ${pendingLocalArchives} pending · ${failedLocalArchives} failed\n`);
|
|
117
|
-
|
|
118
|
-
process.stdout.write(`Native chats ${nativeSessions.length} · ${nativeSessions.reduce((sum, session) => sum + session.pendingTurns, 0)} Turns pending · ${nativeSessions.reduce((sum, session) => sum + session.pendingArchives, 0)} archives pending\n`);
|
|
229
|
+
process.stdout.write(formatNativeSync(nativeSync.config, nativeSessions, nativeSync.error));
|
|
119
230
|
}
|
|
120
231
|
}
|
|
121
232
|
catch (cause) {
|
|
@@ -25,9 +25,10 @@ import { Readable } from "node:stream";
|
|
|
25
25
|
// v2.54.1 keeps the same wire protocol and adds relay diagnostics: data-channel
|
|
26
26
|
// pairing outlives the runner's dial timeout, dial failures distinguish a
|
|
27
27
|
// timeout from an explicit rejection, and teardown-time websocket write
|
|
28
|
-
// failures log at debug instead of warn.
|
|
29
|
-
//
|
|
30
|
-
|
|
28
|
+
// failures log at debug instead of warn. v2.55.0 republishes that compatible
|
|
29
|
+
// runner alongside the matching platform release; no new runner capability is
|
|
30
|
+
// required.
|
|
31
|
+
export const SANDBOXD_VERSION = "v2.55.0";
|
|
31
32
|
const BINARY_NAME = "cohub-sandboxd";
|
|
32
33
|
// Public CDN prefix hosting the release archives (the repo is private, so the
|
|
33
34
|
// GitHub Release assets are not publicly downloadable). Overridable for staging
|
package/dist/commands/search.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isUuid } from "@neta-art/cohub";
|
|
2
2
|
import { createClient } from "../client.js";
|
|
3
|
-
import { table, json as outJson, jsonRequested, error, handleHttp } from "../output.js";
|
|
3
|
+
import { formatLocalDateTime, table, json as outJson, jsonRequested, error, handleHttp } from "../output.js";
|
|
4
4
|
const DEFAULT_LIMIT = 20;
|
|
5
5
|
const MAX_TITLE_LENGTH = 72;
|
|
6
6
|
const MAX_CONTEXT_LENGTH = 42;
|
|
@@ -50,7 +50,7 @@ function rowsFor(items) {
|
|
|
50
50
|
title: truncate(item.title, MAX_TITLE_LENGTH),
|
|
51
51
|
context: truncate(contextFor(item), MAX_CONTEXT_LENGTH),
|
|
52
52
|
match: item.matchedField,
|
|
53
|
-
updated: item.updatedAt ? item.updatedAt.slice(0, 10) : "",
|
|
53
|
+
updated: item.updatedAt ? formatLocalDateTime(item.updatedAt).slice(0, 10) : "",
|
|
54
54
|
href: item.href,
|
|
55
55
|
}));
|
|
56
56
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createClient } from "../client.js";
|
|
2
|
-
import { error, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
|
|
2
|
+
import { error, formatLocalDateTime, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
|
|
3
3
|
import { resolveSpace } from "../space.js";
|
|
4
4
|
const PRODUCT_STATUSES = ["draft", "active"];
|
|
5
5
|
const PRODUCT_UPDATE_STATUSES = ["draft", "active", "archived"];
|
|
@@ -135,7 +135,7 @@ function printOrders(orders) {
|
|
|
135
135
|
{ key: "product", label: "Product" },
|
|
136
136
|
{ key: "status", label: "Status" },
|
|
137
137
|
{ key: "amount", label: "Amount" },
|
|
138
|
-
{ key: "created", label: "Created" },
|
|
138
|
+
{ key: "created", label: "Created", format: formatLocalDateTime },
|
|
139
139
|
]);
|
|
140
140
|
}
|
|
141
141
|
async function confirmDanger(opts, detail) {
|
package/dist/output.d.ts
CHANGED
|
@@ -10,11 +10,18 @@ export type Column = {
|
|
|
10
10
|
format?: (value: unknown, row: Row) => string;
|
|
11
11
|
};
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* Timestamp → local ISO-style date and time with the system's UTC offset.
|
|
14
|
+
*
|
|
15
|
+
* CLI tables are for people and follow the machine's local timezone. Structured
|
|
16
|
+
* output remains untouched so scripts continue to receive the server value.
|
|
17
|
+
*/
|
|
18
|
+
export declare function formatLocalDateTime(value: unknown): string;
|
|
19
|
+
/**
|
|
20
|
+
* Epoch milliseconds → local date and time.
|
|
14
21
|
*
|
|
15
22
|
* Absent values render empty rather than falling through `Number()` coercion:
|
|
16
23
|
* `null` and `""` both become 0 there, which would print a confident-looking
|
|
17
|
-
*
|
|
24
|
+
* Unix epoch for a timestamp the server never sent.
|
|
18
25
|
*/
|
|
19
26
|
export declare function formatEpochMs(value: unknown): string;
|
|
20
27
|
export declare function table(rows: Row[], columns: Column[]): void;
|
package/dist/output.js
CHANGED
|
@@ -1,11 +1,34 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
import { extractBillingPayload } from "@neta-art/cohub";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Timestamp → local ISO-style date and time with the system's UTC offset.
|
|
5
|
+
*
|
|
6
|
+
* CLI tables are for people and follow the machine's local timezone. Structured
|
|
7
|
+
* output remains untouched so scripts continue to receive the server value.
|
|
8
|
+
*/
|
|
9
|
+
export function formatLocalDateTime(value) {
|
|
10
|
+
const date = value instanceof Date
|
|
11
|
+
? value
|
|
12
|
+
: typeof value === "number"
|
|
13
|
+
? new Date(value)
|
|
14
|
+
: typeof value === "string" && value.trim() !== ""
|
|
15
|
+
? new Date(value)
|
|
16
|
+
: null;
|
|
17
|
+
if (!date || Number.isNaN(date.getTime()))
|
|
18
|
+
return "";
|
|
19
|
+
const pad = (part) => String(part).padStart(2, "0");
|
|
20
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
21
|
+
const offsetSign = offsetMinutes >= 0 ? "+" : "-";
|
|
22
|
+
const absoluteOffset = Math.abs(offsetMinutes);
|
|
23
|
+
const offset = `${offsetSign}${pad(Math.floor(absoluteOffset / 60))}:${pad(absoluteOffset % 60)}`;
|
|
24
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())} ${offset}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Epoch milliseconds → local date and time.
|
|
5
28
|
*
|
|
6
29
|
* Absent values render empty rather than falling through `Number()` coercion:
|
|
7
30
|
* `null` and `""` both become 0 there, which would print a confident-looking
|
|
8
|
-
*
|
|
31
|
+
* Unix epoch for a timestamp the server never sent.
|
|
9
32
|
*/
|
|
10
33
|
export function formatEpochMs(value) {
|
|
11
34
|
const ms = typeof value === "number"
|
|
@@ -15,13 +38,14 @@ export function formatEpochMs(value) {
|
|
|
15
38
|
: Number.NaN;
|
|
16
39
|
if (!Number.isFinite(ms))
|
|
17
40
|
return "";
|
|
18
|
-
|
|
19
|
-
return Number.isNaN(date.getTime()) ? "" : date.toISOString();
|
|
41
|
+
return formatLocalDateTime(ms);
|
|
20
42
|
}
|
|
21
43
|
function cellText(column, row) {
|
|
22
44
|
const raw = row[column.key];
|
|
23
45
|
if (column.format)
|
|
24
46
|
return column.format(raw, row);
|
|
47
|
+
if (/(?:At|Timestamp)$/.test(column.key))
|
|
48
|
+
return formatLocalDateTime(raw);
|
|
25
49
|
const v = raw ?? "";
|
|
26
50
|
return typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
27
51
|
}
|
|
@@ -21,6 +21,12 @@ function isRecord(value) {
|
|
|
21
21
|
function redactUrl(value) {
|
|
22
22
|
try {
|
|
23
23
|
const url = new URL(value);
|
|
24
|
+
// Redaction must not rewrite the string when there is nothing to hide;
|
|
25
|
+
// URL.toString() re-encodes (e.g. quotes from wrapped log lines) and
|
|
26
|
+
// makes healthy URLs look corrupted.
|
|
27
|
+
const sensitive = [...url.searchParams.keys()].some((key) => SENSITIVE_KEY.test(key));
|
|
28
|
+
if (!sensitive)
|
|
29
|
+
return value;
|
|
24
30
|
for (const key of [...url.searchParams.keys()]) {
|
|
25
31
|
if (SENSITIVE_KEY.test(key))
|
|
26
32
|
url.searchParams.set(key, "[REDACTED]");
|
|
@@ -37,7 +43,8 @@ export function redactDiagnosticString(value) {
|
|
|
37
43
|
: value;
|
|
38
44
|
return shortened
|
|
39
45
|
.replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [REDACTED]")
|
|
40
|
-
|
|
46
|
+
// Trailing quotes/brackets from wrapped log lines are not part of the URL.
|
|
47
|
+
.replace(/\b(rediss?|https?|wss?):\/\/[^\s"'<>]+/gi, (url) => redactUrl(url))
|
|
41
48
|
.replace(/\b(authorization|password|secret|token|access[_-]?key|refresh[_-]?token)([\s:=]+)([^\s,;]+)/gi, "$1$2[REDACTED]");
|
|
42
49
|
}
|
|
43
50
|
function redactValue(value, seen = new WeakSet(), depth = 0) {
|
package/dist/runtime/launch.js
CHANGED
|
@@ -10,6 +10,7 @@ import { createClient } from "../client.js";
|
|
|
10
10
|
import { currentIdentityKey, explicitSpace } from "../space.js";
|
|
11
11
|
import { canonicalRuntimeRoot, getRuntimeSpaceBinding, resolveRuntimeSpace } from "./space-binding.js";
|
|
12
12
|
import { requestRuntimeInstance, runtimeInstanceDirectory } from "./instance.js";
|
|
13
|
+
import { ensureNativeSync } from "./native-attach.js";
|
|
13
14
|
import { createDiagnosticConsole, printRuntimeSummary, runtimeWebUrl } from "./presentation.js";
|
|
14
15
|
import { runRuntime } from "./supervisor.js";
|
|
15
16
|
export const resolveLocalSpaceName = (root, name) => name?.trim() || basename(root) || "local-space";
|
|
@@ -90,6 +91,9 @@ export async function runtimeUp(program, dir, options) {
|
|
|
90
91
|
if (existing.root !== root || options.harness.length && [...existing.harnesses].sort().join() !== [...harnesses].sort().join() || options.pi || options.codex) {
|
|
91
92
|
throw new Error("Runtime is running with a different configuration. Use down first");
|
|
92
93
|
}
|
|
94
|
+
// `up` is the single idempotent entry: a reused instance adopts its running
|
|
95
|
+
// Harnesses (unless explicitly overridden) and completes native sync setup.
|
|
96
|
+
await ensureNativeSync({ root, spaceId: existing.spaceId, identity, harnesses: options.harness.length ? harnesses : parseRuntimeHarnesses(existing.harnesses), yes: options.yes, executables: { pi: options.pi, codex: options.codex } });
|
|
93
97
|
printRuntimeSummary(existing, options.json, true);
|
|
94
98
|
return;
|
|
95
99
|
}
|
|
@@ -122,8 +126,8 @@ export async function runtimeUp(program, dir, options) {
|
|
|
122
126
|
if (!requested && (!binding || createNew))
|
|
123
127
|
name = (await rl.question(`Space name [${name}]: `)).trim() || name;
|
|
124
128
|
process.stderr.write(`\nDirectory ${root}\n${requested ? `Space ${runtimeWebUrl(requested)}\n` : ""}`);
|
|
125
|
-
const answer = await rl.question("Collaborators can execute as your OS user, beyond this folder. Allow? [
|
|
126
|
-
if (
|
|
129
|
+
const answer = await rl.question("Collaborators can execute as your OS user, beyond this folder. Allow? [Y/n] ");
|
|
130
|
+
if (/^n(o)?$/i.test(answer.trim()))
|
|
127
131
|
return;
|
|
128
132
|
}
|
|
129
133
|
finally {
|
|
@@ -148,6 +152,8 @@ export async function runtimeUp(program, dir, options) {
|
|
|
148
152
|
throw new Error("Space does not have a local Runtime");
|
|
149
153
|
},
|
|
150
154
|
});
|
|
155
|
+
// Consent (or --yes) precedes any user-level integration install; failure never blocks startup.
|
|
156
|
+
await ensureNativeSync({ root, spaceId, identity, harnesses, yes: options.yes, executables: { pi: options.pi, codex: options.codex } });
|
|
151
157
|
const config = { spaceId, root, identity, harnesses, capabilities, executables: { pi: options.pi, codex: options.codex }, background: Boolean(options.detach), verbose: options.verbose };
|
|
152
158
|
const existing = await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId));
|
|
153
159
|
if (existing) {
|
|
@@ -0,0 +1,28 @@
|
|
|
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 {};
|