@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/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 built-in catalog is not authoritative for an arbitrary custom provider. */
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 built-in catalog is not authoritative for an arbitrary custom provider. */
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 customCatalog = typeof config.model_catalog_json === "string";
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({ url: process.env.COHUB_WS_URL }));
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/avif,image/webp,image/png,image/jpeg,image/gif" },
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(`Image exceeds the ${maxBytes} byte download limit`));
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(`Image exceeds the ${maxBytes} byte download limit`));
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(`Image download timed out after ${timeoutMs}ms`));
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("Image download timed out");
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(`Image download timed out after ${timeoutMs}ms`)), timeoutMs);
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("Image URL must be a public HTTP(S) URL");
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("Image URL resolves to a private address");
134
+ throw new Error("Media URL resolves to a private address");
130
135
  }
131
136
  return { url, addresses: [first, ...rest] };
132
137
  }
133
- export async function downloadPublicImage(input, options = {}) {
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("Image redirect is missing a location");
160
+ throw new Error("Media redirect is missing a location");
146
161
  if (redirect === MAX_REDIRECTS)
147
- throw new Error("Too many image redirects");
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(`Image exceeds the ${maxBytes} byte download limit`);
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 || !IMAGE_MIME_TYPES.has(mimeType)) {
163
- throw new Error("Remote background must be a supported raster image");
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 image redirects");
182
+ throw new Error("Too many media redirects");
168
183
  }
package/dist/space.d.ts CHANGED
@@ -11,6 +11,7 @@ export declare class SpaceTargetError extends Error {
11
11
  */
12
12
  export declare function identityKeyFrom(input: {
13
13
  env: string;
14
+ baseUrl?: string;
14
15
  executionToken?: string | null;
15
16
  idToken?: string | null;
16
17
  accessToken?: string | null;
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 ? `${input.env}:${actor}` : null;
48
+ return actor ? `${namespace}:${actor}` : null;
46
49
  }
47
50
  const sub = jwtClaim(input.idToken, "sub") ?? jwtClaim(input.accessToken, "sub");
48
- return sub ? `${input.env}:${sub}` : null;
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
- let homeSpacePromise = null;
88
+ const homeSpaces = new Map();
85
89
  const spaceRefs = new Map();
86
90
  export function clearHomeSpaceCache() {
87
- homeSpacePromise = null;
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
- homeSpacePromise ??= (async () => {
111
- const key = currentIdentityKey();
112
- const cached = key ? readHomeSpaceCache(CACHE_PATH, key) : null;
113
- if (cached)
114
- return cached;
115
- const space = await createClient().spaces.ensureHome();
116
- if (key)
117
- writeHomeSpaceCache(key, space.id);
118
- return space.id;
119
- })();
120
- return homeSpacePromise;
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(key);
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 { resolveCohubEnvironment } from "@neta-art/cohub";
1
+ import { resolveWebBaseUrl } from "@neta-art/cohub";
2
2
  export function webUrl(path) {
3
- const origin = process.env.COHUB_WEB_URL?.replace(/\/+$/, "") ??
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": "9.1.2",
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": "10.0.0",
22
- "@neta-art/generation": "^0.1.30",
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"