@neta-art/cohub-cli 10.2.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 +10 -2
- package/dist/board-kernel.js +102 -102
- package/dist/commands/boards.js +17 -12
- 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/package.json +3 -2
package/README.md
CHANGED
|
@@ -452,8 +452,16 @@ cohub apps resolve <appSlug> --owner <username> --space-slug <spaceSlug>
|
|
|
452
452
|
|
|
453
453
|
Use `--json` for machine-readable output. `apps get`, `apps stats`, and `apps download` also accept `app://<username>/<space>/<app>` mention URIs. `apps stats` reports total, 24-hour, 7-day, and 30-day views with a source breakdown. Download restores newly published file and directory artifacts directly from the CDN with checksum verification. HTML files with companion assets are restored as directory bundles; Board and port apps are not downloadable. The resolve command remains available for explicit slug-based lookup.
|
|
454
454
|
|
|
455
|
-
|
|
456
|
-
|
|
455
|
+
Agents can join hosted Rooms and attach realtime events or media:
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
cohub rooms join '<invite>'
|
|
459
|
+
cohub rooms connect <room-id> --json
|
|
460
|
+
cohub rooms rtc <room-id> --channel chat --data-input -
|
|
461
|
+
cohub rooms rtc <room-id> --audio speech.pcm --output ./received
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
RTC uses an on-demand Go/Pion helper, with external FFmpeg only when media conversion is needed. Encoded Ogg Opus and VP8 IVF inputs avoid re-encoding. Reception defaults to framed RTP archives; `--receive-format raw` decodes PCM/I420 for models. `--output -` writes binary frames to stdout and status to stderr. Recording is explicit, files are never overwritten, and the default receive quota is 1 GiB. Disconnecting keeps the hosted seat. See [Room RTC helper](../../apps/sandbox/cmd/cohub-rtc/README.md) for local builds, formats, migration details, and release prerequisites.
|
|
457
465
|
|
|
458
466
|
## Drive the Cohub UI
|
|
459
467
|
|
package/dist/board-kernel.js
CHANGED
|
@@ -5772,10 +5772,10 @@ const colorString$1 = string().max(120).refine(isBoardColorString$1, "expected a
|
|
|
5772
5772
|
const BoardColorSchema$1 = union([colorString$1, object({
|
|
5773
5773
|
light: colorString$1,
|
|
5774
5774
|
dark: colorString$1
|
|
5775
|
-
}).strict()]);
|
|
5775
|
+
}).strict()]).describe("Palette token (follows the theme), CSS color, or { light, dark }.");
|
|
5776
5776
|
const BoardStyleSchema$1 = object({
|
|
5777
5777
|
fill: BoardColorSchema$1.optional(),
|
|
5778
|
-
fillOpacity: ratio$1.optional(),
|
|
5778
|
+
fillOpacity: ratio$1.optional().describe("Fill opacity, independent of the item's opacity."),
|
|
5779
5779
|
stroke: BoardColorSchema$1.optional(),
|
|
5780
5780
|
strokeWidth: finite$1.min(0).max(64).optional(),
|
|
5781
5781
|
dash: _enum([
|
|
@@ -5783,8 +5783,8 @@ const BoardStyleSchema$1 = object({
|
|
|
5783
5783
|
"dashed",
|
|
5784
5784
|
"dotted"
|
|
5785
5785
|
]).optional(),
|
|
5786
|
-
radius: nonNegative$1.optional(),
|
|
5787
|
-
trim: ratio$1.optional()
|
|
5786
|
+
radius: nonNegative$1.optional().describe("Corner radius in board units."),
|
|
5787
|
+
trim: ratio$1.optional().describe("Fraction of the outline drawn; animate 0 → 1 to draw it in.")
|
|
5788
5788
|
}).strict();
|
|
5789
5789
|
function isSafeBoardSourcePath$1(value) {
|
|
5790
5790
|
return value.length > 0 && value.length <= 4096 && !value.startsWith("/") && !/^[a-z][a-z\d+.-]*:/i.test(value.trimStart()) && !value.includes("\\") && value.split("/").every((part) => part.length > 0 && part !== "." && part !== "..");
|
|
@@ -5800,18 +5800,18 @@ function parseBoardMediaSource$1(src) {
|
|
|
5800
5800
|
value: src
|
|
5801
5801
|
} : null;
|
|
5802
5802
|
}
|
|
5803
|
-
const BoardMediaSourceSchema$1 = string().max(4096).refine((src) => parseBoardMediaSource$1(src) !== null, "src must be a public HTTP(S) URL or relative Space file path").describe("Prefer a
|
|
5803
|
+
const BoardMediaSourceSchema$1 = string().max(4096).refine((src) => parseBoardMediaSource$1(src) !== null, "src must be a public HTTP(S) URL or relative Space file path").describe("Prefer a public HTTP(S) URL; a Space path relative to the workspace root also works.");
|
|
5804
5804
|
const BoardScriptSchema$1 = object({
|
|
5805
|
-
src: srcSchema$1.refine((path) => /\.m?js$/.test(path), "script src must be a .js or .mjs module"),
|
|
5806
|
-
params: record(string(), json()).default({}),
|
|
5807
|
-
targets: record(string().min(1).max(80), BoardIdSchema$1).default({}),
|
|
5808
|
-
seed: string().min(1).max(160).optional(),
|
|
5805
|
+
src: srcSchema$1.refine((path) => /\.m?js$/.test(path), "script src must be a .js or .mjs module").describe("A .js or .mjs Space file."),
|
|
5806
|
+
params: record(string(), json()).default({}).describe("Values the module reads as params."),
|
|
5807
|
+
targets: record(string().min(1).max(80), BoardIdSchema$1).default({}).describe("Named items the module can reach, as ctx.targets."),
|
|
5808
|
+
seed: string().min(1).max(160).optional().describe("Seed for ctx.random(); defaults to the item id."),
|
|
5809
5809
|
enabled: boolean().default(true)
|
|
5810
|
-
}).strict();
|
|
5810
|
+
}).strict().describe("A JavaScript module attached to the item; `cohub boards schema script` lists its API.");
|
|
5811
5811
|
const itemEnvelope$1 = {
|
|
5812
5812
|
script: BoardScriptSchema$1.optional(),
|
|
5813
|
-
parent: BoardIdSchema$1.optional(),
|
|
5814
|
-
z: finite$1.optional(),
|
|
5813
|
+
parent: BoardIdSchema$1.optional().describe("A frame id; position becomes relative to the frame, and the item moves with it."),
|
|
5814
|
+
z: finite$1.optional().describe("Order among siblings; new items go on top."),
|
|
5815
5815
|
position: BoardVec2Schema$1.default({
|
|
5816
5816
|
x: 0,
|
|
5817
5817
|
y: 0
|
|
@@ -5821,7 +5821,7 @@ const itemEnvelope$1 = {
|
|
|
5821
5821
|
origin: BoardVec2Schema$1.default({
|
|
5822
5822
|
x: .5,
|
|
5823
5823
|
y: .5
|
|
5824
|
-
}),
|
|
5824
|
+
}).describe("Rotation and scale origin as fractions of the size."),
|
|
5825
5825
|
opacity: ratio$1.default(1),
|
|
5826
5826
|
style: BoardStyleSchema$1.default({}),
|
|
5827
5827
|
locked: boolean().optional(),
|
|
@@ -5854,8 +5854,8 @@ const BoardFrameItemSchema$1 = object({
|
|
|
5854
5854
|
type: literal("frame"),
|
|
5855
5855
|
props: object({
|
|
5856
5856
|
label: string().max(280).default(""),
|
|
5857
|
-
decoration: _enum(["frame", "none"]).optional(),
|
|
5858
|
-
clip: boolean().default(true)
|
|
5857
|
+
decoration: _enum(["frame", "none"]).optional().describe("none hides the title, border and background: an invisible group whose tracks move every child."),
|
|
5858
|
+
clip: boolean().default(true).describe("Hide children outside the frame.")
|
|
5859
5859
|
}).strict().prefault({})
|
|
5860
5860
|
}).strict();
|
|
5861
5861
|
const BoardTextItemSchema$1 = object({
|
|
@@ -5865,12 +5865,12 @@ const BoardTextItemSchema$1 = object({
|
|
|
5865
5865
|
text: string().default(""),
|
|
5866
5866
|
fontSize: fontSize$1.default(24),
|
|
5867
5867
|
fontWeight: finite$1.min(100).max(900).default(500),
|
|
5868
|
-
font: string().min(1).max(120).default("sans"),
|
|
5868
|
+
font: string().min(1).max(120).default("sans").describe("sans, serif, mono, or a font family name."),
|
|
5869
5869
|
align: _enum(BOARD_TEXT_ALIGNS$1).default("left"),
|
|
5870
5870
|
lineHeight: finite$1.min(.5).max(4).default(4 / 3),
|
|
5871
|
-
width: finite$1.positive().optional(),
|
|
5872
|
-
reveal: ratio$1.default(1),
|
|
5873
|
-
caret: boolean().default(false)
|
|
5871
|
+
width: finite$1.positive().optional().describe("Wrap width; the height follows the text."),
|
|
5872
|
+
reveal: ratio$1.default(1).describe("Fraction of the text shown; animate 0 → 1 to type it."),
|
|
5873
|
+
caret: boolean().default(false).describe("Blink a caret while the text is being revealed.")
|
|
5874
5874
|
}).strict().prefault({})
|
|
5875
5875
|
}).strict();
|
|
5876
5876
|
const BoardShapeItemSchema$1 = object({
|
|
@@ -5878,7 +5878,7 @@ const BoardShapeItemSchema$1 = object({
|
|
|
5878
5878
|
type: literal("shape"),
|
|
5879
5879
|
props: object({
|
|
5880
5880
|
geometry: _enum(BOARD_SHAPE_GEOMETRIES$1).default("rectangle"),
|
|
5881
|
-
path: string().min(1).max(2e5).optional(),
|
|
5881
|
+
path: string().min(1).max(2e5).optional().describe("SVG path data for path geometry, fitted to the item size."),
|
|
5882
5882
|
text: string().default(""),
|
|
5883
5883
|
fontSize: fontSize$1.default(20),
|
|
5884
5884
|
align: _enum(BOARD_TEXT_ALIGNS$1).default("center")
|
|
@@ -5890,12 +5890,12 @@ const BoardShapeItemSchema$1 = object({
|
|
|
5890
5890
|
const BoardDrawPointSchema$1 = object({
|
|
5891
5891
|
x: finite$1,
|
|
5892
5892
|
y: finite$1,
|
|
5893
|
-
p: ratio$1.default(.5)
|
|
5893
|
+
p: ratio$1.default(.5).describe("Pen pressure.")
|
|
5894
5894
|
}).strict();
|
|
5895
5895
|
const BoardDrawItemSchema$1 = object({
|
|
5896
5896
|
...unsized$1,
|
|
5897
5897
|
type: literal("draw"),
|
|
5898
|
-
props: object({ points: array(BoardDrawPointSchema$1).min(1).max(1e5) }).strict()
|
|
5898
|
+
props: object({ points: array(BoardDrawPointSchema$1).min(1).max(1e5).describe("Stroke points relative to position; the size follows them.") }).strict()
|
|
5899
5899
|
}).strict();
|
|
5900
5900
|
const BoardArrowEndSchema$1 = union([BoardVec2Schema$1, object({
|
|
5901
5901
|
item: BoardIdSchema$1,
|
|
@@ -5914,9 +5914,9 @@ const BoardArrowEndSchema$1 = union([BoardVec2Schema$1, object({
|
|
|
5914
5914
|
x: ratio$1,
|
|
5915
5915
|
y: ratio$1
|
|
5916
5916
|
}).strict()
|
|
5917
|
-
]).default("auto"),
|
|
5917
|
+
]).default("auto").describe("auto uses the side facing the other end; or a side with an offset, or a point as fractions of the item."),
|
|
5918
5918
|
port: string().min(1).max(120).optional()
|
|
5919
|
-
}).strict()]);
|
|
5919
|
+
}).strict()]).describe("A point relative to position, or { item } to follow that item.");
|
|
5920
5920
|
const BOARD_ARROW_ROUTES$1 = [
|
|
5921
5921
|
"straight",
|
|
5922
5922
|
"curve",
|
|
@@ -5929,13 +5929,13 @@ const BoardArrowItemSchema$1 = object({
|
|
|
5929
5929
|
start: BoardArrowEndSchema$1,
|
|
5930
5930
|
end: BoardArrowEndSchema$1,
|
|
5931
5931
|
route: _enum(BOARD_ARROW_ROUTES$1).default("straight"),
|
|
5932
|
-
bend: finite$1.min(-.85).max(.85).default(0),
|
|
5933
|
-
waypoints: array(BoardVec2Schema$1).max(64).default([]),
|
|
5932
|
+
bend: finite$1.min(-.85).max(.85).default(0).describe("Sideways curve as a fraction of the length; the sign picks the side."),
|
|
5933
|
+
waypoints: array(BoardVec2Schema$1).max(64).default([]).describe("Points the route passes through."),
|
|
5934
5934
|
arrowStart: boolean().default(false),
|
|
5935
5935
|
arrowEnd: boolean().default(true),
|
|
5936
5936
|
label: string().max(280).default(""),
|
|
5937
5937
|
fontSize: fontSize$1.default(14),
|
|
5938
|
-
relation: string().max(64).regex(/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/, "relation must be a lowercase slug").optional()
|
|
5938
|
+
relation: string().max(64).regex(/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/, "relation must be a lowercase slug").optional().describe("What the arrow means, as a slug such as triggers or depends-on.")
|
|
5939
5939
|
}).strict()
|
|
5940
5940
|
}).strict();
|
|
5941
5941
|
const fileProps$1 = {
|
|
@@ -5951,26 +5951,26 @@ const fileProps$1 = {
|
|
|
5951
5951
|
excerpt: string().optional(),
|
|
5952
5952
|
coverPath: string().optional(),
|
|
5953
5953
|
coverUrl: string().optional()
|
|
5954
|
-
}).strict().optional()
|
|
5954
|
+
}).strict().describe("Cached facts for an instant preview; filled in when the media loads.").optional()
|
|
5955
5955
|
};
|
|
5956
5956
|
const BoardMediaOriginSchema$1 = object({
|
|
5957
5957
|
taskRunId: string().min(1).max(160),
|
|
5958
5958
|
artifactId: string().min(1).max(240)
|
|
5959
|
-
}).strict();
|
|
5959
|
+
}).strict().describe("The generation run and result this media came from.");
|
|
5960
5960
|
const mediaProps$1 = {
|
|
5961
5961
|
...fileProps$1,
|
|
5962
5962
|
origin: BoardMediaOriginSchema$1.optional()
|
|
5963
5963
|
};
|
|
5964
5964
|
const timedFileProps$1 = {
|
|
5965
5965
|
...mediaProps$1,
|
|
5966
|
-
time: nonNegative$1.optional()
|
|
5966
|
+
time: nonNegative$1.optional().describe("Playback position in milliseconds; animate props.time to play it on a timeline.")
|
|
5967
5967
|
};
|
|
5968
5968
|
const BoardCropSchema$1 = object({
|
|
5969
5969
|
x: ratio$1,
|
|
5970
5970
|
y: ratio$1,
|
|
5971
5971
|
w: ratio$1,
|
|
5972
5972
|
h: ratio$1
|
|
5973
|
-
}).strict();
|
|
5973
|
+
}).strict().describe("Region shown, as fractions of the image. Without it the whole image fits inside size.");
|
|
5974
5974
|
const BoardImageItemSchema$1 = object({
|
|
5975
5975
|
...boxed$1(640, 360),
|
|
5976
5976
|
type: literal("image"),
|
|
@@ -6050,8 +6050,8 @@ const BoardTaskItemSchema$1 = object({
|
|
|
6050
6050
|
...boxed$1(420, 240),
|
|
6051
6051
|
type: literal("task"),
|
|
6052
6052
|
props: object({
|
|
6053
|
-
taskRunId: string().min(1).max(160),
|
|
6054
|
-
snapshot: BoardTaskSnapshotSchema$1.describe("Maintained by the server
|
|
6053
|
+
taskRunId: string().min(1).max(160).describe("The generation run shown; the server keeps the card's status and results up to date."),
|
|
6054
|
+
snapshot: BoardTaskSnapshotSchema$1.describe("Maintained by the server; omit it.")
|
|
6055
6055
|
}).strict()
|
|
6056
6056
|
}).strict();
|
|
6057
6057
|
const BOARD_EFFECT_KINDS$1 = [
|
|
@@ -6074,8 +6074,8 @@ const BoardEffectItemSchema$1 = object({
|
|
|
6074
6074
|
spread: finite$1.min(0).max(360).default(360),
|
|
6075
6075
|
gravity: finite$1.default(0),
|
|
6076
6076
|
intensity: nonNegative$1.max(10).default(1),
|
|
6077
|
-
follow: BoardIdSchema$1.optional(),
|
|
6078
|
-
seed: string().min(1).max(160).optional()
|
|
6077
|
+
follow: BoardIdSchema$1.optional().describe("trail: the item to follow."),
|
|
6078
|
+
seed: string().min(1).max(160).optional().describe("Makes procedural motion repeatable.")
|
|
6079
6079
|
}).strict()
|
|
6080
6080
|
}).strict();
|
|
6081
6081
|
const BoardCustomItemSchema$1 = object({
|
|
@@ -6177,21 +6177,21 @@ function isBoardEase$1(value) {
|
|
|
6177
6177
|
}
|
|
6178
6178
|
const BoardEaseSchema$1 = string().max(120).refine(isBoardEase$1, "expected a CSS easing: linear, ease-in-out, cubic-bezier(…) or steps(…)");
|
|
6179
6179
|
const BoardKeyframeSchema$1 = object({
|
|
6180
|
-
at: nonNegative$1,
|
|
6180
|
+
at: nonNegative$1.describe("Time in milliseconds."),
|
|
6181
6181
|
value: unknown(),
|
|
6182
|
-
ease: BoardEaseSchema$1.optional()
|
|
6182
|
+
ease: BoardEaseSchema$1.optional().describe("Easing into this keyframe: a CSS keyword, cubic-bezier() or steps(); defaults to linear.")
|
|
6183
6183
|
}).strict();
|
|
6184
6184
|
const BoardTrackSchema$1 = object({
|
|
6185
|
-
target: string().min(1).max(160),
|
|
6186
|
-
property: string().min(1).max(200),
|
|
6187
|
-
keyframes: array(BoardKeyframeSchema$1).min(1).max(1e5),
|
|
6188
|
-
composite: _enum(["replace", "add"]).default("replace"),
|
|
6185
|
+
target: string().min(1).max(160).describe("An item id, camera, or another animation id (its time property nests that timeline)."),
|
|
6186
|
+
property: string().min(1).max(200).describe("JSON path in the target, e.g. position, opacity, style.fill, props.text."),
|
|
6187
|
+
keyframes: array(BoardKeyframeSchema$1).min(1).max(1e5).describe("Ordered by at. The value holds before the first and after the last keyframe."),
|
|
6188
|
+
composite: _enum(["replace", "add"]).default("replace").describe("replace: the latest-started track wins. add: positions and rotations add; scale and opacity multiply."),
|
|
6189
6189
|
interpolation: _enum([
|
|
6190
6190
|
"auto",
|
|
6191
6191
|
"step",
|
|
6192
6192
|
"spline"
|
|
6193
|
-
]).default("auto"),
|
|
6194
|
-
orient: boolean().optional()
|
|
6193
|
+
]).default("auto").describe("auto blends numbers, vectors and colors and steps other values; spline smooths position paths."),
|
|
6194
|
+
orient: boolean().optional().describe("Rotate the item along its path.")
|
|
6195
6195
|
}).strict().superRefine((track, context) => {
|
|
6196
6196
|
for (let index = 1; index < track.keyframes.length; index += 1) if ((track.keyframes[index]?.at ?? 0) < (track.keyframes[index - 1]?.at ?? 0)) context.addIssue({
|
|
6197
6197
|
code: "custom",
|
|
@@ -6206,20 +6206,20 @@ const BoardTrackSchema$1 = object({
|
|
|
6206
6206
|
const BoardMarkerSchema$1 = object({
|
|
6207
6207
|
at: nonNegative$1,
|
|
6208
6208
|
label: string().max(280).optional(),
|
|
6209
|
-
pause: boolean().optional()
|
|
6209
|
+
pause: boolean().optional().describe("Hold shared playback here until next.")
|
|
6210
6210
|
}).strict();
|
|
6211
6211
|
const BoardAnimationHeaderSchema$1 = object({
|
|
6212
6212
|
name: string().max(280).optional(),
|
|
6213
|
-
duration: finite$1.positive().max(864e5),
|
|
6213
|
+
duration: finite$1.positive().max(864e5).describe("Milliseconds; a new animation without it ends at its last keyframe."),
|
|
6214
6214
|
play: _enum([
|
|
6215
6215
|
"manual",
|
|
6216
6216
|
"auto",
|
|
6217
6217
|
"always"
|
|
6218
|
-
]).default("manual"),
|
|
6218
|
+
]).default("manual").describe("manual: only shared playback starts it. auto: plays when the Board opens. always: loops continuously."),
|
|
6219
6219
|
delay: nonNegative$1.default(0),
|
|
6220
6220
|
loop: boolean().default(false),
|
|
6221
|
-
end: _enum(["hold", "reset"]).default("hold"),
|
|
6222
|
-
markers: array(BoardMarkerSchema$1).max(1e4).default([])
|
|
6221
|
+
end: _enum(["hold", "reset"]).default("hold").describe("hold keeps the last frame; reset returns to the start."),
|
|
6222
|
+
markers: array(BoardMarkerSchema$1).max(1e4).default([]).describe("Labeled points on this timeline; a nested animation's markers do not carry over.")
|
|
6223
6223
|
}).strict();
|
|
6224
6224
|
BoardAnimationHeaderSchema$1.extend({ tracks: record(BoardKeySchema$1, BoardTrackSchema$1).default({}) }).strict();
|
|
6225
6225
|
const BOARD_ENTER_PRESETS$1 = [
|
|
@@ -6237,7 +6237,7 @@ const BoardBackgroundSchema$1 = object({
|
|
|
6237
6237
|
"image"
|
|
6238
6238
|
]).default("dots"),
|
|
6239
6239
|
color: BoardColorSchema$1.optional(),
|
|
6240
|
-
imageUrl: string().url().max(4096).optional(),
|
|
6240
|
+
imageUrl: string().url().max(4096).optional().describe("Background image for kind image."),
|
|
6241
6241
|
fit: _enum([
|
|
6242
6242
|
"cover",
|
|
6243
6243
|
"contain",
|
|
@@ -6249,13 +6249,13 @@ const BoardSettingsSchema$1 = object({
|
|
|
6249
6249
|
title: string().min(1).max(255).optional(),
|
|
6250
6250
|
background: BoardBackgroundSchema$1.default({ kind: "dots" }),
|
|
6251
6251
|
grid: object({
|
|
6252
|
-
visible: boolean().default(false),
|
|
6252
|
+
visible: boolean().default(false).describe("Show the grid overlay."),
|
|
6253
6253
|
size: finite$1.min(4).max(512).default(24)
|
|
6254
6254
|
}).strict().optional(),
|
|
6255
6255
|
enter: object({
|
|
6256
6256
|
preset: _enum(BOARD_ENTER_PRESETS$1),
|
|
6257
6257
|
duration: finite$1.positive().max(1e4).optional()
|
|
6258
|
-
}).strict().optional()
|
|
6258
|
+
}).strict().optional().describe("Motion for items as they are added.")
|
|
6259
6259
|
}).strict();
|
|
6260
6260
|
const patchObject$1 = record(string(), unknown());
|
|
6261
6261
|
const BoardPatchSchema$1 = object({
|
|
@@ -30341,10 +30341,10 @@ const colorString = string().max(120).refine(isBoardColorString, "expected a pal
|
|
|
30341
30341
|
const BoardColorSchema = union([colorString, object({
|
|
30342
30342
|
light: colorString,
|
|
30343
30343
|
dark: colorString
|
|
30344
|
-
}).strict()]);
|
|
30344
|
+
}).strict()]).describe("Palette token (follows the theme), CSS color, or { light, dark }.");
|
|
30345
30345
|
const BoardStyleSchema = object({
|
|
30346
30346
|
fill: BoardColorSchema.optional(),
|
|
30347
|
-
fillOpacity: ratio.optional(),
|
|
30347
|
+
fillOpacity: ratio.optional().describe("Fill opacity, independent of the item's opacity."),
|
|
30348
30348
|
stroke: BoardColorSchema.optional(),
|
|
30349
30349
|
strokeWidth: finite.min(0).max(64).optional(),
|
|
30350
30350
|
dash: _enum([
|
|
@@ -30352,8 +30352,8 @@ const BoardStyleSchema = object({
|
|
|
30352
30352
|
"dashed",
|
|
30353
30353
|
"dotted"
|
|
30354
30354
|
]).optional(),
|
|
30355
|
-
radius: nonNegative.optional(),
|
|
30356
|
-
trim: ratio.optional()
|
|
30355
|
+
radius: nonNegative.optional().describe("Corner radius in board units."),
|
|
30356
|
+
trim: ratio.optional().describe("Fraction of the outline drawn; animate 0 → 1 to draw it in.")
|
|
30357
30357
|
}).strict();
|
|
30358
30358
|
function isSafeBoardSourcePath(value) {
|
|
30359
30359
|
return value.length > 0 && value.length <= 4096 && !value.startsWith("/") && !/^[a-z][a-z\d+.-]*:/i.test(value.trimStart()) && !value.includes("\\") && value.split("/").every((part) => part.length > 0 && part !== "." && part !== "..");
|
|
@@ -30369,18 +30369,18 @@ function parseBoardMediaSource(src) {
|
|
|
30369
30369
|
value: src
|
|
30370
30370
|
} : null;
|
|
30371
30371
|
}
|
|
30372
|
-
const BoardMediaSourceSchema = string().max(4096).refine((src) => parseBoardMediaSource(src) !== null, "src must be a public HTTP(S) URL or relative Space file path").describe("Prefer a
|
|
30372
|
+
const BoardMediaSourceSchema = string().max(4096).refine((src) => parseBoardMediaSource(src) !== null, "src must be a public HTTP(S) URL or relative Space file path").describe("Prefer a public HTTP(S) URL; a Space path relative to the workspace root also works.");
|
|
30373
30373
|
const BoardScriptSchema = object({
|
|
30374
|
-
src: srcSchema.refine((path) => /\.m?js$/.test(path), "script src must be a .js or .mjs module"),
|
|
30375
|
-
params: record(string(), json()).default({}),
|
|
30376
|
-
targets: record(string().min(1).max(80), BoardIdSchema).default({}),
|
|
30377
|
-
seed: string().min(1).max(160).optional(),
|
|
30374
|
+
src: srcSchema.refine((path) => /\.m?js$/.test(path), "script src must be a .js or .mjs module").describe("A .js or .mjs Space file."),
|
|
30375
|
+
params: record(string(), json()).default({}).describe("Values the module reads as params."),
|
|
30376
|
+
targets: record(string().min(1).max(80), BoardIdSchema).default({}).describe("Named items the module can reach, as ctx.targets."),
|
|
30377
|
+
seed: string().min(1).max(160).optional().describe("Seed for ctx.random(); defaults to the item id."),
|
|
30378
30378
|
enabled: boolean().default(true)
|
|
30379
|
-
}).strict();
|
|
30379
|
+
}).strict().describe("A JavaScript module attached to the item; `cohub boards schema script` lists its API.");
|
|
30380
30380
|
const itemEnvelope = {
|
|
30381
30381
|
script: BoardScriptSchema.optional(),
|
|
30382
|
-
parent: BoardIdSchema.optional(),
|
|
30383
|
-
z: finite.optional(),
|
|
30382
|
+
parent: BoardIdSchema.optional().describe("A frame id; position becomes relative to the frame, and the item moves with it."),
|
|
30383
|
+
z: finite.optional().describe("Order among siblings; new items go on top."),
|
|
30384
30384
|
position: BoardVec2Schema.default({
|
|
30385
30385
|
x: 0,
|
|
30386
30386
|
y: 0
|
|
@@ -30390,7 +30390,7 @@ const itemEnvelope = {
|
|
|
30390
30390
|
origin: BoardVec2Schema.default({
|
|
30391
30391
|
x: .5,
|
|
30392
30392
|
y: .5
|
|
30393
|
-
}),
|
|
30393
|
+
}).describe("Rotation and scale origin as fractions of the size."),
|
|
30394
30394
|
opacity: ratio.default(1),
|
|
30395
30395
|
style: BoardStyleSchema.default({}),
|
|
30396
30396
|
locked: boolean().optional(),
|
|
@@ -30423,8 +30423,8 @@ object({
|
|
|
30423
30423
|
type: literal("frame"),
|
|
30424
30424
|
props: object({
|
|
30425
30425
|
label: string().max(280).default(""),
|
|
30426
|
-
decoration: _enum(["frame", "none"]).optional(),
|
|
30427
|
-
clip: boolean().default(true)
|
|
30426
|
+
decoration: _enum(["frame", "none"]).optional().describe("none hides the title, border and background: an invisible group whose tracks move every child."),
|
|
30427
|
+
clip: boolean().default(true).describe("Hide children outside the frame.")
|
|
30428
30428
|
}).strict().prefault({})
|
|
30429
30429
|
}).strict();
|
|
30430
30430
|
object({
|
|
@@ -30434,12 +30434,12 @@ object({
|
|
|
30434
30434
|
text: string().default(""),
|
|
30435
30435
|
fontSize: fontSize.default(24),
|
|
30436
30436
|
fontWeight: finite.min(100).max(900).default(500),
|
|
30437
|
-
font: string().min(1).max(120).default("sans"),
|
|
30437
|
+
font: string().min(1).max(120).default("sans").describe("sans, serif, mono, or a font family name."),
|
|
30438
30438
|
align: _enum(BOARD_TEXT_ALIGNS).default("left"),
|
|
30439
30439
|
lineHeight: finite.min(.5).max(4).default(4 / 3),
|
|
30440
|
-
width: finite.positive().optional(),
|
|
30441
|
-
reveal: ratio.default(1),
|
|
30442
|
-
caret: boolean().default(false)
|
|
30440
|
+
width: finite.positive().optional().describe("Wrap width; the height follows the text."),
|
|
30441
|
+
reveal: ratio.default(1).describe("Fraction of the text shown; animate 0 → 1 to type it."),
|
|
30442
|
+
caret: boolean().default(false).describe("Blink a caret while the text is being revealed.")
|
|
30443
30443
|
}).strict().prefault({})
|
|
30444
30444
|
}).strict();
|
|
30445
30445
|
object({
|
|
@@ -30447,7 +30447,7 @@ object({
|
|
|
30447
30447
|
type: literal("shape"),
|
|
30448
30448
|
props: object({
|
|
30449
30449
|
geometry: _enum(BOARD_SHAPE_GEOMETRIES).default("rectangle"),
|
|
30450
|
-
path: string().min(1).max(2e5).optional(),
|
|
30450
|
+
path: string().min(1).max(2e5).optional().describe("SVG path data for path geometry, fitted to the item size."),
|
|
30451
30451
|
text: string().default(""),
|
|
30452
30452
|
fontSize: fontSize.default(20),
|
|
30453
30453
|
align: _enum(BOARD_TEXT_ALIGNS).default("center")
|
|
@@ -30459,12 +30459,12 @@ object({
|
|
|
30459
30459
|
const BoardDrawPointSchema = object({
|
|
30460
30460
|
x: finite,
|
|
30461
30461
|
y: finite,
|
|
30462
|
-
p: ratio.default(.5)
|
|
30462
|
+
p: ratio.default(.5).describe("Pen pressure.")
|
|
30463
30463
|
}).strict();
|
|
30464
30464
|
object({
|
|
30465
30465
|
...unsized,
|
|
30466
30466
|
type: literal("draw"),
|
|
30467
|
-
props: object({ points: array(BoardDrawPointSchema).min(1).max(1e5) }).strict()
|
|
30467
|
+
props: object({ points: array(BoardDrawPointSchema).min(1).max(1e5).describe("Stroke points relative to position; the size follows them.") }).strict()
|
|
30468
30468
|
}).strict();
|
|
30469
30469
|
const BoardArrowEndSchema = union([BoardVec2Schema, object({
|
|
30470
30470
|
item: BoardIdSchema,
|
|
@@ -30483,9 +30483,9 @@ const BoardArrowEndSchema = union([BoardVec2Schema, object({
|
|
|
30483
30483
|
x: ratio,
|
|
30484
30484
|
y: ratio
|
|
30485
30485
|
}).strict()
|
|
30486
|
-
]).default("auto"),
|
|
30486
|
+
]).default("auto").describe("auto uses the side facing the other end; or a side with an offset, or a point as fractions of the item."),
|
|
30487
30487
|
port: string().min(1).max(120).optional()
|
|
30488
|
-
}).strict()]);
|
|
30488
|
+
}).strict()]).describe("A point relative to position, or { item } to follow that item.");
|
|
30489
30489
|
const BOARD_ARROW_ROUTES = [
|
|
30490
30490
|
"straight",
|
|
30491
30491
|
"curve",
|
|
@@ -30498,13 +30498,13 @@ object({
|
|
|
30498
30498
|
start: BoardArrowEndSchema,
|
|
30499
30499
|
end: BoardArrowEndSchema,
|
|
30500
30500
|
route: _enum(BOARD_ARROW_ROUTES).default("straight"),
|
|
30501
|
-
bend: finite.min(-.85).max(.85).default(0),
|
|
30502
|
-
waypoints: array(BoardVec2Schema).max(64).default([]),
|
|
30501
|
+
bend: finite.min(-.85).max(.85).default(0).describe("Sideways curve as a fraction of the length; the sign picks the side."),
|
|
30502
|
+
waypoints: array(BoardVec2Schema).max(64).default([]).describe("Points the route passes through."),
|
|
30503
30503
|
arrowStart: boolean().default(false),
|
|
30504
30504
|
arrowEnd: boolean().default(true),
|
|
30505
30505
|
label: string().max(280).default(""),
|
|
30506
30506
|
fontSize: fontSize.default(14),
|
|
30507
|
-
relation: string().max(64).regex(/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/, "relation must be a lowercase slug").optional()
|
|
30507
|
+
relation: string().max(64).regex(/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/, "relation must be a lowercase slug").optional().describe("What the arrow means, as a slug such as triggers or depends-on.")
|
|
30508
30508
|
}).strict()
|
|
30509
30509
|
}).strict();
|
|
30510
30510
|
const fileProps = {
|
|
@@ -30520,26 +30520,26 @@ const fileProps = {
|
|
|
30520
30520
|
excerpt: string().optional(),
|
|
30521
30521
|
coverPath: string().optional(),
|
|
30522
30522
|
coverUrl: string().optional()
|
|
30523
|
-
}).strict().optional()
|
|
30523
|
+
}).strict().describe("Cached facts for an instant preview; filled in when the media loads.").optional()
|
|
30524
30524
|
};
|
|
30525
30525
|
const BoardMediaOriginSchema = object({
|
|
30526
30526
|
taskRunId: string().min(1).max(160),
|
|
30527
30527
|
artifactId: string().min(1).max(240)
|
|
30528
|
-
}).strict();
|
|
30528
|
+
}).strict().describe("The generation run and result this media came from.");
|
|
30529
30529
|
const mediaProps = {
|
|
30530
30530
|
...fileProps,
|
|
30531
30531
|
origin: BoardMediaOriginSchema.optional()
|
|
30532
30532
|
};
|
|
30533
30533
|
const timedFileProps = {
|
|
30534
30534
|
...mediaProps,
|
|
30535
|
-
time: nonNegative.optional()
|
|
30535
|
+
time: nonNegative.optional().describe("Playback position in milliseconds; animate props.time to play it on a timeline.")
|
|
30536
30536
|
};
|
|
30537
30537
|
const BoardCropSchema = object({
|
|
30538
30538
|
x: ratio,
|
|
30539
30539
|
y: ratio,
|
|
30540
30540
|
w: ratio,
|
|
30541
30541
|
h: ratio
|
|
30542
|
-
}).strict();
|
|
30542
|
+
}).strict().describe("Region shown, as fractions of the image. Without it the whole image fits inside size.");
|
|
30543
30543
|
object({
|
|
30544
30544
|
...boxed(640, 360),
|
|
30545
30545
|
type: literal("image"),
|
|
@@ -30619,8 +30619,8 @@ object({
|
|
|
30619
30619
|
...boxed(420, 240),
|
|
30620
30620
|
type: literal("task"),
|
|
30621
30621
|
props: object({
|
|
30622
|
-
taskRunId: string().min(1).max(160),
|
|
30623
|
-
snapshot: BoardTaskSnapshotSchema.describe("Maintained by the server
|
|
30622
|
+
taskRunId: string().min(1).max(160).describe("The generation run shown; the server keeps the card's status and results up to date."),
|
|
30623
|
+
snapshot: BoardTaskSnapshotSchema.describe("Maintained by the server; omit it.")
|
|
30624
30624
|
}).strict()
|
|
30625
30625
|
}).strict();
|
|
30626
30626
|
const BOARD_EFFECT_KINDS = [
|
|
@@ -30643,8 +30643,8 @@ object({
|
|
|
30643
30643
|
spread: finite.min(0).max(360).default(360),
|
|
30644
30644
|
gravity: finite.default(0),
|
|
30645
30645
|
intensity: nonNegative.max(10).default(1),
|
|
30646
|
-
follow: BoardIdSchema.optional(),
|
|
30647
|
-
seed: string().min(1).max(160).optional()
|
|
30646
|
+
follow: BoardIdSchema.optional().describe("trail: the item to follow."),
|
|
30647
|
+
seed: string().min(1).max(160).optional().describe("Makes procedural motion repeatable.")
|
|
30648
30648
|
}).strict()
|
|
30649
30649
|
}).strict();
|
|
30650
30650
|
object({
|
|
@@ -30684,21 +30684,21 @@ function isBoardEase(value) {
|
|
|
30684
30684
|
}
|
|
30685
30685
|
const BoardEaseSchema = string().max(120).refine(isBoardEase, "expected a CSS easing: linear, ease-in-out, cubic-bezier(…) or steps(…)");
|
|
30686
30686
|
const BoardKeyframeSchema = object({
|
|
30687
|
-
at: nonNegative,
|
|
30687
|
+
at: nonNegative.describe("Time in milliseconds."),
|
|
30688
30688
|
value: unknown(),
|
|
30689
|
-
ease: BoardEaseSchema.optional()
|
|
30689
|
+
ease: BoardEaseSchema.optional().describe("Easing into this keyframe: a CSS keyword, cubic-bezier() or steps(); defaults to linear.")
|
|
30690
30690
|
}).strict();
|
|
30691
30691
|
const BoardTrackSchema = object({
|
|
30692
|
-
target: string().min(1).max(160),
|
|
30693
|
-
property: string().min(1).max(200),
|
|
30694
|
-
keyframes: array(BoardKeyframeSchema).min(1).max(1e5),
|
|
30695
|
-
composite: _enum(["replace", "add"]).default("replace"),
|
|
30692
|
+
target: string().min(1).max(160).describe("An item id, camera, or another animation id (its time property nests that timeline)."),
|
|
30693
|
+
property: string().min(1).max(200).describe("JSON path in the target, e.g. position, opacity, style.fill, props.text."),
|
|
30694
|
+
keyframes: array(BoardKeyframeSchema).min(1).max(1e5).describe("Ordered by at. The value holds before the first and after the last keyframe."),
|
|
30695
|
+
composite: _enum(["replace", "add"]).default("replace").describe("replace: the latest-started track wins. add: positions and rotations add; scale and opacity multiply."),
|
|
30696
30696
|
interpolation: _enum([
|
|
30697
30697
|
"auto",
|
|
30698
30698
|
"step",
|
|
30699
30699
|
"spline"
|
|
30700
|
-
]).default("auto"),
|
|
30701
|
-
orient: boolean().optional()
|
|
30700
|
+
]).default("auto").describe("auto blends numbers, vectors and colors and steps other values; spline smooths position paths."),
|
|
30701
|
+
orient: boolean().optional().describe("Rotate the item along its path.")
|
|
30702
30702
|
}).strict().superRefine((track, context) => {
|
|
30703
30703
|
for (let index = 1; index < track.keyframes.length; index += 1) if ((track.keyframes[index]?.at ?? 0) < (track.keyframes[index - 1]?.at ?? 0)) context.addIssue({
|
|
30704
30704
|
code: "custom",
|
|
@@ -30713,20 +30713,20 @@ const BoardTrackSchema = object({
|
|
|
30713
30713
|
const BoardMarkerSchema = object({
|
|
30714
30714
|
at: nonNegative,
|
|
30715
30715
|
label: string().max(280).optional(),
|
|
30716
|
-
pause: boolean().optional()
|
|
30716
|
+
pause: boolean().optional().describe("Hold shared playback here until next.")
|
|
30717
30717
|
}).strict();
|
|
30718
30718
|
object({
|
|
30719
30719
|
name: string().max(280).optional(),
|
|
30720
|
-
duration: finite.positive().max(864e5),
|
|
30720
|
+
duration: finite.positive().max(864e5).describe("Milliseconds; a new animation without it ends at its last keyframe."),
|
|
30721
30721
|
play: _enum([
|
|
30722
30722
|
"manual",
|
|
30723
30723
|
"auto",
|
|
30724
30724
|
"always"
|
|
30725
|
-
]).default("manual"),
|
|
30725
|
+
]).default("manual").describe("manual: only shared playback starts it. auto: plays when the Board opens. always: loops continuously."),
|
|
30726
30726
|
delay: nonNegative.default(0),
|
|
30727
30727
|
loop: boolean().default(false),
|
|
30728
|
-
end: _enum(["hold", "reset"]).default("hold"),
|
|
30729
|
-
markers: array(BoardMarkerSchema).max(1e4).default([])
|
|
30728
|
+
end: _enum(["hold", "reset"]).default("hold").describe("hold keeps the last frame; reset returns to the start."),
|
|
30729
|
+
markers: array(BoardMarkerSchema).max(1e4).default([]).describe("Labeled points on this timeline; a nested animation's markers do not carry over.")
|
|
30730
30730
|
}).strict().extend({ tracks: record(BoardKeySchema, BoardTrackSchema).default({}) }).strict();
|
|
30731
30731
|
const BOARD_ENTER_PRESETS = [
|
|
30732
30732
|
"fade-in",
|
|
@@ -30743,7 +30743,7 @@ const BoardBackgroundSchema = object({
|
|
|
30743
30743
|
"image"
|
|
30744
30744
|
]).default("dots"),
|
|
30745
30745
|
color: BoardColorSchema.optional(),
|
|
30746
|
-
imageUrl: string().url().max(4096).optional(),
|
|
30746
|
+
imageUrl: string().url().max(4096).optional().describe("Background image for kind image."),
|
|
30747
30747
|
fit: _enum([
|
|
30748
30748
|
"cover",
|
|
30749
30749
|
"contain",
|
|
@@ -30755,13 +30755,13 @@ object({
|
|
|
30755
30755
|
title: string().min(1).max(255).optional(),
|
|
30756
30756
|
background: BoardBackgroundSchema.default({ kind: "dots" }),
|
|
30757
30757
|
grid: object({
|
|
30758
|
-
visible: boolean().default(false),
|
|
30758
|
+
visible: boolean().default(false).describe("Show the grid overlay."),
|
|
30759
30759
|
size: finite.min(4).max(512).default(24)
|
|
30760
30760
|
}).strict().optional(),
|
|
30761
30761
|
enter: object({
|
|
30762
30762
|
preset: _enum(BOARD_ENTER_PRESETS),
|
|
30763
30763
|
duration: finite.positive().max(1e4).optional()
|
|
30764
|
-
}).strict().optional()
|
|
30764
|
+
}).strict().optional().describe("Motion for items as they are added.")
|
|
30765
30765
|
}).strict();
|
|
30766
30766
|
const patchObject = record(string(), unknown());
|
|
30767
30767
|
object({
|
package/dist/commands/boards.js
CHANGED
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { mkdir } from "node:fs/promises";
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
|
-
import { BOARD_PRESET_NAMES, BOARD_SCHEMA_TARGETS, boardJsonSchema, boardPresetTracks, boardSchemaOverview, isBoardPresetName, listBoardPresets, } from "@neta-art/cohub/board";
|
|
5
|
+
import { BOARD_PRESET_NAMES, BOARD_SCHEMA_TARGETS, BOARD_SCRIPT_API, boardJsonSchema, boardPresetTracks, boardSchemaOverview, isBoardPresetName, listBoardPresets, } from "@neta-art/cohub/board";
|
|
6
6
|
import { BOARD_CREATE_INPUT_MAX_BYTES, BOARD_TRANSACTION_INPUT_MAX_BYTES, parseBoardJsonObject, readBoardJsonObject, resolveBoardId, writeBoardOutput, } from "../board-command-support.js";
|
|
7
7
|
import { formatBoardTime, parseBoardTime, parseBoardTimes } from "../board-time.js";
|
|
8
8
|
import { createClient, createRealtimeClient } from "../client.js";
|
|
@@ -283,13 +283,18 @@ export function registerBoards(program) {
|
|
|
283
283
|
.description("Read, write, animate and export Boards (by id or .board path)")
|
|
284
284
|
.addHelpText("after", `
|
|
285
285
|
A Board is one JSON document: { board, items, animations }.
|
|
286
|
-
get
|
|
287
|
-
apply
|
|
288
|
-
schema
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
286
|
+
get read it
|
|
287
|
+
apply write it with a JSON Merge Patch; null deletes
|
|
288
|
+
schema the fields of any part and what they mean; also the script API
|
|
289
|
+
examples starter documents and a custom script module
|
|
290
|
+
preset tracks for a preset motion, ready to apply
|
|
291
|
+
sequence join chapter animations into one timeline
|
|
292
|
+
path sample a polyline into a position track
|
|
293
|
+
inspect measure items and report layout hints
|
|
294
|
+
export render an image, a frame sequence or a video
|
|
295
|
+
play shared playback; also pause, resume, next, seek, stop
|
|
296
|
+
watch stream changes and playback
|
|
297
|
+
history list versions, or restore one
|
|
293
298
|
Times are milliseconds; 500ms, 12.5s and 2m also work on the command line.`);
|
|
294
299
|
withJson(boards.command("create <path>")
|
|
295
300
|
.description("Create a Board file, optionally with an initial document")
|
|
@@ -444,9 +449,9 @@ Times are milliseconds; 500ms, 12.5s and 2m also work on the command line.`);
|
|
|
444
449
|
boards.command("schema [target]")
|
|
445
450
|
.description("Print JSON Schema for a part of a Board")
|
|
446
451
|
.addHelpText("after", `
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
452
|
+
Without a target: units, item types, colors and every animatable property.
|
|
453
|
+
With a target: its fields and what they mean; script also lists the module API.
|
|
454
|
+
Targets: ${BOARD_SCHEMA_TARGETS.join(", ")}`)
|
|
450
455
|
.action((target) => {
|
|
451
456
|
try {
|
|
452
457
|
if (!target)
|
|
@@ -454,7 +459,7 @@ Without a target: units, item types, colors and every animatable property.`)
|
|
|
454
459
|
const schema = boardJsonSchema(target);
|
|
455
460
|
if (!schema)
|
|
456
461
|
throw new Error(`Unknown target ${target}; expected ${BOARD_SCHEMA_TARGETS.join(", ")}.`);
|
|
457
|
-
outJson(schema);
|
|
462
|
+
outJson(target === "script" ? { ...schema, api: BOARD_SCRIPT_API } : schema);
|
|
458
463
|
}
|
|
459
464
|
catch (cause) {
|
|
460
465
|
handleHttp(cause);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Readable } from "node:stream";
|
|
2
|
+
type Input = {
|
|
3
|
+
op: "publish" | "send";
|
|
4
|
+
type: string;
|
|
5
|
+
data: unknown;
|
|
6
|
+
} | {
|
|
7
|
+
op: "presence";
|
|
8
|
+
presence: Record<string, unknown> | null;
|
|
9
|
+
};
|
|
10
|
+
export declare function parseRoomInput(line: string): Input;
|
|
11
|
+
export declare function readRoomLines(input: Readable, maxBytes?: number): AsyncGenerator<string>;
|
|
12
|
+
export declare function connectToRoom(roomId: string, options: {
|
|
13
|
+
after?: string;
|
|
14
|
+
input?: boolean;
|
|
15
|
+
json: boolean;
|
|
16
|
+
}): Promise<void>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { createRealtimeClient } from "../client.js";
|
|
2
|
+
const MAX_LINE_BYTES = 20 * 1024;
|
|
3
|
+
const MAX_OUTPUT_BUFFER_BYTES = 1024 * 1024;
|
|
4
|
+
export function parseRoomInput(line) {
|
|
5
|
+
if (Buffer.byteLength(line) > MAX_LINE_BYTES)
|
|
6
|
+
throw new Error("Room input line is too large");
|
|
7
|
+
let value;
|
|
8
|
+
try {
|
|
9
|
+
value = JSON.parse(line);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new Error("Room input must be JSON");
|
|
13
|
+
}
|
|
14
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
15
|
+
throw new Error("Room input must be an object");
|
|
16
|
+
const input = value;
|
|
17
|
+
if (input.op === "presence" && (input.presence === null || typeof input.presence === "object" && !Array.isArray(input.presence))) {
|
|
18
|
+
return { op: "presence", presence: input.presence };
|
|
19
|
+
}
|
|
20
|
+
if ((input.op === "publish" || input.op === "send") && typeof input.type === "string" && "data" in input) {
|
|
21
|
+
return { op: input.op, type: input.type, data: input.data };
|
|
22
|
+
}
|
|
23
|
+
throw new Error("Use {\"op\":\"publish|send\",\"type\":\"name\",\"data\":...} or {\"op\":\"presence\",\"presence\":...}");
|
|
24
|
+
}
|
|
25
|
+
export async function* readRoomLines(input, maxBytes = MAX_LINE_BYTES) {
|
|
26
|
+
let pending = Buffer.alloc(0);
|
|
27
|
+
for await (const chunk of input) {
|
|
28
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
29
|
+
let start = 0;
|
|
30
|
+
for (;;) {
|
|
31
|
+
const end = bytes.indexOf(10, start);
|
|
32
|
+
const part = bytes.subarray(start, end < 0 ? bytes.length : end);
|
|
33
|
+
if (pending.length + part.length > maxBytes)
|
|
34
|
+
throw new Error("Room input line is too large");
|
|
35
|
+
pending = pending.length ? Buffer.concat([pending, part]) : part;
|
|
36
|
+
if (end < 0)
|
|
37
|
+
break;
|
|
38
|
+
yield pending.toString("utf8").replace(/\r$/, "");
|
|
39
|
+
pending = Buffer.alloc(0);
|
|
40
|
+
start = end + 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (pending.length)
|
|
44
|
+
yield pending.toString("utf8");
|
|
45
|
+
}
|
|
46
|
+
export async function connectToRoom(roomId, options) {
|
|
47
|
+
const after = options.after === undefined ? undefined : Number(options.after);
|
|
48
|
+
if (after !== undefined && (!Number.isSafeInteger(after) || after < 0))
|
|
49
|
+
throw new Error("--after must be a non-negative sequence number");
|
|
50
|
+
const client = createRealtimeClient();
|
|
51
|
+
let room = null;
|
|
52
|
+
let stopped = false;
|
|
53
|
+
let finish;
|
|
54
|
+
const done = new Promise((resolve) => { finish = resolve; });
|
|
55
|
+
const stop = () => {
|
|
56
|
+
if (stopped)
|
|
57
|
+
return;
|
|
58
|
+
stopped = true;
|
|
59
|
+
finish();
|
|
60
|
+
};
|
|
61
|
+
const write = (kind, value) => {
|
|
62
|
+
if (stopped)
|
|
63
|
+
return;
|
|
64
|
+
if (process.stdout.writableLength > MAX_OUTPUT_BUFFER_BYTES) {
|
|
65
|
+
process.stderr.write("Room output is too slow; disconnecting.\n");
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
stop();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
process.stdout.write(options.json ? `${JSON.stringify({ kind, ...value })}\n` : `${kind}: ${JSON.stringify(value)}\n`);
|
|
71
|
+
};
|
|
72
|
+
const onSignal = () => { stop(); void room?.disconnect().catch(() => undefined); void client.disconnect(); };
|
|
73
|
+
process.once("SIGINT", onSignal);
|
|
74
|
+
process.once("SIGTERM", onSignal);
|
|
75
|
+
let inputTask = null;
|
|
76
|
+
try {
|
|
77
|
+
room = await client.rooms.connect(roomId, {
|
|
78
|
+
after,
|
|
79
|
+
onRoom: (active) => {
|
|
80
|
+
room = active;
|
|
81
|
+
if (stopped) {
|
|
82
|
+
void active.disconnect();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
active.subscribeAll((event) => write("event", { event }));
|
|
86
|
+
active.onMembersChanged((members) => write("members", { members }));
|
|
87
|
+
active.onOutOfSync((from, to) => write("gap", { from, to }));
|
|
88
|
+
active.onSendError((failure) => write("error", { message: failure.message }));
|
|
89
|
+
active.onStateChange((state) => {
|
|
90
|
+
write("state", { state });
|
|
91
|
+
if (state === "closed" || state === "expired")
|
|
92
|
+
stop();
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
if (!stopped)
|
|
97
|
+
write("connected", { roomId, participantId: room.participantId, sequence: room.sequence });
|
|
98
|
+
if (!stopped && options.input) {
|
|
99
|
+
const active = room;
|
|
100
|
+
inputTask = (async () => {
|
|
101
|
+
let lineNumber = 0;
|
|
102
|
+
for await (const line of readRoomLines(process.stdin)) {
|
|
103
|
+
if (stopped)
|
|
104
|
+
break;
|
|
105
|
+
lineNumber++;
|
|
106
|
+
if (!line.trim())
|
|
107
|
+
continue;
|
|
108
|
+
try {
|
|
109
|
+
const input = parseRoomInput(line);
|
|
110
|
+
if (input.op === "presence")
|
|
111
|
+
await active.setPresence(input.presence);
|
|
112
|
+
else if (input.op === "send")
|
|
113
|
+
active.send(input.type, input.data);
|
|
114
|
+
else {
|
|
115
|
+
const result = await active.publish(input.type, input.data);
|
|
116
|
+
write("ack", { line: lineNumber, sequence: result.sequence, eventId: result.eventId });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (cause) {
|
|
120
|
+
write("input_error", { line: lineNumber, message: cause instanceof Error ? cause.message : String(cause) });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
})().catch((cause) => {
|
|
124
|
+
write("input_error", { message: cause instanceof Error ? cause.message : String(cause) });
|
|
125
|
+
process.exitCode = 1;
|
|
126
|
+
stop();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
await done;
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
if (options.input)
|
|
133
|
+
process.stdin.destroy();
|
|
134
|
+
await room?.disconnect().catch(() => undefined);
|
|
135
|
+
await client.disconnect();
|
|
136
|
+
await inputTask?.catch(() => undefined);
|
|
137
|
+
process.removeListener("SIGINT", onSignal);
|
|
138
|
+
process.removeListener("SIGTERM", onSignal);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createReadStream, createWriteStream } from "node:fs";
|
|
3
|
+
import { chmod, lstat, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import { Readable, Transform } from "node:stream";
|
|
7
|
+
import { pipeline } from "node:stream/promises";
|
|
8
|
+
import { createGunzip } from "node:zlib";
|
|
9
|
+
// Pin a global release tag after its helper artifacts are published.
|
|
10
|
+
export const RTC_HELPER_VERSION = "v2.67.0";
|
|
11
|
+
const MAX_BINARY_BYTES = 128 * 1024 * 1024;
|
|
12
|
+
async function digest(path) {
|
|
13
|
+
const hash = createHash("sha256");
|
|
14
|
+
await pipeline(createReadStream(path), hash);
|
|
15
|
+
return hash.digest("hex");
|
|
16
|
+
}
|
|
17
|
+
async function readText(response, limit) {
|
|
18
|
+
if (!response.body)
|
|
19
|
+
throw new Error("Empty RTC helper response");
|
|
20
|
+
const reader = response.body.getReader();
|
|
21
|
+
const parts = [];
|
|
22
|
+
let bytes = 0;
|
|
23
|
+
try {
|
|
24
|
+
for (;;) {
|
|
25
|
+
const { done, value } = await reader.read();
|
|
26
|
+
if (done)
|
|
27
|
+
break;
|
|
28
|
+
bytes += value.length;
|
|
29
|
+
if (bytes > limit)
|
|
30
|
+
throw new Error("RTC helper metadata exceeds the size limit");
|
|
31
|
+
parts.push(value);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
await reader.cancel();
|
|
36
|
+
}
|
|
37
|
+
return Buffer.concat(parts).toString("utf8");
|
|
38
|
+
}
|
|
39
|
+
export async function ensureRtcBinary(local) {
|
|
40
|
+
const override = local || process.env.COHUB_RTC_BIN?.trim();
|
|
41
|
+
if (override) {
|
|
42
|
+
if (!(await lstat(override)).isFile())
|
|
43
|
+
throw new Error("RTC helper must be a regular file");
|
|
44
|
+
return resolve(override);
|
|
45
|
+
}
|
|
46
|
+
const platform = { linux: "linux", darwin: "darwin" }[process.platform];
|
|
47
|
+
const arch = { x64: "amd64", arm64: "arm64" }[process.arch];
|
|
48
|
+
if (!platform || !arch)
|
|
49
|
+
throw new Error("Unsupported RTC helper platform; use --helper with a local binary");
|
|
50
|
+
const version = process.env.COHUB_RTC_VERSION?.trim() || RTC_HELPER_VERSION;
|
|
51
|
+
if (!version)
|
|
52
|
+
throw new Error("No RTC helper release is pinned yet; use --helper or COHUB_RTC_VERSION with a published global release");
|
|
53
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/.test(version))
|
|
54
|
+
throw new Error("Invalid RTC helper version");
|
|
55
|
+
const directory = join(homedir(), ".cache", "cohub", "rtc", version, `${platform}-${arch}`);
|
|
56
|
+
const binary = join(directory, "cohub-rtc");
|
|
57
|
+
const checksum = `${binary}.sha256`;
|
|
58
|
+
const expected = await readFile(checksum, "utf8").catch(() => "");
|
|
59
|
+
if (/^[a-f0-9]{64}$/.test(expected) && (await lstat(binary).catch(() => null))?.isFile() && await digest(binary) === expected)
|
|
60
|
+
return binary;
|
|
61
|
+
await mkdir(directory, { recursive: true });
|
|
62
|
+
const temporary = await mkdtemp(join(directory, ".download-"));
|
|
63
|
+
const base = (process.env.COHUB_RTC_CDN_BASE_URL?.trim() || "https://public.cohub.live/rtc").replace(/\/+$/, "");
|
|
64
|
+
const url = `${base}/${version}/cohub-rtc_${platform}_${arch}.gz`;
|
|
65
|
+
const signal = AbortSignal.timeout(120_000);
|
|
66
|
+
try {
|
|
67
|
+
const manifest = await fetch(`${url}.sha256`, { signal });
|
|
68
|
+
if (!manifest.ok)
|
|
69
|
+
throw new Error(`RTC helper ${version} is unavailable (${manifest.status}); use --helper with a locally built cohub-rtc`);
|
|
70
|
+
const text = await readText(manifest, 1024);
|
|
71
|
+
const expectedArchive = text.trim().split(/\s+/)[0];
|
|
72
|
+
if (!expectedArchive || !/^[a-f0-9]{64}$/.test(expectedArchive))
|
|
73
|
+
throw new Error("Invalid RTC helper checksum");
|
|
74
|
+
const response = await fetch(url, { signal });
|
|
75
|
+
if (!response.ok || !response.body)
|
|
76
|
+
throw new Error(`RTC helper download failed (${response.status})`);
|
|
77
|
+
let bytes = 0;
|
|
78
|
+
const bound = () => new Transform({ transform(chunk, _encoding, done) {
|
|
79
|
+
bytes += chunk.length;
|
|
80
|
+
done(bytes > MAX_BINARY_BYTES ? new Error("RTC helper exceeds the size limit") : null, chunk);
|
|
81
|
+
} });
|
|
82
|
+
const archive = join(temporary, "archive.gz");
|
|
83
|
+
await pipeline(Readable.fromWeb(response.body), bound(), createWriteStream(archive, { flags: "wx" }), { signal });
|
|
84
|
+
if (await digest(archive) !== expectedArchive)
|
|
85
|
+
throw new Error("RTC helper checksum mismatch");
|
|
86
|
+
for (const name of ["LICENSE", "NOTICE"]) {
|
|
87
|
+
const response = await fetch(`${base}/${version}/${name}`, { signal });
|
|
88
|
+
if (!response.ok)
|
|
89
|
+
throw new Error(`RTC helper ${name} is unavailable (${response.status})`);
|
|
90
|
+
const text = await readText(response, 64 * 1024);
|
|
91
|
+
await writeFile(join(temporary, name), text, { flag: "wx" });
|
|
92
|
+
await rename(join(temporary, name), join(directory, name));
|
|
93
|
+
}
|
|
94
|
+
const extracted = join(temporary, "cohub-rtc");
|
|
95
|
+
bytes = 0;
|
|
96
|
+
await pipeline(createReadStream(archive), createGunzip(), bound(), createWriteStream(extracted, { flags: "wx", mode: 0o700 }), { signal });
|
|
97
|
+
await chmod(extracted, 0o700);
|
|
98
|
+
const hash = await digest(extracted);
|
|
99
|
+
await writeFile(join(temporary, "checksum"), hash, { flag: "wx" });
|
|
100
|
+
await rename(extracted, binary);
|
|
101
|
+
await rename(join(temporary, "checksum"), checksum);
|
|
102
|
+
return binary;
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
await rm(temporary, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type RtcOptions = {
|
|
2
|
+
helper?: string;
|
|
3
|
+
audio?: string;
|
|
4
|
+
video?: string;
|
|
5
|
+
audioFormat: "pcm" | "opus";
|
|
6
|
+
videoFormat: "i420" | "vp8";
|
|
7
|
+
size?: string;
|
|
8
|
+
fps: string;
|
|
9
|
+
rate: string;
|
|
10
|
+
channels: string;
|
|
11
|
+
output?: string;
|
|
12
|
+
receiveFormat: "rtp" | "raw";
|
|
13
|
+
receiveSize: string;
|
|
14
|
+
maxBytes: string;
|
|
15
|
+
participant?: string;
|
|
16
|
+
channel?: string;
|
|
17
|
+
delivery: "reliable" | "latest";
|
|
18
|
+
dataInput?: string;
|
|
19
|
+
json?: boolean;
|
|
20
|
+
};
|
|
21
|
+
export declare function runRoomRtc(roomId: string, options: RtcOptions): Promise<void>;
|
|
@@ -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/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,7 +18,7 @@
|
|
|
18
18
|
"NOTICE"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@neta-art/cohub": "11.
|
|
21
|
+
"@neta-art/cohub": "11.3.0",
|
|
22
22
|
"@neta-art/generation": "^0.1.35",
|
|
23
23
|
"commander": "^15.0.0",
|
|
24
24
|
"sharp": "^0.35.4",
|
|
@@ -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",
|