@neta-art/cohub-cli 6.11.2 → 7.0.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/README.md CHANGED
@@ -79,6 +79,27 @@ cohub -s <spaceId> spaces prompt "message" --json
79
79
  COHUB_SPACE_ID=<spaceId> cohub spaces prompt "message" --json
80
80
  ```
81
81
 
82
+ ## Local Runtime / 本地 Runtime
83
+
84
+ Connect one local workspace to a Space and select Pi or Codex per turn.
85
+ 将一个本地工作区连接到 Space,每轮可选择 Pi 或 Codex。
86
+
87
+ ```bash
88
+ cohub runtime up ./project --harness pi --harness codex
89
+ cohub runtime up ./project --space <spaceId> --harness codex
90
+ cohub -s <spaceId> spaces prompt "Continue / 继续" --harness codex
91
+ cohub runtime status --space <spaceId>
92
+ ```
93
+
94
+ The Runtime uses WebSockets, native Pi RPC / Codex app-server, and existing cloud
95
+ streaming. Local executables and credentials are required. `sandbox up` has been
96
+ removed. Reconnection and result reconciliation are automatic; no recovery command is needed.
97
+ See [Runtime details](../../docs/local-runtime.md).
98
+
99
+ Runtime 使用 WebSocket、原生 Pi RPC / Codex app-server 和现有云端流式链路。
100
+ 需要本机已安装并登录对应程序。Runtime 断线重连和结果核对自动完成,无需恢复命令。
101
+ 仅在无法确定结果时,由 Space 页头提供异常确认。旧 `sandbox up` 命令已移除。
102
+
82
103
  ## Chats and prompts
83
104
 
84
105
  Use `spaces prompt` for immediate sends, delayed sends, one-time schedules, recurring schedules, new Chats, and existing Chats.
@@ -31,6 +31,15 @@ function parseJsonObject(value, name) {
31
31
  }
32
32
  return error(`Invalid ${name}`, `${name} must be a JSON object`);
33
33
  }
34
+ /** Human-readable version provenance: a session title, a channel, or blank. */
35
+ function formatVersionSource(value) {
36
+ if (!value || typeof value !== "object")
37
+ return "";
38
+ const source = value;
39
+ if (source?.session)
40
+ return source.session.title?.trim() || source.session.id;
41
+ return source?.via ? `via ${source.via}` : "";
42
+ }
34
43
  function parseJsonValue(value, name) {
35
44
  if (!value)
36
45
  return null;
@@ -499,7 +508,7 @@ export function registerApps(program) {
499
508
  .option("--app-scope <scope>", "Scope granted directly to the app runtime (space.view, session.view, file.view, file.edit, taskrun.view, session.prompt.readonly, session.prompt.fullaccess, command.execute)", collectOption, [])
500
509
  .option("--viewer-scope <scope>", "Deprecated: viewer grants are no longer gated by the app configuration", collectOption, [])
501
510
  .option("--clear-app-scopes", "Clear app runtime scopes")
502
- .option("--clear-viewer-scopes", "Clear viewer-requestable scopes")
511
+ .option("--clear-viewer-scopes", "Deprecated: clear legacy scope metadata / 已废弃:清除旧权限元数据")
503
512
  .option("--meta <json>", "App metadata as a JSON object")
504
513
  .option("--hide-cohub-bar", "Hide the Cohub footer bar on the public app page")
505
514
  .option("--show-cohub-bar", "Show the Cohub footer bar on the public app page")
@@ -599,6 +608,7 @@ export function registerApps(program) {
599
608
  { key: "id", label: "ID" },
600
609
  { key: "targetType", label: "Target" },
601
610
  { key: "targetRef", label: "Ref" },
611
+ { key: "source", label: "Source", format: formatVersionSource },
602
612
  { key: "createdAt", label: "Created" },
603
613
  ]);
604
614
  }
@@ -718,6 +728,7 @@ export function registerApps(program) {
718
728
  .description("Grant an app scopes as the current user")
719
729
  .requiredOption("--scope <scope>", "Scope to grant (repeatable)", collectOption, [])
720
730
  .option("--space <spaceId>", "Target space; defaults to the app's own space")
731
+ .option("--extend", "Add scopes without replacing active grants / 增加权限,保留有效授权")
721
732
  .option("--json", "Output as JSON")
722
733
  .action(async (appRef, opts) => {
723
734
  const client = createClient();
@@ -726,6 +737,7 @@ export function registerApps(program) {
726
737
  const result = await client.apps.authorize(detail.app.id, {
727
738
  scopes: opts.scope,
728
739
  ...(opts.space ? { spaceId: opts.space } : {}),
740
+ ...(opts.extend ? { scopeMode: "extend" } : {}),
729
741
  });
730
742
  if (jsonRequested(opts))
731
743
  return outJson(result);
@@ -0,0 +1,4 @@
1
+ import type { Command } from "commander";
2
+ export declare const resolveLocalSpaceName: (root: string, name?: string) => string;
3
+ export declare function parseRuntimeHarnesses(values: string[]): ("pi" | "codex")[];
4
+ export declare function registerRuntime(program: Command): void;
@@ -0,0 +1,117 @@
1
+ import { spawn } from "node:child_process";
2
+ import { stat } from "node:fs/promises";
3
+ import { basename, resolve } from "node:path";
4
+ import { createInterface } from "node:readline/promises";
5
+ import { isLocalHarness, resolveCohubEnvironment, resolveWebsocketUrl } from "@neta-art/cohub";
6
+ import { requireAccessToken } from "../auth.js";
7
+ import { createClient } from "../client.js";
8
+ import { error, json as outJson, jsonRequested } from "../output.js";
9
+ import { resolveSpace } from "../space.js";
10
+ import { discoverHarnesses } from "../runtime/harness.js";
11
+ import { serveRuntime } from "../runtime/connection.js";
12
+ import { RuntimeSessionStore } from "../runtime/session-store.js";
13
+ import { ensureSandboxdBinary } from "./sandboxd-binary.js";
14
+ export const resolveLocalSpaceName = (root, name) => name?.trim() || basename(root) || "local-space";
15
+ export function parseRuntimeHarnesses(values) {
16
+ const names = values.flatMap((value) => value.split(",")).map((name) => name.trim()).filter(Boolean);
17
+ if (names.some((name) => !isLocalHarness(name)))
18
+ throw new Error("Harness must be pi or codex / Harness 必须是 pi 或 codex");
19
+ return [...new Set(names.length ? names : ["pi"])];
20
+ }
21
+ export function registerRuntime(program) {
22
+ const runtime = program.command("runtime").description("Connect a local workspace / 连接本地工作区");
23
+ runtime.command("up [dir]")
24
+ .description("Connect local Harnesses and files / 连接本地 Harness 与文件")
25
+ .option("-s, --space <id>", "Target Space / 目标 Space")
26
+ .option("-n, --name <name>", "New Space name / 新 Space 名称")
27
+ .option("--harness <name>", "Pi or Codex; repeatable / Pi 或 Codex,可重复", (value, previous) => [...previous, value], [])
28
+ .option("--pi <path>", "Pi executable / Pi 可执行文件")
29
+ .option("--codex <path>", "Codex executable / Codex 可执行文件")
30
+ .option("-y, --yes", "Accept local execution access / 同意本机执行权限")
31
+ .option("--json", "JSON output / JSON 输出")
32
+ .action(async (dir, options) => {
33
+ const controller = new AbortController();
34
+ const stop = () => controller.abort();
35
+ process.once("SIGINT", stop);
36
+ process.once("SIGTERM", stop);
37
+ try {
38
+ const root = resolve(dir ?? process.cwd());
39
+ if (!(await stat(root)).isDirectory())
40
+ throw new Error("Workspace is not a directory / 工作区不是目录");
41
+ const harnesses = parseRuntimeHarnesses(options.harness);
42
+ if (!options.yes) {
43
+ if (!process.stdin.isTTY)
44
+ throw new Error("Use --yes to authorize local execution / 请使用 --yes 授权本机执行");
45
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
46
+ try {
47
+ const answer = await rl.question(`Connect ${root}? Space collaborators can run commands as your OS user, beyond this folder.\n连接此目录?Space 协作者可使用当前系统用户执行命令,权限不限于此目录。 [y/N] `);
48
+ if (!/^y(es)?$/i.test(answer.trim()))
49
+ return;
50
+ }
51
+ finally {
52
+ rl.close();
53
+ }
54
+ }
55
+ const capabilities = await discoverHarnesses(harnesses, options, root);
56
+ const client = createClient();
57
+ const requested = options.space?.trim() || program.opts().space?.trim();
58
+ const spaceId = requested || (await client.spaces.create({ name: resolveLocalSpaceName(root, options.name), config: { sandbox: { provider: "local" } } })).space.id;
59
+ const sandbox = (await client.space(spaceId).sandbox.get()).sandbox;
60
+ if (sandbox?.provider !== "local")
61
+ throw new Error("Space does not have a local Runtime / Space 不是本地 Runtime");
62
+ const binary = await ensureSandboxdBinary();
63
+ const wsBase = resolveWebsocketUrl({ url: process.env.COHUB_WS_URL });
64
+ const url = new URL(wsBase);
65
+ url.pathname = "/runtime/relay";
66
+ const relay = new URL(wsBase);
67
+ relay.pathname = "/sandbox/relay";
68
+ let bridge = null;
69
+ let bridgeClosed = Promise.resolve();
70
+ const token = await requireAccessToken();
71
+ try {
72
+ await serveRuntime({
73
+ spaceId, cwd: root, url: url.toString(), capabilities, harnesses: options,
74
+ token: requireAccessToken, signal: controller.signal, store: new RuntimeSessionStore(spaceId, undefined, client.space(spaceId)),
75
+ onReady: () => {
76
+ if (!bridge) {
77
+ bridge = spawn(binary, ["--local", "--space", spaceId, "--root", root, "--relay", process.env.COHUB_RELAY_URL?.trim() || relay.toString()], { stdio: ["ignore", "inherit", "inherit"], env: { ...process.env, COHUB_RELAY_TOKEN: token } });
78
+ bridgeClosed = new Promise((resolveClosed) => bridge?.once("close", () => resolveClosed()));
79
+ bridge.on("error", (cause) => { console.error(cause); controller.abort(); });
80
+ bridge.once("exit", () => controller.abort());
81
+ }
82
+ const webUrl = `${resolveCohubEnvironment() === "prod" ? "https://cohub.live" : "https://dev.cohub.live"}/spaces/${spaceId}`;
83
+ if (jsonRequested(options))
84
+ outJson({ spaceId, root, harnesses, url: webUrl });
85
+ else
86
+ console.error(`Runtime connected / Runtime 已连接: ${webUrl}`);
87
+ },
88
+ });
89
+ }
90
+ finally {
91
+ if (bridge) {
92
+ const child = bridge;
93
+ child.kill("SIGTERM");
94
+ const timeout = setTimeout(() => child.kill("SIGKILL"), 3000);
95
+ await bridgeClosed;
96
+ clearTimeout(timeout);
97
+ }
98
+ }
99
+ }
100
+ catch (cause) {
101
+ if (!controller.signal.aborted)
102
+ error("Runtime failed / Runtime 失败", cause instanceof Error ? cause.message : String(cause));
103
+ }
104
+ finally {
105
+ process.removeListener("SIGINT", stop);
106
+ process.removeListener("SIGTERM", stop);
107
+ }
108
+ });
109
+ runtime.command("status").description("Runtime status / Runtime 状态").option("-s, --space <id>", "Target Space / 目标 Space").action(async (options) => {
110
+ const spaceId = options.space?.trim() || await resolveSpace(program);
111
+ const { archives } = new RuntimeSessionStore(spaceId);
112
+ const [status, pendingLocalArchives, failedLocalArchives] = await Promise.all([
113
+ createClient().space(spaceId).getRuntime(), archives.pendingCount(), archives.failedCaptureCount(),
114
+ ]);
115
+ outJson({ ...status, pendingLocalArchives, failedLocalArchives });
116
+ });
117
+ }
@@ -12,7 +12,7 @@ import { Readable } from "node:stream";
12
12
  //
13
13
  // IMPORTANT: this must point at a tag whose CDN artifacts have already been
14
14
  // published by .github/workflows/sandbox-binaries-build.yml. Only bump it AFTER
15
- // that tag's publish-cdn job has succeeded, otherwise `sandbox up` 404s on the
15
+ // that tag's publish-cdn job has succeeded, otherwise `runtime up` 404s on the
16
16
  // default download.
17
17
  export const SANDBOXD_VERSION = "v1.82.4";
18
18
  const BINARY_NAME = "cohub-sandboxd";
@@ -117,7 +117,7 @@ const extractTarGz = (archivePath, cwd) => new Promise((res, rej) => {
117
117
  child.on("close", (code) => code === 0 ? res() : rej(new SandboxdDownloadError(`tar extraction failed: ${stderr.trim() || `exit ${code}`}`)));
118
118
  });
119
119
  // Cross-process lock via atomic mkdir, mirroring the CLI self-update lock so two
120
- // concurrent `sandbox up` invocations don't download the same archive twice.
120
+ // concurrent `runtime up` invocations don't download the same archive twice.
121
121
  const withLock = async (version, fn) => {
122
122
  const lockPath = join(cacheDir(version), ".download.lock");
123
123
  await mkdir(dirname(lockPath), { recursive: true });
@@ -282,6 +282,7 @@ async function sendPrompt(command, words, opts) {
282
282
  content: promptContent,
283
283
  model: opts.model,
284
284
  provider: opts.provider,
285
+ harness: opts.harness,
285
286
  thinkingLevel: thinkingLevel,
286
287
  accessMode: opts.readOnly ? "read_only" : "full_access",
287
288
  intent: opts.steer ? "steer" : undefined,
@@ -388,6 +389,7 @@ export function registerPrompt(program) {
388
389
  .option("--title <title>", "Title for a newly created session or schedule")
389
390
  .option("-m, --model <model>", "Model name")
390
391
  .option("-p, --provider <provider>", "Provider name")
392
+ .option("--harness <harness>", "Harness: cohub, pi, or codex", "cohub")
391
393
  .option("--thinking-level <level>", "Thinking level: off|minimal|low|medium|high|xhigh|max")
392
394
  .option("--read-only", "Use read-only tools")
393
395
  .option("--steer", "Interrupt the current turn and run immediately")
@@ -615,6 +617,7 @@ export function registerSpaces(program) {
615
617
  .command("prompt [content...]", { hidden: true })
616
618
  .alias("send")
617
619
  .description("Send or schedule a prompt in the target space")
620
+ .option("--harness <harness>", "Harness: cohub, pi, or codex", "cohub")
618
621
  .option("--session <id>", "Target session ID")
619
622
  .option("--title <title>", "Title for a newly created session or schedule")
620
623
  .option("-m, --model <model>", "Model name")
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ import { registerReferences } from "./commands/references.js";
17
17
  import { registerReferrals } from "./commands/referrals.js";
18
18
  import { registerPrompt, registerSpaces } from "./commands/spaces.js";
19
19
  import { maybeHandleRunCommand, printRunHelp } from "./commands/run.js";
20
- import { registerSandbox } from "./commands/sandbox.js";
20
+ import { registerRuntime } from "./commands/runtime.js";
21
21
  import { registerTasks } from "./commands/tasks.js";
22
22
  import { registerDesktop, registerLegacyUi } from "./commands/desktop.js";
23
23
  import { registerApps } from "./commands/apps.js";
@@ -53,7 +53,7 @@ Common commands:
53
53
  cohub prompt "Fix the failing tests"
54
54
  cohub completion "Summarize AGENTS.md" --system-prompt AGENTS.md --stream
55
55
  cohub run -- git status
56
- cohub sandbox up ./my-project
56
+ cohub runtime up ./my-project
57
57
  cohub search "release notes"
58
58
  cohub -s <space-id> boards inspect <board-id>
59
59
  cohub -s <space-id> spaces turns ls --author others
@@ -82,7 +82,7 @@ registerProfile(program);
82
82
  registerMe(program);
83
83
  registerPrompt(program);
84
84
  registerSpaces(program);
85
- registerSandbox(program);
85
+ registerRuntime(program);
86
86
  registerChannels(program);
87
87
  registerGenerations(program);
88
88
  registerModels(program);
@@ -0,0 +1,44 @@
1
+ import { type HarnessArchive, type HarnessArchiveIndex, type RuntimeArchivePage, type RuntimeArchiveUpload } from "@neta-art/cohub";
2
+ export type ArchiveTransport = {
3
+ fetchObject?: typeof fetch;
4
+ prepareRuntimeArchive(index: HarnessArchiveIndex, options?: {
5
+ signal?: AbortSignal;
6
+ }): Promise<{
7
+ uploads: RuntimeArchiveUpload[];
8
+ }>;
9
+ commitRuntimeArchive(index: HarnessArchiveIndex, options?: {
10
+ signal?: AbortSignal;
11
+ }): Promise<{
12
+ ready: true;
13
+ }>;
14
+ getRuntimeArchive(sessionId: string, turnId: string, options?: {
15
+ signal?: AbortSignal;
16
+ }): Promise<RuntimeArchivePage>;
17
+ };
18
+ export declare function checksumNativeFile(path: string): Promise<string>;
19
+ export declare function atomicRuntimeJson(path: string, value: unknown): Promise<void>;
20
+ /** Immutable local segments are the outbox. Model acknowledgements never remove them. */
21
+ export declare class RuntimeArchiveStore {
22
+ readonly root: string;
23
+ private readonly transport?;
24
+ private flushing;
25
+ private readonly capturing;
26
+ constructor(root: string, transport?: ArchiveTransport | undefined);
27
+ pendingCount(): Promise<number>;
28
+ failedCaptureCount(): Promise<number>;
29
+ hasCapture(turnId: string): Promise<boolean>;
30
+ private version;
31
+ private blob;
32
+ private saveBlob;
33
+ private readIndex;
34
+ stage(state: {
35
+ sessionId: string;
36
+ harness: "pi" | "codex";
37
+ nativeSessionId: string;
38
+ path: string;
39
+ }, turnId: string): Promise<HarnessArchive>;
40
+ private capture;
41
+ flush(signal: AbortSignal): Promise<void>;
42
+ private drain;
43
+ restore(reference: HarnessArchive, target: string, signal?: AbortSignal): Promise<HarnessArchiveIndex>;
44
+ }