@neta-art/cohub-cli 9.1.2 → 10.1.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 +22 -0
- package/dist/auth.d.ts +2 -1
- package/dist/auth.js +39 -28
- 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 +3318 -990
- 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/commands/run.js +14 -2
- package/dist/endpoint-options.d.ts +4 -0
- package/dist/endpoint-options.js +26 -0
- package/dist/help-path.js +2 -2
- package/dist/index.js +8 -1
- package/dist/runtime/model-catalog.d.ts +1 -1
- package/dist/runtime/model-catalog.js +2 -4
- package/dist/runtime/supervisor.js +3 -3
- package/dist/safe-remote-image.d.ts +4 -0
- package/dist/safe-remote-image.js +30 -15
- package/dist/space.d.ts +1 -0
- package/dist/space.js +26 -18
- package/dist/web.js +2 -4
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
|
+
import { registerEndpointOptions } from "./endpoint-options.js";
|
|
4
5
|
import { registerAuth } from "./commands/auth.js";
|
|
5
6
|
import { registerBoards } from "./commands/boards.js";
|
|
6
7
|
import { registerChannels } from "./commands/channels.js";
|
|
@@ -21,6 +22,7 @@ import { registerRuntime } from "./commands/runtime.js";
|
|
|
21
22
|
import { registerTasks } from "./commands/tasks.js";
|
|
22
23
|
import { registerDesktop, registerLegacyUi } from "./commands/desktop.js";
|
|
23
24
|
import { registerApps } from "./commands/apps.js";
|
|
25
|
+
import { registerRooms } from "./commands/rooms.js";
|
|
24
26
|
import { formatUnknownCommandError, resolveHelpPath } from "./help-path.js";
|
|
25
27
|
const VERSION = (() => {
|
|
26
28
|
try {
|
|
@@ -31,7 +33,7 @@ const VERSION = (() => {
|
|
|
31
33
|
return "1.0.0";
|
|
32
34
|
}
|
|
33
35
|
})();
|
|
34
|
-
const program = new Command("cohub");
|
|
36
|
+
const program = registerEndpointOptions(new Command("cohub"));
|
|
35
37
|
program
|
|
36
38
|
.name("cohub")
|
|
37
39
|
.summary("Work with Cohub from your terminal")
|
|
@@ -63,6 +65,7 @@ Common commands:
|
|
|
63
65
|
cohub -s <space-id> public upload ./dist demo
|
|
64
66
|
cohub -s <space-id> apps publish demo --file dist/index.html
|
|
65
67
|
cohub desktop open <app-id> --call selection.get
|
|
68
|
+
cohub rooms join <invite>
|
|
66
69
|
cohub -s <space-id> spaces commerce products list
|
|
67
70
|
cohub models ls
|
|
68
71
|
cohub models ls --model-type multimodal
|
|
@@ -77,6 +80,9 @@ Target space:
|
|
|
77
80
|
Environment:
|
|
78
81
|
COHUB_SPACE_ID Target Space when -s is omitted
|
|
79
82
|
COHUB_EXECUTION_TOKEN Use this token instead of the stored Logto session
|
|
83
|
+
COHUB_BASE_URL Override the API base URL
|
|
84
|
+
COHUB_WS_URL Override the realtime WebSocket URL
|
|
85
|
+
COHUB_WEB_URL Override the web URL used in links
|
|
80
86
|
ENV=dev Use the development Cohub environment
|
|
81
87
|
HTTPS_PROXY Honored for API and uploads (also HTTP_PROXY, NO_PROXY)
|
|
82
88
|
Runtime logs ~/.local/state/cohub/runtime/<space-id>/diagnostics
|
|
@@ -100,6 +106,7 @@ registerReferrals(program);
|
|
|
100
106
|
registerTasks(program);
|
|
101
107
|
registerCronJobs(program);
|
|
102
108
|
registerApps(program);
|
|
109
|
+
registerRooms(program);
|
|
103
110
|
registerDesktop(program);
|
|
104
111
|
registerLegacyUi(program);
|
|
105
112
|
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)
|
|
@@ -102,10 +102,10 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
|
|
|
102
102
|
config.capabilities ?? discoverHarnesses(config.harnesses, config.executables, config.root),
|
|
103
103
|
]);
|
|
104
104
|
signal.throwIfAborted();
|
|
105
|
-
const url = new URL(resolveWebsocketUrl(
|
|
106
|
-
url.pathname = "/runtime/relay";
|
|
105
|
+
const url = new URL(resolveWebsocketUrl());
|
|
106
|
+
url.pathname = url.pathname.replace(/\/ws$/, "/runtime/relay");
|
|
107
107
|
const relay = new URL(url);
|
|
108
|
-
relay.pathname = "/sandbox/relay";
|
|
108
|
+
relay.pathname = relay.pathname.replace(/\/runtime\/relay$/, "/sandbox/relay");
|
|
109
109
|
const runBridge = async () => {
|
|
110
110
|
let attempts = 0;
|
|
111
111
|
while (!signal.aborted) {
|
|
@@ -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 {};
|
|
@@ -13,6 +13,11 @@ const IMAGE_MIME_TYPES = new Set([
|
|
|
13
13
|
"image/png",
|
|
14
14
|
"image/webp",
|
|
15
15
|
]);
|
|
16
|
+
const AUDIO_MIME_TYPES = new Set([
|
|
17
|
+
"audio/mpeg", "audio/mp3", "audio/mp4", "audio/aac", "audio/ogg",
|
|
18
|
+
"audio/wav", "audio/x-wav", "audio/wave", "audio/flac", "audio/x-flac",
|
|
19
|
+
"audio/webm", "audio/opus", "application/ogg",
|
|
20
|
+
]);
|
|
16
21
|
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
17
22
|
async function defaultLookup(hostname) {
|
|
18
23
|
const records = await dns.lookup(hostname, { all: true, verbatim: true });
|
|
@@ -48,7 +53,7 @@ function requestPinned(url, addresses, timeoutMs, maxBytes) {
|
|
|
48
53
|
const client = url.protocol === "https:" ? https : http;
|
|
49
54
|
const request = client.request(url, {
|
|
50
55
|
agent: false,
|
|
51
|
-
headers: { Accept: "image/
|
|
56
|
+
headers: { Accept: "image/*,audio/*,application/ogg" },
|
|
52
57
|
lookup: pinnedLookup(addresses),
|
|
53
58
|
}, (response) => {
|
|
54
59
|
const status = response.statusCode ?? 0;
|
|
@@ -61,7 +66,7 @@ function requestPinned(url, addresses, timeoutMs, maxBytes) {
|
|
|
61
66
|
const declaredLength = Number(headers.get("content-length") ?? 0);
|
|
62
67
|
if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
|
|
63
68
|
response.destroy();
|
|
64
|
-
reject(new Error(`
|
|
69
|
+
reject(new Error(`Media exceeds the ${maxBytes} byte download limit`));
|
|
65
70
|
return;
|
|
66
71
|
}
|
|
67
72
|
const chunks = [];
|
|
@@ -69,7 +74,7 @@ function requestPinned(url, addresses, timeoutMs, maxBytes) {
|
|
|
69
74
|
response.on("data", (chunk) => {
|
|
70
75
|
total += chunk.byteLength;
|
|
71
76
|
if (total > maxBytes) {
|
|
72
|
-
response.destroy(new Error(`
|
|
77
|
+
response.destroy(new Error(`Media exceeds the ${maxBytes} byte download limit`));
|
|
73
78
|
return;
|
|
74
79
|
}
|
|
75
80
|
chunks.push(chunk);
|
|
@@ -86,7 +91,7 @@ function requestPinned(url, addresses, timeoutMs, maxBytes) {
|
|
|
86
91
|
});
|
|
87
92
|
});
|
|
88
93
|
request.setTimeout(timeoutMs, () => {
|
|
89
|
-
request.destroy(new Error(`
|
|
94
|
+
request.destroy(new Error(`Media download timed out after ${timeoutMs}ms`));
|
|
90
95
|
});
|
|
91
96
|
request.once("error", reject);
|
|
92
97
|
request.end();
|
|
@@ -95,7 +100,7 @@ function requestPinned(url, addresses, timeoutMs, maxBytes) {
|
|
|
95
100
|
function remainingMs(deadline) {
|
|
96
101
|
const remaining = deadline - Date.now();
|
|
97
102
|
if (remaining <= 0)
|
|
98
|
-
throw new Error("
|
|
103
|
+
throw new Error("Media download timed out");
|
|
99
104
|
return remaining;
|
|
100
105
|
}
|
|
101
106
|
async function withDeadline(promise, deadline) {
|
|
@@ -105,7 +110,7 @@ async function withDeadline(promise, deadline) {
|
|
|
105
110
|
return await Promise.race([
|
|
106
111
|
promise,
|
|
107
112
|
new Promise((_, reject) => {
|
|
108
|
-
timer = setTimeout(() => reject(new Error(`
|
|
113
|
+
timer = setTimeout(() => reject(new Error(`Media download timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
109
114
|
}),
|
|
110
115
|
]);
|
|
111
116
|
}
|
|
@@ -117,7 +122,7 @@ async function withDeadline(promise, deadline) {
|
|
|
117
122
|
async function resolvePublicUrl(value, lookup, deadline) {
|
|
118
123
|
const normalized = normalizeBoardRemoteUrl(value);
|
|
119
124
|
if (!normalized)
|
|
120
|
-
throw new Error("
|
|
125
|
+
throw new Error("Media URL must be a public HTTP(S) URL");
|
|
121
126
|
const url = new URL(normalized);
|
|
122
127
|
const hostname = url.hostname.replace(/^\[|\]$/g, "");
|
|
123
128
|
const addresses = isIP(hostname)
|
|
@@ -126,11 +131,21 @@ async function resolvePublicUrl(value, lookup, deadline) {
|
|
|
126
131
|
const [first, ...rest] = addresses;
|
|
127
132
|
if (!first ||
|
|
128
133
|
addresses.some((entry) => !isPublicBoardRemoteAddress(entry.address))) {
|
|
129
|
-
throw new Error("
|
|
134
|
+
throw new Error("Media URL resolves to a private address");
|
|
130
135
|
}
|
|
131
136
|
return { url, addresses: [first, ...rest] };
|
|
132
137
|
}
|
|
133
|
-
export
|
|
138
|
+
export function downloadPublicImage(input, options = {}) {
|
|
139
|
+
return downloadPublicMedia(input, IMAGE_MIME_TYPES, options);
|
|
140
|
+
}
|
|
141
|
+
export function downloadPublicAudio(input, options = {}) {
|
|
142
|
+
return downloadPublicMedia(input, AUDIO_MIME_TYPES, {
|
|
143
|
+
maxBytes: 64 * 1024 * 1024,
|
|
144
|
+
timeoutMs: 30_000,
|
|
145
|
+
...options,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
async function downloadPublicMedia(input, mimeTypes, options) {
|
|
134
149
|
const lookup = options.lookup ?? defaultLookup;
|
|
135
150
|
const requester = options.requester ?? requestPinned;
|
|
136
151
|
const maxBytes = options.maxBytes ?? REMOTE_IMAGE_MAX_BYTES;
|
|
@@ -142,9 +157,9 @@ export async function downloadPublicImage(input, options = {}) {
|
|
|
142
157
|
if (REDIRECT_STATUSES.has(response.status)) {
|
|
143
158
|
const location = response.headers.get("location");
|
|
144
159
|
if (!location)
|
|
145
|
-
throw new Error("
|
|
160
|
+
throw new Error("Media redirect is missing a location");
|
|
146
161
|
if (redirect === MAX_REDIRECTS)
|
|
147
|
-
throw new Error("Too many
|
|
162
|
+
throw new Error("Too many media redirects");
|
|
148
163
|
current = new URL(location, url).toString();
|
|
149
164
|
continue;
|
|
150
165
|
}
|
|
@@ -152,17 +167,17 @@ export async function downloadPublicImage(input, options = {}) {
|
|
|
152
167
|
throw new Error(`HTTP ${response.status}`);
|
|
153
168
|
}
|
|
154
169
|
if (response.bytes.byteLength > maxBytes) {
|
|
155
|
-
throw new Error(`
|
|
170
|
+
throw new Error(`Media exceeds the ${maxBytes} byte download limit`);
|
|
156
171
|
}
|
|
157
172
|
const mimeType = response.headers
|
|
158
173
|
.get("content-type")
|
|
159
174
|
?.split(";", 1)[0]
|
|
160
175
|
?.trim()
|
|
161
176
|
.toLowerCase();
|
|
162
|
-
if (!mimeType || !
|
|
163
|
-
throw new Error("Remote
|
|
177
|
+
if (!mimeType || !mimeTypes.has(mimeType)) {
|
|
178
|
+
throw new Error("Remote resource has an unsupported media type");
|
|
164
179
|
}
|
|
165
180
|
return { bytes: response.bytes, mimeType };
|
|
166
181
|
}
|
|
167
|
-
throw new Error("Too many
|
|
182
|
+
throw new Error("Too many media redirects");
|
|
168
183
|
}
|
package/dist/space.d.ts
CHANGED
package/dist/space.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { HOME_SPACE_SLUG, parseSpaceRef, resolveCohubEnvironment } from "@neta-art/cohub";
|
|
4
|
+
import { COHUB_ENVIRONMENTS, HOME_SPACE_SLUG, normalizeBaseUrl, parseSpaceRef, resolveApiBaseUrl, resolveCohubEnvironment } from "@neta-art/cohub";
|
|
5
5
|
import { readAuthSession } from "./auth.js";
|
|
6
6
|
import { createClient } from "./client.js";
|
|
7
7
|
import { getRuntimeSpaceBinding } from "./runtime/space-binding.js";
|
|
@@ -40,17 +40,21 @@ function jwtClaim(token, key) {
|
|
|
40
40
|
* Execution grants identify the actor as `actorUserId`, not `sub`.
|
|
41
41
|
*/
|
|
42
42
|
export function identityKeyFrom(input) {
|
|
43
|
+
const baseUrl = input.baseUrl ? normalizeBaseUrl(input.baseUrl) : "";
|
|
44
|
+
const defaultBaseUrl = COHUB_ENVIRONMENTS[input.env === "dev" ? "dev" : "prod"].apiBaseUrl;
|
|
45
|
+
const namespace = baseUrl && baseUrl !== defaultBaseUrl ? `${input.env}:${baseUrl}` : input.env;
|
|
43
46
|
if (input.executionToken) {
|
|
44
47
|
const actor = jwtClaim(input.executionToken, "actorUserId") ?? jwtClaim(input.executionToken, "sub");
|
|
45
|
-
return actor ? `${
|
|
48
|
+
return actor ? `${namespace}:${actor}` : null;
|
|
46
49
|
}
|
|
47
50
|
const sub = jwtClaim(input.idToken, "sub") ?? jwtClaim(input.accessToken, "sub");
|
|
48
|
-
return sub ? `${
|
|
51
|
+
return sub ? `${namespace}:${sub}` : null;
|
|
49
52
|
}
|
|
50
53
|
export function currentIdentityKey() {
|
|
51
54
|
const session = readAuthSession();
|
|
52
55
|
return identityKeyFrom({
|
|
53
56
|
env: resolveCohubEnvironment(),
|
|
57
|
+
baseUrl: resolveApiBaseUrl(),
|
|
54
58
|
executionToken: process.env.COHUB_EXECUTION_TOKEN?.trim(),
|
|
55
59
|
idToken: session?.idToken,
|
|
56
60
|
accessToken: session?.accessToken,
|
|
@@ -81,10 +85,10 @@ function writeHomeSpaceCache(key, spaceId) {
|
|
|
81
85
|
// Cache is best-effort; never fail the command over it.
|
|
82
86
|
}
|
|
83
87
|
}
|
|
84
|
-
|
|
88
|
+
const homeSpaces = new Map();
|
|
85
89
|
const spaceRefs = new Map();
|
|
86
90
|
export function clearHomeSpaceCache() {
|
|
87
|
-
|
|
91
|
+
homeSpaces.clear();
|
|
88
92
|
spaceRefs.clear();
|
|
89
93
|
try {
|
|
90
94
|
rmSync(CACHE_PATH, { force: true });
|
|
@@ -107,17 +111,21 @@ export function explicitSpace(program) {
|
|
|
107
111
|
}
|
|
108
112
|
/** Cached per identity and memoized per process. */
|
|
109
113
|
export function resolveHomeSpace() {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
114
|
+
const key = currentIdentityKey();
|
|
115
|
+
const memoKey = key ?? resolveApiBaseUrl();
|
|
116
|
+
let resolved = homeSpaces.get(memoKey);
|
|
117
|
+
if (!resolved)
|
|
118
|
+
resolved = (async () => {
|
|
119
|
+
const cached = key ? readHomeSpaceCache(CACHE_PATH, key) : null;
|
|
120
|
+
if (cached)
|
|
121
|
+
return cached;
|
|
122
|
+
const space = await createClient().spaces.ensureHome();
|
|
123
|
+
if (key)
|
|
124
|
+
writeHomeSpaceCache(key, space.id);
|
|
125
|
+
return space.id;
|
|
126
|
+
})();
|
|
127
|
+
homeSpaces.set(memoKey, resolved);
|
|
128
|
+
return resolved;
|
|
121
129
|
}
|
|
122
130
|
async function lookupSpaceRef(value) {
|
|
123
131
|
const ref = parseSpaceRef(value);
|
|
@@ -141,10 +149,10 @@ async function lookupSpaceRef(value) {
|
|
|
141
149
|
}
|
|
142
150
|
}
|
|
143
151
|
export function resolveSpaceRef(value) {
|
|
144
|
-
const key = value.trim()
|
|
152
|
+
const key = `${currentIdentityKey() ?? resolveApiBaseUrl()}:${value.trim()}`;
|
|
145
153
|
let resolved = spaceRefs.get(key);
|
|
146
154
|
if (!resolved) {
|
|
147
|
-
resolved = lookupSpaceRef(
|
|
155
|
+
resolved = lookupSpaceRef(value.trim());
|
|
148
156
|
spaceRefs.set(key, resolved);
|
|
149
157
|
}
|
|
150
158
|
return resolved;
|
package/dist/web.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveWebBaseUrl } from "@neta-art/cohub";
|
|
2
2
|
export function webUrl(path) {
|
|
3
|
-
|
|
4
|
-
`https://${resolveCohubEnvironment() === "prod" ? "" : "dev."}cohub.live`;
|
|
5
|
-
return `${origin}${path}`;
|
|
3
|
+
return `${resolveWebBaseUrl()}${path}`;
|
|
6
4
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neta-art/cohub-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "10.1.0",
|
|
4
4
|
"description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"NOTICE"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@neta-art/cohub": "
|
|
22
|
-
"@neta-art/generation": "^0.1.
|
|
21
|
+
"@neta-art/cohub": "11.1.0",
|
|
22
|
+
"@neta-art/generation": "^0.1.33",
|
|
23
23
|
"commander": "^15.0.0",
|
|
24
24
|
"sharp": "^0.35.4",
|
|
25
25
|
"ws": "^8.21.3"
|