@neta-art/cohub-cli 10.1.0 → 10.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -4
- package/dist/board-kernel.js +2128 -1705
- package/dist/command-input.d.ts +4 -0
- package/dist/command-input.js +16 -0
- package/dist/commands/app-commerce.js +1 -3
- package/dist/commands/boards.js +17 -12
- package/dist/commands/decision.d.ts +4 -0
- package/dist/commands/decision.js +113 -0
- package/dist/commands/rooms-connect.d.ts +17 -0
- package/dist/commands/rooms-connect.js +140 -0
- package/dist/commands/rooms-rtc-binary.d.ts +2 -0
- package/dist/commands/rooms-rtc-binary.js +107 -0
- package/dist/commands/rooms-rtc.d.ts +21 -0
- package/dist/commands/rooms-rtc.js +171 -0
- package/dist/commands/rooms.js +45 -0
- package/dist/commands/sandboxd-binary.d.ts +1 -1
- package/dist/commands/sandboxd-binary.js +1 -1
- package/dist/commands/spaces.js +4 -15
- package/dist/index.js +2 -1
- package/package.json +4 -3
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { HttpError } from "@neta-art/cohub";
|
|
3
|
+
import { createRealtimeClient } from "../client.js";
|
|
4
|
+
import { readRoomLines } from "./rooms-connect.js";
|
|
5
|
+
import { ensureRtcBinary } from "./rooms-rtc-binary.js";
|
|
6
|
+
export async function runRoomRtc(roomId, options) {
|
|
7
|
+
if (options.delivery !== "reliable" && options.delivery !== "latest")
|
|
8
|
+
throw new Error("--delivery must be reliable or latest");
|
|
9
|
+
if (options.dataInput && !options.channel)
|
|
10
|
+
throw new Error("--data-input requires --channel");
|
|
11
|
+
if ([options.audio, options.video, options.dataInput].filter((value) => value === "-").length > 1)
|
|
12
|
+
throw new Error("Only one input can use stdin");
|
|
13
|
+
const maxBytes = Number(options.maxBytes);
|
|
14
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0)
|
|
15
|
+
throw new Error("--max-bytes must be a non-negative safe integer");
|
|
16
|
+
const client = createRealtimeClient();
|
|
17
|
+
const sessions = new Set();
|
|
18
|
+
const pending = new Set();
|
|
19
|
+
let room;
|
|
20
|
+
let helper;
|
|
21
|
+
let control;
|
|
22
|
+
let exit;
|
|
23
|
+
let killTimer;
|
|
24
|
+
let stopped = false;
|
|
25
|
+
let failure;
|
|
26
|
+
const stop = () => {
|
|
27
|
+
if (stopped)
|
|
28
|
+
return;
|
|
29
|
+
stopped = true;
|
|
30
|
+
helper?.kill("SIGTERM");
|
|
31
|
+
if (helper)
|
|
32
|
+
killTimer = setTimeout(() => helper?.kill("SIGKILL"), 5_000);
|
|
33
|
+
};
|
|
34
|
+
const fail = (cause) => { failure ??= cause; stop(); };
|
|
35
|
+
const send = (value) => {
|
|
36
|
+
if (!control || control.destroyed || stopped)
|
|
37
|
+
return;
|
|
38
|
+
const line = `${JSON.stringify(value)}\n`;
|
|
39
|
+
if (Buffer.byteLength(line) > 256 * 1024 || control.writableLength > 1024 * 1024)
|
|
40
|
+
throw new Error("RTC control input is too slow or too large");
|
|
41
|
+
control.write(line);
|
|
42
|
+
};
|
|
43
|
+
let previousMembers = "";
|
|
44
|
+
const report = (value) => {
|
|
45
|
+
const output = options.output === "-" ? process.stderr : process.stdout;
|
|
46
|
+
if (output.writableLength > 1024 * 1024)
|
|
47
|
+
throw new Error("RTC output is too slow");
|
|
48
|
+
output.write(`${JSON.stringify(value)}\n`);
|
|
49
|
+
};
|
|
50
|
+
const release = async (sessionId) => {
|
|
51
|
+
if (!sessions.has(sessionId))
|
|
52
|
+
return;
|
|
53
|
+
await client.rooms.signalRtc(roomId, { method: "DELETE", path: `/${sessionId}` });
|
|
54
|
+
sessions.delete(sessionId);
|
|
55
|
+
};
|
|
56
|
+
const request = async (message) => {
|
|
57
|
+
try {
|
|
58
|
+
if (!["GET", "POST", "PUT", "PATCH", "DELETE"].includes(message.method))
|
|
59
|
+
throw new Error("Invalid RTC signaling method");
|
|
60
|
+
const opening = message.method === "POST" && message.path === "";
|
|
61
|
+
const body = await client.rooms.signalRtc(roomId, {
|
|
62
|
+
method: message.method,
|
|
63
|
+
path: message.path,
|
|
64
|
+
body: opening ? { ticket: "hosted", participantId: room?.participantId } : message.method === "GET" ? undefined : message.body,
|
|
65
|
+
});
|
|
66
|
+
if (opening && body.sessionId)
|
|
67
|
+
sessions.add(body.sessionId);
|
|
68
|
+
if (message.method === "GET" && room) {
|
|
69
|
+
const snapshot = await client.rooms.get(roomId);
|
|
70
|
+
send({ kind: "members", body: snapshot.members.map(({ participantId, publications }) => ({ participantId, publications })) });
|
|
71
|
+
previousMembers = "";
|
|
72
|
+
}
|
|
73
|
+
send({ kind: "response", id: message.id, body });
|
|
74
|
+
}
|
|
75
|
+
catch (cause) {
|
|
76
|
+
send({ kind: "response", id: message.id, error: cause instanceof Error ? cause.message : String(cause), code: cause instanceof HttpError ? cause.code : "RTC_FAILED" });
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const track = (task) => {
|
|
80
|
+
pending.add(task);
|
|
81
|
+
void task.catch(fail).finally(() => pending.delete(task));
|
|
82
|
+
};
|
|
83
|
+
process.once("SIGINT", stop);
|
|
84
|
+
process.once("SIGTERM", stop);
|
|
85
|
+
process.stdout.on("error", fail);
|
|
86
|
+
process.stderr.on("error", fail);
|
|
87
|
+
try {
|
|
88
|
+
const binary = await ensureRtcBinary(options.helper);
|
|
89
|
+
if (stopped)
|
|
90
|
+
return;
|
|
91
|
+
room = await client.rooms.connect(roomId);
|
|
92
|
+
if (stopped)
|
|
93
|
+
return;
|
|
94
|
+
room.onStateChange((state) => {
|
|
95
|
+
try {
|
|
96
|
+
report({ kind: "room", state });
|
|
97
|
+
}
|
|
98
|
+
catch (cause) {
|
|
99
|
+
fail(cause);
|
|
100
|
+
}
|
|
101
|
+
if (state === "closed" || state === "expired")
|
|
102
|
+
stop();
|
|
103
|
+
});
|
|
104
|
+
helper = spawn(binary, [], { stdio: ["inherit", "inherit", "inherit", "pipe", "pipe"] });
|
|
105
|
+
control = helper.stdio[3];
|
|
106
|
+
const events = helper.stdio[4];
|
|
107
|
+
control.on("error", fail);
|
|
108
|
+
exit = new Promise((resolve, reject) => {
|
|
109
|
+
helper?.once("error", reject);
|
|
110
|
+
helper?.once("close", (code, signal) => {
|
|
111
|
+
if (killTimer)
|
|
112
|
+
clearTimeout(killTimer);
|
|
113
|
+
if (code === 0 || stopped && signal === "SIGTERM")
|
|
114
|
+
resolve();
|
|
115
|
+
else
|
|
116
|
+
reject(new Error(`RTC helper exited with ${signal ?? code}`));
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
void exit.catch(fail);
|
|
120
|
+
send({ protocol: 1, ...options, maxBytes, participantId: room.participantId });
|
|
121
|
+
const updateMembers = () => {
|
|
122
|
+
const body = room?.members.map(({ participantId, publications }) => ({ participantId, publications }));
|
|
123
|
+
const next = JSON.stringify(body);
|
|
124
|
+
if (next === previousMembers)
|
|
125
|
+
return;
|
|
126
|
+
send({ kind: "members", body });
|
|
127
|
+
previousMembers = next;
|
|
128
|
+
};
|
|
129
|
+
room.onMembersChanged(() => { try {
|
|
130
|
+
updateMembers();
|
|
131
|
+
}
|
|
132
|
+
catch (cause) {
|
|
133
|
+
fail(cause);
|
|
134
|
+
} });
|
|
135
|
+
updateMembers();
|
|
136
|
+
report({ kind: "connected", roomId, participantId: room.participantId });
|
|
137
|
+
for await (const line of readRoomLines(events, 256 * 1024)) {
|
|
138
|
+
const message = JSON.parse(line);
|
|
139
|
+
if (message.kind === "request") {
|
|
140
|
+
if (pending.size >= 16)
|
|
141
|
+
throw new Error("Too many pending RTC requests");
|
|
142
|
+
track(request(message));
|
|
143
|
+
}
|
|
144
|
+
else if (message.kind === "release") {
|
|
145
|
+
await release(message.sessionId);
|
|
146
|
+
}
|
|
147
|
+
else
|
|
148
|
+
report(message);
|
|
149
|
+
}
|
|
150
|
+
await exit;
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
stop();
|
|
154
|
+
await exit?.catch((cause) => { failure ??= cause; });
|
|
155
|
+
if (killTimer)
|
|
156
|
+
clearTimeout(killTimer);
|
|
157
|
+
await Promise.allSettled(pending);
|
|
158
|
+
const results = await Promise.allSettled([...sessions].map(release));
|
|
159
|
+
for (const result of results)
|
|
160
|
+
if (result.status === "rejected")
|
|
161
|
+
failure ??= result.reason;
|
|
162
|
+
await room?.disconnect().catch((cause) => { failure ??= cause; });
|
|
163
|
+
await client.disconnect().catch((cause) => { failure ??= cause; });
|
|
164
|
+
process.removeListener("SIGINT", stop);
|
|
165
|
+
process.removeListener("SIGTERM", stop);
|
|
166
|
+
process.stdout.removeListener("error", fail);
|
|
167
|
+
process.stderr.removeListener("error", fail);
|
|
168
|
+
}
|
|
169
|
+
if (failure)
|
|
170
|
+
throw failure;
|
|
171
|
+
}
|
package/dist/commands/rooms.js
CHANGED
|
@@ -136,6 +136,51 @@ An Agent waits for the room in a background command and wakes when it returns:
|
|
|
136
136
|
printView(view);
|
|
137
137
|
console.log(`\nWait for events: cohub rooms events ${view.room.id} --after ${view.sequence} --wait 10m`);
|
|
138
138
|
}));
|
|
139
|
+
rooms
|
|
140
|
+
.command("connect <room>")
|
|
141
|
+
.description("Stream room events and members over realtime; disconnecting keeps your seat")
|
|
142
|
+
.option("--after <sequence>", "Replay retained events after this sequence")
|
|
143
|
+
.option("--input", "Accept NDJSON publish/send/presence commands on stdin")
|
|
144
|
+
.option("--json", "Output newline-delimited JSON")
|
|
145
|
+
.action(async (roomId, opts) => {
|
|
146
|
+
try {
|
|
147
|
+
const { connectToRoom } = await import("./rooms-connect.js");
|
|
148
|
+
await connectToRoom(roomId, { ...opts, json: jsonRequested(opts) });
|
|
149
|
+
}
|
|
150
|
+
catch (cause) {
|
|
151
|
+
handleHttp(cause);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
rooms
|
|
155
|
+
.command("rtc <room>")
|
|
156
|
+
.description("Stream RTC media and data through the Go/Pion helper")
|
|
157
|
+
.option("--helper <path>", "Use a local cohub-rtc binary")
|
|
158
|
+
.option("--channel <name>", "Open a JSON/binary RTC data channel")
|
|
159
|
+
.option("--delivery <policy>", "Data channel delivery: reliable or latest", "reliable")
|
|
160
|
+
.option("--data-input <path>", "NDJSON channel input file or - for stdin")
|
|
161
|
+
.option("--audio <path>", "Audio input file or - for stdin")
|
|
162
|
+
.option("--audio-format <format>", "Audio input: pcm or opus (Ogg)", "pcm")
|
|
163
|
+
.option("--rate <hz>", "Audio sample rate", "48000")
|
|
164
|
+
.option("--channels <count>", "Audio channels", "1")
|
|
165
|
+
.option("--video <path>", "Video input file or - for stdin")
|
|
166
|
+
.option("--video-format <format>", "Video input: i420 or vp8 (IVF)", "i420")
|
|
167
|
+
.option("--size <widthxheight>", "Video frame dimensions")
|
|
168
|
+
.option("--fps <rate>", "Video frame rate", "30")
|
|
169
|
+
.option("--output <directory|->", "Receive framed media into unique .rtc files or binary stdout")
|
|
170
|
+
.option("--receive-format <format>", "Receive rtp packets or raw decoded frames", "rtp")
|
|
171
|
+
.option("--receive-size <widthxheight>", "Decoded I420 output dimensions", "640x360")
|
|
172
|
+
.option("--max-bytes <bytes>", "Stop after this many received bytes; 0 is unlimited", "1073741824")
|
|
173
|
+
.option("--participant <id>", "Only receive this participant's tracks")
|
|
174
|
+
.option("--json", "Output newline-delimited JSON")
|
|
175
|
+
.action(async (roomId, opts) => {
|
|
176
|
+
try {
|
|
177
|
+
const { runRoomRtc } = await import("./rooms-rtc.js");
|
|
178
|
+
await runRoomRtc(roomId, opts);
|
|
179
|
+
}
|
|
180
|
+
catch (cause) {
|
|
181
|
+
handleHttp(cause);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
139
184
|
rooms
|
|
140
185
|
.command("get <room>")
|
|
141
186
|
.description("Show the room, its members and your seat")
|
|
@@ -7,7 +7,7 @@ import { dirname, join } from "node:path";
|
|
|
7
7
|
import { pipeline } from "node:stream/promises";
|
|
8
8
|
import { Readable } from "node:stream";
|
|
9
9
|
// Pin only after all platform artifacts are published to the public CDN.
|
|
10
|
-
export const SANDBOXD_VERSION = "v2.
|
|
10
|
+
export const SANDBOXD_VERSION = "v2.67.0";
|
|
11
11
|
const BINARY_NAME = "cohub-sandboxd";
|
|
12
12
|
// Public CDN prefix hosting the release archives (the repo is private, so the
|
|
13
13
|
// GitHub Release assets are not publicly downloadable). Overridable for staging
|
package/dist/commands/spaces.js
CHANGED
|
@@ -3,10 +3,12 @@ import { basename, dirname, relative, resolve, sep } from "node:path";
|
|
|
3
3
|
import { readSentTurns, readSessionTurnOrigin, resolveCohubEnvironment } from "@neta-art/cohub";
|
|
4
4
|
import { uploadAvatarAsset, uploadChatImageAsset } from "../avatar.js";
|
|
5
5
|
import { createClient } from "../client.js";
|
|
6
|
+
import { collectOption, readPromptContent } from "../command-input.js";
|
|
6
7
|
import { table, json as outJson, jsonRequested, ok, error, handleHttp, formatEpochMs, truncateText } from "../output.js";
|
|
7
8
|
import { SESSION_STATUS_COLUMN } from "../session-status.js";
|
|
8
9
|
import { resolveSpace } from "../space.js";
|
|
9
10
|
import { uploadLocalFiles } from "../space-files/upload.js";
|
|
11
|
+
import { registerDecisionCommand } from "./decision.js";
|
|
10
12
|
import { registerSpaceCommerce } from "./space-commerce.js";
|
|
11
13
|
import { registerSpaceFileTransfer } from "./space-files.js";
|
|
12
14
|
import { registerSpaceActivity } from "./space-activity.js";
|
|
@@ -33,9 +35,6 @@ function parseInteger(value, name, options = {}) {
|
|
|
33
35
|
return error(`Invalid ${name}`, `${name} must be at most ${options.max}`);
|
|
34
36
|
return parsed;
|
|
35
37
|
}
|
|
36
|
-
function collectOption(value, previous = []) {
|
|
37
|
-
return [...previous, value];
|
|
38
|
-
}
|
|
39
38
|
function parseEnvOptions(values) {
|
|
40
39
|
if (!values?.length)
|
|
41
40
|
return undefined;
|
|
@@ -181,18 +180,6 @@ async function confirmRestart(opts) {
|
|
|
181
180
|
if (answer !== "y" && answer !== "yes")
|
|
182
181
|
return error("Cancelled");
|
|
183
182
|
}
|
|
184
|
-
async function readPromptContent(words, options = {}) {
|
|
185
|
-
let content = words.join(" ");
|
|
186
|
-
if (!content && !process.stdin.isTTY) {
|
|
187
|
-
const chunks = [];
|
|
188
|
-
for await (const chunk of process.stdin)
|
|
189
|
-
chunks.push(chunk);
|
|
190
|
-
content = Buffer.concat(chunks).toString().trim();
|
|
191
|
-
}
|
|
192
|
-
if (!content && !options.allowEmpty)
|
|
193
|
-
return error("No content", "Pass as argument or pipe via stdin");
|
|
194
|
-
return content;
|
|
195
|
-
}
|
|
196
183
|
async function sendPrompt(command, words, opts) {
|
|
197
184
|
const content = await readPromptContent(words, { allowEmpty: Boolean(opts.image?.length) });
|
|
198
185
|
const scheduleFlags = [opts.delayMs, opts.at, opts.cron].filter((value) => value !== undefined);
|
|
@@ -379,6 +366,7 @@ export function registerPrompt(program) {
|
|
|
379
366
|
.option("--thinking-level <level>", "Thinking level: off|minimal|low|medium|high|xhigh")
|
|
380
367
|
.option("--json", "Output as JSON")
|
|
381
368
|
.action((words, opts) => runCompletionCommand(program, words, opts));
|
|
369
|
+
registerDecisionCommand(program);
|
|
382
370
|
}
|
|
383
371
|
export function registerSpaceCreate(spacesCmd, dependencies = {}) {
|
|
384
372
|
const getClient = dependencies.createClient ?? createClient;
|
|
@@ -699,6 +687,7 @@ export function registerSpaces(program) {
|
|
|
699
687
|
.option("--thinking-level <level>", "Thinking level: off|minimal|low|medium|high|xhigh")
|
|
700
688
|
.option("--json", "Output as JSON")
|
|
701
689
|
.action((words, opts) => runCompletionCommand(spacesCmd, words, opts));
|
|
690
|
+
registerDecisionCommand(spacesCmd);
|
|
702
691
|
// ── spaces files ──
|
|
703
692
|
registerFiles(spacesCmd);
|
|
704
693
|
registerSpaceDisplays(spacesCmd);
|
package/dist/index.js
CHANGED
|
@@ -54,6 +54,7 @@ Common commands:
|
|
|
54
54
|
cohub spaces ls
|
|
55
55
|
cohub prompt "Fix the failing tests"
|
|
56
56
|
cohub completion "Summarize AGENTS.md" --system-prompt AGENTS.md --stream
|
|
57
|
+
cohub decision "I was charged twice" -q questions.json
|
|
57
58
|
cohub run -- git status
|
|
58
59
|
cohub runtime up ./my-project
|
|
59
60
|
cohub runtime logs --follow
|
|
@@ -74,7 +75,7 @@ Common commands:
|
|
|
74
75
|
Target space:
|
|
75
76
|
-s <space>, then COHUB_SPACE_ID, then the current directory Runtime binding.
|
|
76
77
|
<space> is an ID, a slug you own such as home, or username/slug.
|
|
77
|
-
prompt, completion, generate, apps, and public fall back to your Home Space;
|
|
78
|
+
prompt, completion, decision, generate, apps, and public fall back to your Home Space;
|
|
78
79
|
other Space commands need a target.
|
|
79
80
|
|
|
80
81
|
Environment:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neta-art/cohub-cli",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.3.0",
|
|
4
4
|
"description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"NOTICE"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@neta-art/cohub": "11.
|
|
22
|
-
"@neta-art/generation": "^0.1.
|
|
21
|
+
"@neta-art/cohub": "11.3.0",
|
|
22
|
+
"@neta-art/generation": "^0.1.35",
|
|
23
23
|
"commander": "^15.0.0",
|
|
24
24
|
"sharp": "^0.35.4",
|
|
25
25
|
"ws": "^8.21.3"
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "tsc -p tsconfig.build.json && tsdown",
|
|
44
44
|
"verify:sandboxd": "node --import tsx scripts/verify-sandboxd-release.ts",
|
|
45
|
+
"verify:rtc": "node --import tsx scripts/verify-rtc-release.ts",
|
|
45
46
|
"verify:board-kernel": "node --import tsx scripts/verify-board-kernel.ts",
|
|
46
47
|
"test": "node ../../scripts/test/run.mjs 'tests/**/*.test.ts'",
|
|
47
48
|
"test:runtime": "node --import tsx --test tests/runtime-*.integration.ts",
|