@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 +21 -0
- package/dist/commands/apps.js +13 -1
- package/dist/commands/runtime.d.ts +4 -0
- package/dist/commands/runtime.js +117 -0
- package/dist/commands/sandboxd-binary.js +2 -2
- package/dist/commands/spaces.js +3 -0
- package/dist/index.js +3 -3
- package/dist/runtime/archive-store.d.ts +44 -0
- package/dist/runtime/archive-store.js +353 -0
- package/dist/runtime/codex-usage.d.ts +9 -0
- package/dist/runtime/codex-usage.js +23 -0
- package/dist/runtime/connection.d.ts +16 -0
- package/dist/runtime/connection.js +318 -0
- package/dist/runtime/harness.d.ts +19 -0
- package/dist/runtime/harness.js +403 -0
- package/dist/runtime/json-rpc.d.ts +37 -0
- package/dist/runtime/json-rpc.js +165 -0
- package/dist/runtime/model-catalog.d.ts +6 -0
- package/dist/runtime/model-catalog.js +19 -0
- package/dist/runtime/native-archive.d.ts +16 -0
- package/dist/runtime/native-archive.js +88 -0
- package/dist/runtime/process-group.d.ts +3 -0
- package/dist/runtime/process-group.js +68 -0
- package/dist/runtime/session-store.d.ts +46 -0
- package/dist/runtime/session-store.js +273 -0
- package/package.json +11 -4
- package/dist/commands/sandbox.d.ts +0 -3
- package/dist/commands/sandbox.js +0 -175
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.
|
package/dist/commands/apps.js
CHANGED
|
@@ -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", "
|
|
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 `
|
|
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 `
|
|
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 });
|
package/dist/commands/spaces.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|