@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.
@@ -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
+ }
@@ -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")
@@ -1,4 +1,4 @@
1
- export declare const SANDBOXD_VERSION = "v2.61.1";
1
+ export declare const SANDBOXD_VERSION = "v2.67.0";
2
2
  export declare class SandboxdDownloadError extends Error {
3
3
  name: string;
4
4
  }
@@ -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.61.1";
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
@@ -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.1.0",
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.1.0",
22
- "@neta-art/generation": "^0.1.33",
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",