@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 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
- cohub runtime attach --harness pi --harness codex
93
- # Reload Pi / restart Codex and approve its hook trust prompt.
94
- # 重载 Pi / 重启 Codex,并审核 Hook 信任提示。
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
- `attach` explicitly authorizes this project's opened conversation history and raw native archives.
99
- The Runtime Supervisor is the single local Daemon: Pi / Codex integrations use its private IPC,
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
  }
@@ -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;
@@ -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
- for (const action of ["attach", "detach"])
42
- runtime.command(action)
43
- .description(action === "attach" ? "Sync native Pi / Codex Turns" : "Pause native sync; retain all receipts")
44
- .option("-s, --space <id>", "Target Space")
45
- .option("--harness <name>", "Pi or Codex; repeatable", (value, previous) => [...previous, value], [])
46
- .option("--pi <path>", "Pi executable for capability checks")
47
- .option("--codex <path>", "Codex executable for capability checks")
48
- .option("-y, --yes", "Authorize project conversation and native archive uploads")
49
- .option("--json", "JSON output")
50
- .action(async (options) => {
51
- try {
52
- const spaceId = await resolveRuntimeTarget(program, options.space);
53
- const identity = currentIdentityKey();
54
- if (!identity)
55
- throw new Error("Sign in first");
56
- const root = await canonicalRuntimeRoot(process.cwd());
57
- if (action === "attach" && (await getRuntimeSpaceBinding(root, identity))?.spaceId !== spaceId)
58
- throw new Error("Bind this directory with runtime up --space first");
59
- const instance = await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId));
60
- if (action === "attach" && (!instance || instance.root !== root))
61
- throw new Error("Start this directory's Runtime first: cohub runtime up -d");
62
- if (action === "attach" && !instance?.nativeSync)
63
- throw new Error("Restart the Runtime with this CLI before attaching");
64
- const harnesses = parseRuntimeHarnesses(options.harness.length ? options.harness : instance?.harnesses ?? ["pi", "codex"]);
65
- if (action === "attach" && harnesses.some((harness) => !instance?.harnesses.includes(harness)))
66
- throw new Error("Enable these Harnesses with runtime up first");
67
- if (action === "attach" && !options.yes) {
68
- if (!process.stdin.isTTY)
69
- throw new Error("Use --yes to authorize native sync");
70
- const rl = createInterface({ input: process.stdin, output: process.stderr });
71
- try {
72
- const answer = await rl.question(`Install user-level ${harnesses.join(" / ")} integration and upload this project's opened conversations, tool output and raw archives to this Space? History may contain secrets. [y/N] `);
73
- if (!/^y(es)?$/i.test(answer.trim()))
74
- return;
75
- }
76
- finally {
77
- rl.close();
78
- }
79
- }
80
- const result = await installNativeSync({ root, spaceId, identity, harnesses, disabled: action === "detach", executables: { pi: options.pi, codex: options.codex } });
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(action === "attach"
85
- ? `Native sync enabled. Reload Pi or restart Codex and review its hook trust prompt.\n${result.configPath}\n`
86
- : "Native sync paused; all local records retained\n");
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
- catch (cause) {
89
- reportFailure(cause);
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
- const result = { ...remote.value, spaceId, local, remote: remote.value, remoteError: remote.error, diagnosticsPath: runtimeDiagnosticsDirectory(store.root), pendingLocalArchives, failedLocalArchives, nativeSessions };
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
- if (nativeSessions.length)
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) {
@@ -1,4 +1,4 @@
1
- export declare const SANDBOXD_VERSION = "v2.54.1";
1
+ export declare const SANDBOXD_VERSION = "v2.55.0";
2
2
  export declare class SandboxdDownloadError extends Error {
3
3
  name: string;
4
4
  }
@@ -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. No new capability is required, so this
29
- // pin is a diagnostics upgrade only.
30
- export const SANDBOXD_VERSION = "v2.54.1";
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
@@ -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
- * Epoch milliseconds → ISO 8601, matching the ISO timestamps other tables print.
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
- * `1970-01-01T00:00:00.000Z` for a timestamp the server never sent.
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
- * Epoch milliseconds → ISO 8601, matching the ISO timestamps other tables print.
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
- * `1970-01-01T00:00:00.000Z` for a timestamp the server never sent.
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
- const date = new Date(ms);
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
- .replace(/\b(rediss?|https?|wss?):\/\/[^\s]+/gi, (url) => redactUrl(url))
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) {
@@ -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? [y/N] ");
126
- if (!/^y(es)?$/i.test(answer.trim()))
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 {};