@neta-art/cohub-cli 9.1.2 → 10.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/dist/board-export.d.ts +14 -0
- package/dist/board-export.js +136 -80
- package/dist/board-kernel/lexer.js +84 -0
- package/dist/board-kernel.d.ts +4 -2
- package/dist/board-kernel.js +2913 -803
- package/dist/board-time.js +6 -0
- package/dist/commands/boards/authoring.d.ts +2 -0
- package/dist/commands/boards/authoring.js +90 -0
- package/dist/commands/boards/examples.js +12 -7
- package/dist/commands/boards.js +46 -11
- package/dist/commands/rooms.d.ts +3 -0
- package/dist/commands/rooms.js +232 -0
- package/dist/index.js +3 -0
- package/dist/runtime/model-catalog.d.ts +1 -1
- package/dist/runtime/model-catalog.js +2 -4
- package/dist/safe-remote-image.d.ts +4 -0
- package/dist/safe-remote-image.js +30 -15
- package/package.json +3 -3
package/dist/board-time.js
CHANGED
|
@@ -6,6 +6,12 @@ export function parseBoardTime(value, name = "time") {
|
|
|
6
6
|
return Math.round(Number(match[1]) * (UNITS[(match[2] ?? "").toLowerCase()] ?? 1) * 1000) / 1000;
|
|
7
7
|
}
|
|
8
8
|
export function parseBoardTimes(value, name = "--at", limit = 10_000) {
|
|
9
|
+
if (value.includes(",")) {
|
|
10
|
+
const entries = value.split(",");
|
|
11
|
+
if (entries.length > limit)
|
|
12
|
+
throw new Error(`${name}: frame count exceeds the limit of ${limit}`);
|
|
13
|
+
return entries.map((entry) => parseBoardTime(entry, name));
|
|
14
|
+
}
|
|
9
15
|
const parts = value.split(":");
|
|
10
16
|
if (parts.length === 1)
|
|
11
17
|
return [parseBoardTime(value, name)];
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { readBoardJsonObject } from "../../board-command-support.js";
|
|
2
|
+
import { loadBoardDocument, resolveBundledFonts } from "../../board-export.js";
|
|
3
|
+
import { boardPathTrack, boardSequencePatch, buildBoardScene, createBoardAnimationSampler, createNodeBoardRenderer, inspectBoardScene, measureBoardText, } from "../../board-kernel.js";
|
|
4
|
+
import { parseBoardTimes } from "../../board-time.js";
|
|
5
|
+
import { handleHttp, json as outJson } from "../../output.js";
|
|
6
|
+
import { resolveSpace } from "../../space.js";
|
|
7
|
+
export function registerBoardAuthoringCommands(boards) {
|
|
8
|
+
boards
|
|
9
|
+
.command("inspect <board>")
|
|
10
|
+
.description("Measure items and report visible layout hints without changing the Board")
|
|
11
|
+
.option("--animation <id>", "Animation to sample")
|
|
12
|
+
.option("--at <times>", "Time, comma-separated times, or start:end:step", "0")
|
|
13
|
+
.option("--items <ids>", "Limit measurements to comma-separated item ids")
|
|
14
|
+
.option("--json", "Output as JSON")
|
|
15
|
+
.action(async (target, options) => {
|
|
16
|
+
try {
|
|
17
|
+
const { document } = await loadBoardDocument(await resolveSpace(boards), target);
|
|
18
|
+
const times = parseBoardTimes(options.at, "--at", 100);
|
|
19
|
+
const sampler = options.animation ? createBoardAnimationSampler(document, options.animation) : null;
|
|
20
|
+
const renderer = await createNodeBoardRenderer({ fonts: resolveBundledFonts() });
|
|
21
|
+
try {
|
|
22
|
+
const ids = options.items ? new Set(options.items.split(",")) : null;
|
|
23
|
+
outJson({
|
|
24
|
+
samples: times.map((time) => {
|
|
25
|
+
const scene = sampler?.at(time).scene ?? buildBoardScene(document);
|
|
26
|
+
return {
|
|
27
|
+
time,
|
|
28
|
+
diagnostics: inspectBoardScene(scene),
|
|
29
|
+
items: scene.items
|
|
30
|
+
.filter((item) => !ids || ids.has(item.id))
|
|
31
|
+
.map((item) => ({
|
|
32
|
+
id: item.id,
|
|
33
|
+
bounds: item.frame,
|
|
34
|
+
...(item.type === "text" ? { text: measureBoardText(item.props) } : {}),
|
|
35
|
+
})),
|
|
36
|
+
};
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
renderer.destroy();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch (cause) {
|
|
45
|
+
handleHttp(cause);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
boards
|
|
49
|
+
.command("sequence <board>")
|
|
50
|
+
.description("Compile chapters to an apply-ready animation patch; does not write")
|
|
51
|
+
.requiredOption("-i, --input <file>", "JSON with a chapters array; use - for stdin")
|
|
52
|
+
.requiredOption("--animation <id>", "Parent animation id; recompiling replaces its tracks")
|
|
53
|
+
.action(async (target, options) => {
|
|
54
|
+
try {
|
|
55
|
+
const input = await readBoardJsonObject(options.input);
|
|
56
|
+
if (!Array.isArray(input.chapters) ||
|
|
57
|
+
input.chapters.some((chapter) => !chapter || typeof chapter !== "object" || typeof chapter.animation !== "string"))
|
|
58
|
+
throw new Error("Input needs a chapters array with animation IDs.");
|
|
59
|
+
const { document } = await loadBoardDocument(await resolveSpace(boards), target);
|
|
60
|
+
outJson(boardSequencePatch(document, options.animation, input.chapters));
|
|
61
|
+
}
|
|
62
|
+
catch (cause) {
|
|
63
|
+
handleHttp(cause);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
boards
|
|
67
|
+
.command("path <target>")
|
|
68
|
+
.description("Sample a polyline into a position track; does not write")
|
|
69
|
+
.requiredOption("-i, --input <file>", "JSON with points, duration (ms), optional at, samples and orient")
|
|
70
|
+
.action(async (target, options) => {
|
|
71
|
+
try {
|
|
72
|
+
const input = await readBoardJsonObject(options.input);
|
|
73
|
+
if (!Array.isArray(input.points) || input.points.some((point) => !point || typeof point !== "object"))
|
|
74
|
+
throw new Error("Input needs a points array.");
|
|
75
|
+
if (typeof input.duration !== "number")
|
|
76
|
+
throw new Error("Input needs duration in milliseconds.");
|
|
77
|
+
if (input.orient !== undefined && typeof input.orient !== "boolean")
|
|
78
|
+
throw new Error("orient must be boolean.");
|
|
79
|
+
outJson(boardPathTrack(target, input.points, {
|
|
80
|
+
duration: input.duration,
|
|
81
|
+
...(input.at === undefined ? {} : { at: Number(input.at) }),
|
|
82
|
+
...(input.samples === undefined ? {} : { samples: Number(input.samples) }),
|
|
83
|
+
orient: input.orient,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
catch (cause) {
|
|
87
|
+
handleHttp(cause);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { json } from "../../output.js";
|
|
2
|
+
import { BOARD_SCRIPT_DRAWING_EXAMPLE } from "../../board-kernel.js";
|
|
2
3
|
/**
|
|
3
4
|
* Starter documents for `boards create -i` and patches for `boards apply -i`.
|
|
4
5
|
* Every example is a valid Board patch; `boards.test.ts` checks that.
|
|
@@ -64,14 +65,11 @@ export const BOARD_EXAMPLES = {
|
|
|
64
65
|
},
|
|
65
66
|
},
|
|
66
67
|
},
|
|
67
|
-
|
|
68
|
-
description: "
|
|
68
|
+
custom: {
|
|
69
|
+
description: "Custom Canvas content; use --module to print scripts/wave.js.",
|
|
69
70
|
value: {
|
|
70
71
|
items: {
|
|
71
|
-
wave: { type: "
|
|
72
|
-
},
|
|
73
|
-
animations: {
|
|
74
|
-
loop: { duration: 4000, play: "always", tracks: {} },
|
|
72
|
+
wave: { type: "custom", position: { x: 0, y: 0 }, size: { width: 640, height: 360 }, script: { src: "scripts/wave.js", params: { amplitude: 60 } } },
|
|
75
73
|
},
|
|
76
74
|
},
|
|
77
75
|
},
|
|
@@ -79,11 +77,18 @@ export const BOARD_EXAMPLES = {
|
|
|
79
77
|
export function registerBoardExampleCommands(boards) {
|
|
80
78
|
boards.command("examples [name]")
|
|
81
79
|
.description("Print a starter Board document or list the examples")
|
|
80
|
+
.option("--module", "Print the custom example's JavaScript module")
|
|
82
81
|
.addHelpText("after", `
|
|
83
82
|
Examples are valid input for create and apply:
|
|
84
83
|
cohub boards examples lesson > lesson.json
|
|
85
84
|
cohub boards create lesson.board -i lesson.json`)
|
|
86
|
-
.action((name) => {
|
|
85
|
+
.action((name, options) => {
|
|
86
|
+
if (options.module) {
|
|
87
|
+
if (name !== "custom")
|
|
88
|
+
throw new Error("--module requires the custom example.");
|
|
89
|
+
process.stdout.write(BOARD_SCRIPT_DRAWING_EXAMPLE);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
87
92
|
if (!name) {
|
|
88
93
|
for (const [key, example] of Object.entries(BOARD_EXAMPLES))
|
|
89
94
|
console.log(`${key.padEnd(10)} ${example.description}`);
|
package/dist/commands/boards.js
CHANGED
|
@@ -9,6 +9,7 @@ import { createClient, createRealtimeClient } from "../client.js";
|
|
|
9
9
|
import { error, handleHttp, json as outJson, jsonRequested, ok, table } from "../output.js";
|
|
10
10
|
import { resolveSpace } from "../space.js";
|
|
11
11
|
import { registerBoardExampleCommands } from "./boards/examples.js";
|
|
12
|
+
import { registerBoardAuthoringCommands } from "./boards/authoring.js";
|
|
12
13
|
export const parseJsonObject = parseBoardJsonObject;
|
|
13
14
|
function withJson(command) {
|
|
14
15
|
return command.option("--json", "Output as JSON");
|
|
@@ -86,7 +87,7 @@ function showPlayback(playback) {
|
|
|
86
87
|
}
|
|
87
88
|
/** One region flag at most: being told to pick beats guessing which one wins. */
|
|
88
89
|
function parseExportRegion(options) {
|
|
89
|
-
const chosen = [options.frame && "--frame", options.items && "--items", options.rect && "--rect"].filter(Boolean);
|
|
90
|
+
const chosen = [options.camera && "--camera", options.frame && "--frame", options.items && "--items", options.rect && "--rect"].filter(Boolean);
|
|
90
91
|
if (chosen.length > 1)
|
|
91
92
|
throw new Error(`Pick one region: ${chosen.join(", ")} cannot be combined.`);
|
|
92
93
|
if (options.frame)
|
|
@@ -137,7 +138,9 @@ function registerExportCommand(boards) {
|
|
|
137
138
|
withJson(boards.command("export <board>")
|
|
138
139
|
.description("Render a Board to an image, or an animation to a frame sequence")
|
|
139
140
|
.requiredOption("-o, --out <file>", "Output file; the extension picks the format. Sequences need %d, e.g. frames/%04d.png")
|
|
140
|
-
.option("--at <time>", "A moment
|
|
141
|
+
.option("--at <time>", "A moment, comma-separated moments, or start:end:step")
|
|
142
|
+
.option("--camera", "Follow the animation camera")
|
|
143
|
+
.option("--size <widthxheight>", "Camera viewport in pixels", "1600x900")
|
|
141
144
|
.option("--animation <id>", "Animation the --at times refer to")
|
|
142
145
|
.option("--frame <item-id>", "Export one frame as a page")
|
|
143
146
|
.option("--items <ids>", "Comma-separated item ids")
|
|
@@ -158,9 +161,25 @@ Examples:
|
|
|
158
161
|
cohub boards export plan.board --animation lecture --at 4.5s -o moment.png
|
|
159
162
|
cohub boards export plan.board --animation lecture --at 0:6s:40ms -o frames/%04d.png`)
|
|
160
163
|
.action(async (target, options) => {
|
|
164
|
+
const controller = new AbortController();
|
|
165
|
+
const cancel = () => controller.abort(new Error("Export cancelled."));
|
|
166
|
+
process.once("SIGINT", cancel);
|
|
161
167
|
try {
|
|
162
168
|
const out = options.out;
|
|
163
169
|
const times = options.at ? parseBoardTimes(options.at) : undefined;
|
|
170
|
+
let camera;
|
|
171
|
+
if (options.camera) {
|
|
172
|
+
if (!options.animation || !times)
|
|
173
|
+
throw new Error("--camera needs --animation and --at.");
|
|
174
|
+
const size = /^(\d+)x(\d+)$/.exec(options.size ?? "1600x900");
|
|
175
|
+
if (!size?.[1] || !size[2])
|
|
176
|
+
throw new Error("--size must be widthxheight, e.g. 1600x900.");
|
|
177
|
+
camera = { width: parseNumber(size[1], "width", { min: 1, max: 8192 }), height: parseNumber(size[2], "height", { min: 1, max: 8192 }) };
|
|
178
|
+
}
|
|
179
|
+
const progress = (completed, total) => {
|
|
180
|
+
if (!jsonRequested(options) && (completed === total || completed % 30 === 0))
|
|
181
|
+
process.stderr.write(`Rendered ${completed}/${total} frames\n`);
|
|
182
|
+
};
|
|
164
183
|
const pathVideoFormat = videoFormatFromPath(out);
|
|
165
184
|
const requestedFormat = options.format?.toLowerCase();
|
|
166
185
|
if (requestedFormat && ![...BOARD_EXPORT_FORMATS, ...BOARD_VIDEO_FORMATS].includes(requestedFormat))
|
|
@@ -183,6 +202,10 @@ Examples:
|
|
|
183
202
|
const result = await runBoardVideoExport({
|
|
184
203
|
spaceId: await resolveSpace(boards),
|
|
185
204
|
target,
|
|
205
|
+
camera,
|
|
206
|
+
onProgress: progress,
|
|
207
|
+
signal: controller.signal,
|
|
208
|
+
force: options.force,
|
|
186
209
|
region: parseExportRegion(options),
|
|
187
210
|
times,
|
|
188
211
|
...(options.animation ? { animation: options.animation } : {}),
|
|
@@ -203,10 +226,25 @@ Examples:
|
|
|
203
226
|
console.log(` ! ${warning}`);
|
|
204
227
|
return;
|
|
205
228
|
}
|
|
229
|
+
const written = [];
|
|
230
|
+
for (let index = 0; index < (times?.length ?? 1); index++) {
|
|
231
|
+
const path = sequence ? framePath(out, index) : out;
|
|
232
|
+
if (!options.force && existsSync(path))
|
|
233
|
+
throw new Error(`${path} already exists; use --force to replace it.`);
|
|
234
|
+
}
|
|
206
235
|
const { runBoardExport } = await import("../board-export.js");
|
|
207
236
|
const result = await runBoardExport({
|
|
208
237
|
spaceId: await resolveSpace(boards),
|
|
209
238
|
target,
|
|
239
|
+
camera,
|
|
240
|
+
onProgress: progress,
|
|
241
|
+
signal: controller.signal,
|
|
242
|
+
onFrame: async (frame, index) => {
|
|
243
|
+
const path = sequence ? framePath(out, index) : out;
|
|
244
|
+
await mkdir(dirname(path), { recursive: true });
|
|
245
|
+
await writeBoardOutput(path, frame.bytes, Boolean(options.force));
|
|
246
|
+
written.push({ path, time: frame.time, width: frame.width, height: frame.height, bytes: frame.bytes.length });
|
|
247
|
+
},
|
|
210
248
|
region: parseExportRegion(options),
|
|
211
249
|
...(times ? { times } : {}),
|
|
212
250
|
...(options.animation ? { animation: options.animation } : {}),
|
|
@@ -220,18 +258,11 @@ Examples:
|
|
|
220
258
|
});
|
|
221
259
|
if (!result)
|
|
222
260
|
return error("Nothing to export: the region contains no items.");
|
|
223
|
-
const written = [];
|
|
224
|
-
for (const [index, frame] of result.frames.entries()) {
|
|
225
|
-
const path = sequence ? framePath(out, index) : out;
|
|
226
|
-
await mkdir(dirname(path), { recursive: true });
|
|
227
|
-
await writeBoardOutput(path, frame.bytes, Boolean(options.force));
|
|
228
|
-
written.push({ path, time: frame.time, width: frame.width, height: frame.height, bytes: frame.bytes.length });
|
|
229
|
-
}
|
|
230
261
|
if (jsonRequested(options))
|
|
231
262
|
return outJson({ files: written, warnings: result.warnings });
|
|
232
|
-
const first =
|
|
263
|
+
const first = written[0];
|
|
233
264
|
if (first) {
|
|
234
|
-
const what = sequence ? `${written.length} frames` : (
|
|
265
|
+
const what = sequence ? `${written.length} frames` : parseExportFormat(options, out).toUpperCase();
|
|
235
266
|
ok(`Exported ${first.width}×${first.height} ${what} to ${sequence ? dirname(out) || "." : out}`);
|
|
236
267
|
}
|
|
237
268
|
for (const warning of result.warnings)
|
|
@@ -240,6 +271,9 @@ Examples:
|
|
|
240
271
|
catch (cause) {
|
|
241
272
|
handleHttp(cause);
|
|
242
273
|
}
|
|
274
|
+
finally {
|
|
275
|
+
process.removeListener("SIGINT", cancel);
|
|
276
|
+
}
|
|
243
277
|
});
|
|
244
278
|
}
|
|
245
279
|
// ─── Commands ────────────────────────────────────────────────────────────────
|
|
@@ -460,6 +494,7 @@ Presets: ${BOARD_PRESET_NAMES.join(", ")}
|
|
|
460
494
|
});
|
|
461
495
|
registerBoardExampleCommands(boards);
|
|
462
496
|
registerExportCommand(boards);
|
|
497
|
+
registerBoardAuthoringCommands(boards);
|
|
463
498
|
registerPlaybackCommands(boards);
|
|
464
499
|
withJson(boards.command("watch <board>")
|
|
465
500
|
.description("Stream Board changes and playback as they happen"))
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { getAppByRef } from "../app-ref.js";
|
|
2
|
+
import { createClient } from "../client.js";
|
|
3
|
+
import { error, handleHttp, json as outJson, jsonRequested, ok, table, truncateText } from "../output.js";
|
|
4
|
+
const DURATION_RE = /^(\d+)(ms|s|m|h|d)?$/i;
|
|
5
|
+
const DURATION_UNITS = { ms: 1, s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 };
|
|
6
|
+
export function parseDuration(name, value) {
|
|
7
|
+
const match = DURATION_RE.exec(value.trim());
|
|
8
|
+
if (!match)
|
|
9
|
+
return error(`Invalid ${name}`, "Use a duration such as 90s, 10m or 2h.");
|
|
10
|
+
return Number(match[1]) * (DURATION_UNITS[(match[2] ?? "s").toLowerCase()] ?? 1_000);
|
|
11
|
+
}
|
|
12
|
+
function seconds(name, value, range) {
|
|
13
|
+
const result = Math.round(parseDuration(name, value) / 1000);
|
|
14
|
+
if (result < range[0] || result > range[1]) {
|
|
15
|
+
return error(`Invalid ${name}`, `Use a duration from ${formatSeconds(range[0])} to ${formatSeconds(range[1])}.`);
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
const formatSeconds = (value) => (value % 3600 === 0 ? `${value / 3600}h` : value % 60 === 0 ? `${value / 60}m` : `${value}s`);
|
|
20
|
+
const MINUTE = 60;
|
|
21
|
+
const DAY = 24 * 60 * MINUTE;
|
|
22
|
+
function parseJson(name, value) {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(value);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return error(`Invalid ${name}`, `${name} must be JSON, e.g. '{"text":"hi"}'.`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function readStdin() {
|
|
31
|
+
let input = "";
|
|
32
|
+
for await (const chunk of process.stdin)
|
|
33
|
+
input += chunk;
|
|
34
|
+
return input;
|
|
35
|
+
}
|
|
36
|
+
function parsePresence(value) {
|
|
37
|
+
const presence = parseJson("presence", value);
|
|
38
|
+
if (presence === null || (typeof presence === "object" && !Array.isArray(presence)))
|
|
39
|
+
return presence;
|
|
40
|
+
return error("Invalid presence", "presence must be a JSON object, or null to clear it.");
|
|
41
|
+
}
|
|
42
|
+
const short = (participantId) => participantId.slice(0, 8);
|
|
43
|
+
function printMembers(view) {
|
|
44
|
+
table(view.members.map((member) => ({
|
|
45
|
+
member: `${short(member.participantId)}${member.participantId === view.participantId ? " (you)" : ""}`,
|
|
46
|
+
kind: member.kind ?? "user",
|
|
47
|
+
model: member.model?.id ?? "",
|
|
48
|
+
presence: member.presence ? truncateText(JSON.stringify(member.presence), 60) : "",
|
|
49
|
+
})), [
|
|
50
|
+
{ key: "member", label: "Member" },
|
|
51
|
+
{ key: "kind", label: "Kind" },
|
|
52
|
+
{ key: "model", label: "Model" },
|
|
53
|
+
{ key: "presence", label: "Presence" },
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
function printView(view) {
|
|
57
|
+
console.log(`Room ${view.room.code} · ${view.room.id}`);
|
|
58
|
+
console.log(`You are ${view.participantId} · sequence ${view.sequence} · expires ${view.room.expiresAt}\n`);
|
|
59
|
+
printMembers(view);
|
|
60
|
+
}
|
|
61
|
+
const formatEvent = (event) => `#${event.sequence} ${event.type} from ${event.self ? "you" : short(event.sender.participantId)} ${JSON.stringify(event.data)}`;
|
|
62
|
+
function printEvents(roomId, page, options) {
|
|
63
|
+
if (page.truncated)
|
|
64
|
+
console.log("Some earlier events are no longer in the room history.");
|
|
65
|
+
if (page.events.length === 0)
|
|
66
|
+
console.log("No new events.");
|
|
67
|
+
for (const event of page.events)
|
|
68
|
+
console.log(formatEvent(event));
|
|
69
|
+
const filter = options.types.map((type) => ` --type '${type}'`).join("");
|
|
70
|
+
const wait = options.wait ? ` --wait ${options.wait}` : "";
|
|
71
|
+
console.log(`\nNext: cohub rooms events ${roomId} --after ${page.cursor}${filter}${wait}`);
|
|
72
|
+
}
|
|
73
|
+
const collect = (value, previous = []) => [...previous, value];
|
|
74
|
+
async function withClient(run) {
|
|
75
|
+
try {
|
|
76
|
+
return await run(createClient());
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
return handleHttp(e);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export function registerRooms(program) {
|
|
83
|
+
const rooms = program
|
|
84
|
+
.command("rooms")
|
|
85
|
+
.description("Take part in App rooms: join, read and publish events, set presence")
|
|
86
|
+
.addHelpText("after", `
|
|
87
|
+
A room belongs to a published App. Join one with an invite from the App
|
|
88
|
+
(room.invite()), or create one for an App you manage. Your seat stays while
|
|
89
|
+
you use it and lapses after --idle without a call.
|
|
90
|
+
|
|
91
|
+
An Agent waits for the room in a background command and wakes when it returns:
|
|
92
|
+
cohub rooms join <invite> --presence '{"name":"Guide"}'
|
|
93
|
+
cohub rooms events <room> --type 'chat.*' --wait 10m
|
|
94
|
+
cohub rooms publish <room> chat.message '{"text":"Hello"}'
|
|
95
|
+
`);
|
|
96
|
+
rooms
|
|
97
|
+
.command("create")
|
|
98
|
+
.description("Create a room for an App you manage, with an invite and a link to it")
|
|
99
|
+
.requiredOption("--app <app>", "App: ID, URL, or username/space/app")
|
|
100
|
+
.option("--code <code>", "Room code people type or open with ?room=")
|
|
101
|
+
.option("--ttl <duration>", "Room lifetime, 1m to 24h (default 2h)")
|
|
102
|
+
.option("--max <count>", "Most members at once, 2 to 128 (default 16)")
|
|
103
|
+
.option("--seat-per-user", "Give each person one App seat across tabs")
|
|
104
|
+
.option("--json", "Output as JSON")
|
|
105
|
+
.action((opts) => withClient(async (client) => {
|
|
106
|
+
const app = await getAppByRef(client, opts.app);
|
|
107
|
+
const created = await client.rooms.create({
|
|
108
|
+
appId: app.app.id,
|
|
109
|
+
...(opts.code ? { code: opts.code } : {}),
|
|
110
|
+
...(opts.ttl ? { expiresInSeconds: seconds("ttl", opts.ttl, [MINUTE, DAY]) } : {}),
|
|
111
|
+
...(opts.max ? { maxParticipants: Number(opts.max) } : {}),
|
|
112
|
+
...(opts.seatPerUser ? { seatPerUser: true } : {}),
|
|
113
|
+
});
|
|
114
|
+
if (jsonRequested(opts))
|
|
115
|
+
return outJson(created);
|
|
116
|
+
console.log(`Room ${created.room.code} · ${created.room.id}`);
|
|
117
|
+
if (created.url)
|
|
118
|
+
console.log(`Open: ${created.url}`);
|
|
119
|
+
console.log(`Invite (until ${created.expiresAt}):\n${created.invite}\n`);
|
|
120
|
+
console.log(`Join: cohub rooms join <invite>`);
|
|
121
|
+
}));
|
|
122
|
+
rooms
|
|
123
|
+
.command("join <invite>")
|
|
124
|
+
.description("Take a seat in a room with an invite")
|
|
125
|
+
.option("--presence <json>", "What others see about you, e.g. '{\"name\":\"Guide\"}'")
|
|
126
|
+
.option("--idle <duration>", "Leave after this long without a call, 1m to 24h (default 30m)")
|
|
127
|
+
.option("--json", "Output as JSON")
|
|
128
|
+
.action((invite, opts) => withClient(async (client) => {
|
|
129
|
+
const view = await client.rooms.join({
|
|
130
|
+
invite,
|
|
131
|
+
...(opts.presence !== undefined ? { presence: parsePresence(opts.presence) } : {}),
|
|
132
|
+
...(opts.idle ? { idleSeconds: seconds("idle", opts.idle, [MINUTE, DAY]) } : {}),
|
|
133
|
+
});
|
|
134
|
+
if (jsonRequested(opts))
|
|
135
|
+
return outJson(view);
|
|
136
|
+
printView(view);
|
|
137
|
+
console.log(`\nWait for events: cohub rooms events ${view.room.id} --after ${view.sequence} --wait 10m`);
|
|
138
|
+
}));
|
|
139
|
+
rooms
|
|
140
|
+
.command("get <room>")
|
|
141
|
+
.description("Show the room, its members and your seat")
|
|
142
|
+
.option("--json", "Output as JSON")
|
|
143
|
+
.action((roomId, opts) => withClient(async (client) => {
|
|
144
|
+
const view = await client.rooms.get(roomId);
|
|
145
|
+
if (jsonRequested(opts))
|
|
146
|
+
return outJson(view);
|
|
147
|
+
printView(view);
|
|
148
|
+
}));
|
|
149
|
+
rooms
|
|
150
|
+
.command("events <room>")
|
|
151
|
+
.description("Read published events, oldest first; --wait holds until someone else publishes")
|
|
152
|
+
.option("--after <sequence>", "Only events after this sequence")
|
|
153
|
+
.option("--type <pattern>", "Event type to include, * matches any text; repeatable", collect)
|
|
154
|
+
.option("--wait <duration>", "Wait up to this long for a new event from someone else")
|
|
155
|
+
.option("--follow", "Keep printing events as they arrive")
|
|
156
|
+
.option("--limit <count>", "Most events to return")
|
|
157
|
+
.option("--json", "Output as JSON")
|
|
158
|
+
.action((roomId, opts) => withClient(async (client) => {
|
|
159
|
+
const types = opts.type ?? [];
|
|
160
|
+
const after = opts.after === undefined ? undefined : Number(opts.after);
|
|
161
|
+
if (after !== undefined && (!Number.isInteger(after) || after < 0))
|
|
162
|
+
return error("Invalid --after", "Use a sequence number.");
|
|
163
|
+
const limit = opts.limit === undefined ? undefined : Number(opts.limit);
|
|
164
|
+
if (opts.follow)
|
|
165
|
+
return follow(client, roomId, { after, types, json: jsonRequested(opts) });
|
|
166
|
+
const page = await client.rooms.events(roomId, {
|
|
167
|
+
after,
|
|
168
|
+
types,
|
|
169
|
+
...(limit ? { limit } : {}),
|
|
170
|
+
...(opts.wait ? { waitMs: parseDuration("wait", opts.wait) } : {}),
|
|
171
|
+
});
|
|
172
|
+
if (jsonRequested(opts))
|
|
173
|
+
return outJson(page);
|
|
174
|
+
printEvents(roomId, page, { types, wait: opts.wait });
|
|
175
|
+
}));
|
|
176
|
+
rooms
|
|
177
|
+
.command("publish <room> <type> [data]")
|
|
178
|
+
.description("Publish an event to everyone in the room; data is JSON, or - for stdin")
|
|
179
|
+
.option("--json", "Output as JSON")
|
|
180
|
+
.action((roomId, type, data, opts) => withClient(async (client) => {
|
|
181
|
+
const payload = data === undefined ? null : parseJson("data", data === "-" ? await readStdin() : data);
|
|
182
|
+
const result = await client.rooms.publish(roomId, type, payload);
|
|
183
|
+
if (jsonRequested(opts))
|
|
184
|
+
return outJson(result);
|
|
185
|
+
ok(`Published #${result.sequence} ${type}`);
|
|
186
|
+
}));
|
|
187
|
+
rooms
|
|
188
|
+
.command("presence <room> <json>")
|
|
189
|
+
.description("Set what others see about you; null clears it")
|
|
190
|
+
.option("--json", "Output as JSON")
|
|
191
|
+
.action((roomId, value, opts) => withClient(async (client) => {
|
|
192
|
+
const result = await client.rooms.setPresence(roomId, parsePresence(value));
|
|
193
|
+
if (jsonRequested(opts))
|
|
194
|
+
return outJson(result);
|
|
195
|
+
ok("Presence updated");
|
|
196
|
+
}));
|
|
197
|
+
rooms
|
|
198
|
+
.command("invite <room>")
|
|
199
|
+
.description("Create an invite to a room you are in")
|
|
200
|
+
.option("--ttl <duration>", "How long the invite admits people (default 10m)")
|
|
201
|
+
.option("--json", "Output as JSON")
|
|
202
|
+
.action((roomId, opts) => withClient(async (client) => {
|
|
203
|
+
const invite = await client.rooms.invite(roomId, opts.ttl ? { expiresInSeconds: seconds("ttl", opts.ttl, [MINUTE, DAY]) } : {});
|
|
204
|
+
if (jsonRequested(opts))
|
|
205
|
+
return outJson(invite);
|
|
206
|
+
console.log(`Invite (until ${invite.expiresAt}):\n${invite.invite}`);
|
|
207
|
+
}));
|
|
208
|
+
rooms
|
|
209
|
+
.command("leave <room>")
|
|
210
|
+
.description("Give up your seat")
|
|
211
|
+
.option("--json", "Output as JSON")
|
|
212
|
+
.action((roomId, opts) => withClient(async (client) => {
|
|
213
|
+
const result = await client.rooms.leave(roomId);
|
|
214
|
+
if (jsonRequested(opts))
|
|
215
|
+
return outJson(result);
|
|
216
|
+
ok(result.left ? "Left the room" : "You were not in this room");
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
async function follow(client, roomId, input) {
|
|
220
|
+
const controller = new AbortController();
|
|
221
|
+
process.once("SIGINT", () => controller.abort());
|
|
222
|
+
let after = input.after ?? (await client.rooms.get(roomId)).sequence;
|
|
223
|
+
while (!controller.signal.aborted) {
|
|
224
|
+
const page = await client.rooms.events(roomId, { after, types: input.types, waitMs: 25_000, signal: controller.signal })
|
|
225
|
+
.catch((e) => (controller.signal.aborted ? null : Promise.reject(e)));
|
|
226
|
+
if (!page)
|
|
227
|
+
break;
|
|
228
|
+
for (const event of page.events)
|
|
229
|
+
console.log(input.json ? JSON.stringify(event) : formatEvent(event));
|
|
230
|
+
after = page.cursor;
|
|
231
|
+
}
|
|
232
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,7 @@ 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";
|
|
24
|
+
import { registerRooms } from "./commands/rooms.js";
|
|
24
25
|
import { formatUnknownCommandError, resolveHelpPath } from "./help-path.js";
|
|
25
26
|
const VERSION = (() => {
|
|
26
27
|
try {
|
|
@@ -63,6 +64,7 @@ Common commands:
|
|
|
63
64
|
cohub -s <space-id> public upload ./dist demo
|
|
64
65
|
cohub -s <space-id> apps publish demo --file dist/index.html
|
|
65
66
|
cohub desktop open <app-id> --call selection.get
|
|
67
|
+
cohub rooms join <invite>
|
|
66
68
|
cohub -s <space-id> spaces commerce products list
|
|
67
69
|
cohub models ls
|
|
68
70
|
cohub models ls --model-type multimodal
|
|
@@ -100,6 +102,7 @@ registerReferrals(program);
|
|
|
100
102
|
registerTasks(program);
|
|
101
103
|
registerCronJobs(program);
|
|
102
104
|
registerApps(program);
|
|
105
|
+
registerRooms(program);
|
|
103
106
|
registerDesktop(program);
|
|
104
107
|
registerLegacyUi(program);
|
|
105
108
|
const argv = process.argv.slice(2);
|
|
@@ -9,6 +9,6 @@ type Model = RuntimeCapabilities["models"][number];
|
|
|
9
9
|
* to the picker's rule, and Cohub always sends the chosen level explicitly.
|
|
10
10
|
*/
|
|
11
11
|
export declare function piModelCatalog(entries: unknown[], state: JsonRecord): Model[];
|
|
12
|
-
/** Codex's
|
|
12
|
+
/** Use Codex's picker-visible model/list for every provider, including custom providers. */
|
|
13
13
|
export declare function codexModelCatalog(configResponse: JsonRecord, entries: unknown[]): Model[];
|
|
14
14
|
export {};
|
|
@@ -45,14 +45,12 @@ function codexThinking(model, configuredEffort) {
|
|
|
45
45
|
...(defaultThinkingLevel ? { defaultThinkingLevel } : {}),
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
-
/** Codex's
|
|
48
|
+
/** Use Codex's picker-visible model/list for every provider, including custom providers. */
|
|
49
49
|
export function codexModelCatalog(configResponse, entries) {
|
|
50
50
|
const config = record(configResponse.config);
|
|
51
51
|
const provider = typeof config.model_provider === "string" ? config.model_provider : "openai";
|
|
52
52
|
const configuredModel = typeof config.model === "string" ? config.model : null;
|
|
53
|
-
const
|
|
54
|
-
const catalog = provider !== "openai" && !customCatalog ? [] : entries;
|
|
55
|
-
const models = catalog.flatMap((value) => {
|
|
53
|
+
const models = entries.flatMap((value) => {
|
|
56
54
|
const model = record(value);
|
|
57
55
|
const id = typeof model.model === "string" ? model.model : typeof model.id === "string" ? model.id : null;
|
|
58
56
|
if (!id || model.hidden)
|
|
@@ -25,4 +25,8 @@ export declare function downloadPublicImage(input: string, options?: RemoteImage
|
|
|
25
25
|
bytes: Uint8Array;
|
|
26
26
|
mimeType: string;
|
|
27
27
|
}>;
|
|
28
|
+
export declare function downloadPublicAudio(input: string, options?: RemoteImageDownloadOptions): Promise<{
|
|
29
|
+
bytes: Uint8Array;
|
|
30
|
+
mimeType: string;
|
|
31
|
+
}>;
|
|
28
32
|
export {};
|