@neta-art/cohub-cli 9.0.0 → 9.1.1

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.
@@ -145,7 +145,7 @@ function registerExportCommand(boards) {
145
145
  .option("--scale <factor>", "Output pixels per board unit", "2")
146
146
  .option("--padding <units>", "Padding around the content in board units")
147
147
  .option("--theme <mode>", "dark or light", "dark")
148
- .option("--background <mode>", "paper or transparent", "paper")
148
+ .option("--paper <mode>", "Output paper: paper or transparent", "paper")
149
149
  .option("--format <format>", `Override the format (${BOARD_EXPORT_FORMATS.join(", ")}, ${BOARD_VIDEO_FORMATS.join(", ")})`)
150
150
  .option("--quality <q>", "JPEG/WebP quality from 0 to 1", "0.92")
151
151
  .option("--fps <rate>", "Video frame rate; defaults to the --at step")
@@ -189,7 +189,7 @@ Examples:
189
189
  scale: parseNumber(options.scale ?? "2", "--scale", { min: 0.01, max: 16 }),
190
190
  ...(options.padding === undefined ? {} : { padding: parseNumber(options.padding, "--padding", { min: 0 }) }),
191
191
  colorScheme: parseChoice(options.theme, "--theme", ["dark", "light"]),
192
- background: parseChoice(options.background, "--background", ["paper", "transparent"]),
192
+ background: parseChoice(options.paper, "--paper", ["paper", "transparent"]),
193
193
  format: videoFormat,
194
194
  fps,
195
195
  output: out,
@@ -213,7 +213,7 @@ Examples:
213
213
  scale: parseNumber(options.scale ?? "2", "--scale", { min: 0.01, max: 16 }),
214
214
  ...(options.padding === undefined ? {} : { padding: parseNumber(options.padding, "--padding", { min: 0 }) }),
215
215
  colorScheme: parseChoice(options.theme, "--theme", ["dark", "light"]),
216
- background: parseChoice(options.background, "--background", ["paper", "transparent"]),
216
+ background: parseChoice(options.paper, "--paper", ["paper", "transparent"]),
217
217
  format: parseExportFormat(options, out),
218
218
  quality: parseNumber(options.quality ?? "0.92", "--quality", { min: 0, max: 1 }),
219
219
  withImages: options.images !== false,
@@ -252,6 +252,9 @@ A Board is one JSON document: { board, items, animations }.
252
252
  get read it
253
253
  apply write it with a JSON Merge Patch; null deletes
254
254
  schema the schema of any part, and what tracks can animate
255
+ preset tracks for a preset motion, ready to apply
256
+ history list versions, or restore one
257
+ export render an image, a frame sequence or a video
255
258
  examples starter documents
256
259
  Times are milliseconds; 500ms, 12.5s and 2m also work on the command line.`);
257
260
  withJson(boards.command("create <path>")
@@ -288,7 +291,7 @@ Times are milliseconds; 500ms, 12.5s and 2m also work on the command line.`);
288
291
  });
289
292
  boards.command("get <board>")
290
293
  .description("Print the Board document as JSON")
291
- .option("--only <sections>", "items, animations, or both (comma-separated)")
294
+ .option("--only <sections>", "board, items, animations (comma-separated)")
292
295
  .option("--items <ids>", "Only these items")
293
296
  .option("--within <frame-id>", "A frame and everything inside it")
294
297
  .option("--rect <rect>", "Items intersecting x,y,width,height")
@@ -298,14 +301,15 @@ Times are milliseconds; 500ms, 12.5s and 2m also work on the command line.`);
298
301
  .addHelpText("after", `
299
302
  cohub boards get plan.board
300
303
  cohub boards get plan.board --within s2
301
- cohub boards get plan.board --only animations`)
304
+ cohub boards get plan.board --only board`)
302
305
  .action(async (target, options) => {
303
306
  try {
304
307
  const only = list(options.only);
305
- if (only?.some((section) => section !== "items" && section !== "animations"))
306
- throw new Error("--only takes items, animations, or both.");
308
+ if (only?.some((section) => section !== "board" && section !== "items" && section !== "animations"))
309
+ throw new Error("--only takes board, items, animations, or a combination.");
310
+ const sections = only ? new Set(only) : null;
307
311
  const input = {
308
- ...(only ? { only: only } : {}),
312
+ ...(only ? { only: only.filter((section) => section !== "board") } : {}),
309
313
  ...(list(options.items) ? { items: list(options.items) } : {}),
310
314
  ...(options.within ? { within: options.within } : {}),
311
315
  ...(options.rect ? { rect: parseRect(options.rect) } : {}),
@@ -313,7 +317,20 @@ Times are milliseconds; 500ms, 12.5s and 2m also work on the command line.`);
313
317
  ...(options.limit ? { limit: parseNumber(options.limit, "--limit", { min: 1, integer: true }) } : {}),
314
318
  ...(options.cursor ? { cursor: options.cursor } : {}),
315
319
  };
316
- outJson(await (await boardClient(boards, target)).get(input));
320
+ const result = await (await boardClient(boards, target)).get(input);
321
+ if (!sections)
322
+ return outJson(result);
323
+ outJson({
324
+ id: result.id,
325
+ title: result.title,
326
+ version: result.version,
327
+ updatedAt: result.updatedAt,
328
+ playback: result.playback,
329
+ ...(sections.has("board") ? { board: result.board } : {}),
330
+ ...(sections.has("items") && result.items ? { items: result.items } : {}),
331
+ ...(sections.has("animations") && result.animations ? { animations: result.animations } : {}),
332
+ ...(result.next ? { next: result.next } : {}),
333
+ });
317
334
  }
318
335
  catch (cause) {
319
336
  handleHttp(cause);
@@ -393,6 +410,7 @@ Times are milliseconds; 500ms, 12.5s and 2m also work on the command line.`);
393
410
  boards.command("schema [target]")
394
411
  .description("Print JSON Schema for a part of a Board")
395
412
  .addHelpText("after", `
413
+ With a target: one item type, or animation, track, or camera.
396
414
  Targets: ${BOARD_SCHEMA_TARGETS.join(", ")}
397
415
  Without a target: units, item types, colors and every animatable property.`)
398
416
  .action((target) => {
@@ -411,14 +429,15 @@ Without a target: units, item types, colors and every animatable property.`)
411
429
  boards.command("preset <name>")
412
430
  .description("Print tracks for a preset motion as a patch for apply")
413
431
  .requiredOption("--targets <ids>", "Comma-separated item ids")
414
- .requiredOption("--animation <id>", "Animation to add the tracks to")
432
+ .option("--animation <id>", "Wrap the tracks in this animation; omit to get bare tracks")
415
433
  .option("--at <time>", "Start of the first target", "0")
416
434
  .option("--stagger <time>", "Delay added per target", "0")
417
435
  .option("--duration <time>", "Length of each target's motion")
418
436
  .option("--ease <ease>", "CSS easing, e.g. ease-out or cubic-bezier(.2,.8,.2,1)")
419
437
  .addHelpText("after", `
420
438
  Presets: ${BOARD_PRESET_NAMES.join(", ")}
421
- cohub boards preset rise --targets a,b,c --animation intro --stagger 120ms | cohub boards apply plan.board -i -`)
439
+ cohub boards preset rise --targets a,b,c --animation intro --stagger 120ms | cohub boards apply plan.board -i -
440
+ cohub boards preset float --targets ship | jq -c '{animations:{idle:.}}' | cohub boards apply plan.board -i -`)
422
441
  .action((name, options) => {
423
442
  try {
424
443
  if (!isBoardPresetName(name))
@@ -433,7 +452,7 @@ Presets: ${BOARD_PRESET_NAMES.join(", ")}
433
452
  ...(options.duration ? { duration: parseBoardTime(options.duration, "--duration") } : {}),
434
453
  ...(options.ease ? { ease: options.ease } : {}),
435
454
  });
436
- outJson({ animations: { [options.animation]: { tracks } } });
455
+ outJson(options.animation ? { animations: { [options.animation]: { tracks } } } : { tracks });
437
456
  }
438
457
  catch (cause) {
439
458
  handleHttp(cause);
@@ -41,8 +41,9 @@ export function registerRuntime(program) {
41
41
  .option("--harness <name>", "Pi or Codex; repeatable", (value, previous) => [...previous, value], [])
42
42
  .option("--pi <path>", "Pi executable")
43
43
  .option("--codex <path>", "Codex executable")
44
- .option("--display [screen]", "Share a screen: auto (this computer), or xvfb[:WIDTHxHEIGHT] for a virtual one")
45
- .option("-y, --yes", "Accept defaults and authorize local execution and native sync")
44
+ .option("--display [screen]", "Share a screen (default: auto when available), or xvfb[:WIDTHxHEIGHT] for a virtual one")
45
+ .option("--no-display", "Disable screen sharing and skip its confirmation")
46
+ .option("-y, --yes", "Accept defaults and authorize local execution, native sync, and screen sharing")
46
47
  .option("--verbose", "Show diagnostic details")
47
48
  .option("--json", "JSON output")
48
49
  .action(async (dir, options) => {
@@ -1,4 +1,4 @@
1
- export declare const SANDBOXD_VERSION = "v2.55.0";
1
+ export declare const SANDBOXD_VERSION = "v2.61.1";
2
2
  export declare class SandboxdDownloadError extends Error {
3
3
  name: string;
4
4
  }
@@ -6,29 +6,8 @@ import { homedir, tmpdir } from "node:os";
6
6
  import { dirname, join } from "node:path";
7
7
  import { pipeline } from "node:stream/promises";
8
8
  import { Readable } from "node:stream";
9
- // The sandboxd binary version is pinned independently of the CLI package: it
10
- // only needs to change when apps/sandbox changes, and the agent-sandbox wire
11
- // protocol ("1") guarantees backward compatibility.
12
- //
13
- // IMPORTANT: this must point at a tag whose CDN artifacts have already been
14
- // published by .github/workflows/sandbox-binaries-build.yml. Only bump it AFTER
15
- // that tag's publish-cdn job has succeeded, otherwise `runtime up` 404s on the
16
- // default download.
17
- //
18
- // v2.54.0 is the first published tag with the private managed Runtime control
19
- // pipe (`COHUB_RUNTIME_MANAGED` over fd 3), so `runtime up` reads connection
20
- // state from the daemon instead of polling the API every five seconds. It also
21
- // carries the optional workspace-search runner download. v2.53.1 already has
22
- // the native FSEvents backends and the `runtimeId` control frame, and older
23
- // releases stay usable through the compatibility readiness/restart path.
24
- //
25
- // v2.54.1 keeps the same wire protocol and adds relay diagnostics: data-channel
26
- // pairing outlives the runner's dial timeout, dial failures distinguish a
27
- // timeout from an explicit rejection, and teardown-time websocket write
28
- // failures log at debug instead of warn. v2.55.0 republishes that compatible
29
- // runner alongside the matching platform release; no new runner capability is
30
- // required.
31
- export const SANDBOXD_VERSION = "v2.55.0";
9
+ // Pin only after all platform artifacts are published to the public CDN.
10
+ export const SANDBOXD_VERSION = "v2.61.1";
32
11
  const BINARY_NAME = "cohub-sandboxd";
33
12
  // Public CDN prefix hosting the release archives (the repo is private, so the
34
13
  // GitHub Release assets are not publicly downloadable). Overridable for staging
package/dist/index.js CHANGED
@@ -56,7 +56,7 @@ Common commands:
56
56
  cohub runtime up ./my-project
57
57
  cohub runtime logs --follow
58
58
  cohub search "release notes"
59
- cohub -s <space-id> boards inspect <board-id>
59
+ cohub -s <space-id> boards get <board-id>
60
60
  cohub -s <space-id> spaces turns ls --author others
61
61
  cohub -s <space-id> spaces sessions turns ls <session-id>
62
62
  cohub -s home spaces files ls
@@ -0,0 +1,3 @@
1
+ export declare const isFetchFailure: (error: unknown) => error is Error;
2
+ export declare function fetchFailureReason(error: unknown): string | null;
3
+ export declare function describeError(error: unknown): string;
@@ -0,0 +1,22 @@
1
+ const text = (value) => (typeof value === "string" && value ? value : null);
2
+ export const isFetchFailure = (error) => error instanceof Error && error.message === "fetch failed";
3
+ export function fetchFailureReason(error) {
4
+ if (!isFetchFailure(error) || typeof error.cause !== "object" || !error.cause)
5
+ return null;
6
+ const cause = error.cause;
7
+ const message = text(cause.message);
8
+ const code = text(cause.code);
9
+ const hostname = text(cause.hostname);
10
+ const parts = [
11
+ code && !message?.includes(code) ? code : null,
12
+ hostname && !message?.includes(hostname) ? `host: ${hostname}` : null,
13
+ message,
14
+ ].filter(Boolean);
15
+ return parts.length > 0 ? parts.join(" · ") : null;
16
+ }
17
+ export function describeError(error) {
18
+ if (!(error instanceof Error))
19
+ return String(error);
20
+ const reason = fetchFailureReason(error);
21
+ return reason ? `${error.message} (${reason})` : error.message;
22
+ }
package/dist/output.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import process from "node:process";
2
2
  import { extractBillingPayload } from "@neta-art/cohub";
3
+ import { fetchFailureReason, isFetchFailure } from "./network-error.js";
3
4
  /**
4
5
  * Timestamp → local ISO-style date and time with the system's UTC offset.
5
6
  *
@@ -146,16 +147,10 @@ function debugErrorMetaFromBody(body) {
146
147
  return items;
147
148
  }
148
149
  function fetchFailureDetail(e) {
149
- if (!(e instanceof Error) || e.message !== "fetch failed")
150
+ if (!isFetchFailure(e))
150
151
  return null;
151
- const cause = e.cause;
152
- const code = typeof cause?.code === "string" ? cause.code : null;
153
- const hostname = typeof cause?.hostname === "string" ? cause.hostname : null;
154
- const message = typeof cause?.message === "string" ? cause.message : null;
155
- const parts = [code, hostname && `host: ${hostname}`, message].filter(Boolean);
156
- return parts.length > 0
157
- ? `Network request failed (${parts.join(" · ")}). Check DNS/proxy/firewall settings and try again.`
158
- : "Network request failed. Check DNS/proxy/firewall settings and try again.";
152
+ const reason = fetchFailureReason(e);
153
+ return `Network request failed${reason ? ` (${reason})` : ""}. Check DNS/proxy/firewall settings and try again.`;
159
154
  }
160
155
  function errorPresentationFromHttpError(e) {
161
156
  const httpError = e;
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { setTimeout as delay } from "node:timers/promises";
3
3
  import { RUNTIME_MAX_FRAME_BYTES, RUNTIME_PROTOCOL_VERSION, runtimeCommandSchema, runtimeReadySchema, runtimeNativeStopSchema, } from "@neta-art/cohub";
4
+ import { describeError } from "../network-error.js";
4
5
  import { executeTurn } from "./native/execution.js";
5
6
  import { ContextRequiredError } from "./native/results.js";
6
7
  import { ProcessCleanupUncertainError } from "./process-group.js";
@@ -579,7 +580,7 @@ async function connect(options) {
579
580
  error: serializeDiagnosticError(error),
580
581
  }, context);
581
582
  try {
582
- emit({ type: "turn.error", message: error instanceof Error ? error.message : String(error), uncertain: !!execution.result || frame.resumeOnly === true || error instanceof ProcessCleanupUncertainError });
583
+ emit({ type: "turn.error", message: describeError(error), uncertain: !!execution.result || frame.resumeOnly === true || error instanceof ProcessCleanupUncertainError });
583
584
  }
584
585
  catch {
585
586
  // Native files remain for recovery.
@@ -16,7 +16,7 @@ export type RuntimeUpOptions = {
16
16
  };
17
17
  export declare const resolveLocalSpaceName: (root: string, name?: string) => string;
18
18
  export declare function parseRuntimeHarnesses(values: string[]): ("pi" | "codex")[];
19
- export declare function parseRuntimeDisplay(value: string | boolean | undefined): string | undefined;
19
+ export declare function parseRuntimeDisplay(value: string | boolean | undefined, platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): string | undefined;
20
20
  export declare function resolveRuntimeTarget(program: Command, target?: string): Promise<string>;
21
21
  export declare function startBackgroundRuntime(config: RuntimeLaunch): Promise<RuntimeSummary>;
22
22
  export declare function runtimeUp(program: Command, dir: string | undefined, options: RuntimeUpOptions): Promise<void>;
@@ -20,15 +20,17 @@ export function parseRuntimeHarnesses(values) {
20
20
  throw new Error("Harness must be pi or codex");
21
21
  return [...new Set(names.length ? names : ["pi"])];
22
22
  }
23
- export function parseRuntimeDisplay(value) {
24
- if (value === undefined || value === false)
23
+ export function parseRuntimeDisplay(value, platform = process.platform, env = process.env) {
24
+ if (value === false)
25
25
  return undefined;
26
- const spec = value === true ? "auto" : value.trim();
26
+ if (value === undefined && platform !== "darwin" && (platform !== "linux" || !env.DISPLAY))
27
+ return undefined;
28
+ const spec = value === undefined || value === true ? "auto" : value.trim();
27
29
  if (!/^(auto|macos|x11(:[\w.:-]+)?|xvfb(:\d{3,4}x\d{3,4})?)$/.test(spec)) {
28
30
  throw new Error("Display must be auto, macos, x11[:display] or xvfb[:WIDTHxHEIGHT]");
29
31
  }
30
- if (process.platform === "linux" && spec === "auto" && !process.env.DISPLAY) {
31
- throw new Error(process.env.WAYLAND_DISPLAY
32
+ if (platform === "linux" && spec === "auto" && !env.DISPLAY) {
33
+ throw new Error(env.WAYLAND_DISPLAY
32
34
  ? "Wayland sessions cannot be shared yet: log in with an X11 session, or use --display xvfb for a virtual screen"
33
35
  : "No X11 session to share (DISPLAY is unset): use --display xvfb for a virtual screen");
34
36
  }
@@ -106,11 +108,11 @@ export async function runtimeUp(program, dir, options) {
106
108
  const binding = await getRuntimeSpaceBinding(root, identity);
107
109
  const existingId = requested || binding?.spaceId;
108
110
  let harnesses = parseRuntimeHarnesses(options.harness);
109
- let display = parseRuntimeDisplay(options.display);
111
+ let display = options.display === undefined ? undefined : parseRuntimeDisplay(options.display);
110
112
  if (existingId && !options.new) {
111
113
  const existing = await requestRuntimeInstance(runtimeInstanceDirectory(identity, existingId));
112
114
  if (existing) {
113
- if (existing.root !== root || options.harness.length && [...existing.harnesses].sort().join() !== [...harnesses].sort().join() || options.pi || options.codex || display && display !== existing.display) {
115
+ if (existing.root !== root || options.harness.length && [...existing.harnesses].sort().join() !== [...harnesses].sort().join() || options.pi || options.codex || options.display !== undefined && display !== existing.display) {
114
116
  throw new Error("Runtime is running with a different configuration. Use down first");
115
117
  }
116
118
  // `up` is the single idempotent entry: a reused instance adopts its running
@@ -125,6 +127,7 @@ export async function runtimeUp(program, dir, options) {
125
127
  return;
126
128
  }
127
129
  }
130
+ display = parseRuntimeDisplay(options.display);
128
131
  if (!options.harness.length)
129
132
  harnesses = await installedHarnesses(root, options);
130
133
  if (!harnesses.length)
@@ -157,8 +160,8 @@ export async function runtimeUp(program, dir, options) {
157
160
  if (/^n(o)?$/i.test(answer.trim()))
158
161
  return;
159
162
  if (display && !isVirtualDisplay(display)) {
160
- const share = await rl.question("Collaborators and agents can see and control this screen. Share it? [y/N] ");
161
- if (!/^y(es)?$/i.test(share.trim()))
163
+ const share = await rl.question("Collaborators and agents can see and control this screen. Share it? [Y/n] ");
164
+ if (!/^(y(es)?)?$/i.test(share.trim()))
162
165
  display = undefined;
163
166
  }
164
167
  }
@@ -1,5 +1,6 @@
1
1
  import { imageForPi } from "../harness.js";
2
2
  import { JsonRpcProcess, record, RpcProcessClosedError } from "../json-rpc.js";
3
+ import { describeError } from "../../network-error.js";
3
4
  import { ProcessCleanupUncertainError } from "../process-group.js";
4
5
  import { serializeDiagnosticError } from "../diagnostics.js";
5
6
  import { openPrivateServer } from "./codex-link.js";
@@ -164,7 +165,7 @@ async function runPi(executor, turn, context) {
164
165
  await closeHost(host, null).catch(() => undefined);
165
166
  throw error;
166
167
  }
167
- message = { ...message, stopReason: signal.aborted ? "aborted" : "error", errorMessage: signal.aborted ? null : error instanceof Error ? error.message : String(error) };
168
+ message = { ...message, stopReason: signal.aborted ? "aborted" : "error", errorMessage: signal.aborted ? null : describeError(error) };
168
169
  }
169
170
  return { session, resume: prepared.resume, message, host };
170
171
  }
@@ -306,7 +307,7 @@ async function runCodex(executor, turn, context) {
306
307
  await closeHost(host, null).catch(() => undefined);
307
308
  throw error;
308
309
  }
309
- message = { ...message, stopReason: signal.aborted ? "aborted" : "error", errorMessage: signal.aborted ? null : error instanceof Error ? error.message : String(error) };
310
+ message = { ...message, stopReason: signal.aborted ? "aborted" : "error", errorMessage: signal.aborted ? null : describeError(error) };
310
311
  }
311
312
  return { session, resume: prepared.resume, message, host };
312
313
  }
@@ -1,22 +1,26 @@
1
+ import { type LookupFunction } from "node:net";
1
2
  export declare const REMOTE_IMAGE_MAX_BYTES: number;
2
3
  export declare const REMOTE_IMAGE_TIMEOUT_MS = 15000;
3
4
  type ResolvedAddress = {
4
5
  address: string;
5
6
  family: 4 | 6;
6
7
  };
8
+ type PinnedAddresses = readonly [ResolvedAddress, ...ResolvedAddress[]];
7
9
  type Lookup = (hostname: string) => Promise<readonly ResolvedAddress[]>;
8
10
  type RemoteResponse = {
9
11
  status: number;
10
12
  headers: Headers;
11
13
  bytes: Uint8Array;
12
14
  };
13
- type Requester = (url: URL, address: ResolvedAddress, timeoutMs: number, maxBytes: number) => Promise<RemoteResponse>;
15
+ type Requester = (url: URL, addresses: PinnedAddresses, timeoutMs: number, maxBytes: number) => Promise<RemoteResponse>;
14
16
  export type RemoteImageDownloadOptions = {
15
17
  lookup?: Lookup;
16
18
  requester?: Requester;
17
19
  maxBytes?: number;
18
20
  timeoutMs?: number;
19
21
  };
22
+ /** Async like `dns.lookup`: a synchronous answer crashes TLS on an immediate connect failure. */
23
+ export declare function pinnedLookup(addresses: PinnedAddresses): LookupFunction;
20
24
  export declare function downloadPublicImage(input: string, options?: RemoteImageDownloadOptions): Promise<{
21
25
  bytes: Uint8Array;
22
26
  mimeType: string;
@@ -34,15 +34,22 @@ function responseHeaders(input) {
34
34
  }
35
35
  return headers;
36
36
  }
37
- function requestPinned(url, address, timeoutMs, maxBytes) {
37
+ /** Async like `dns.lookup`: a synchronous answer crashes TLS on an immediate connect failure. */
38
+ export function pinnedLookup(addresses) {
39
+ const [first] = addresses;
40
+ return (_hostname, options, callback) => {
41
+ process.nextTick(() => options.all
42
+ ? callback(null, [...addresses])
43
+ : callback(null, first.address, first.family));
44
+ };
45
+ }
46
+ function requestPinned(url, addresses, timeoutMs, maxBytes) {
38
47
  return new Promise((resolve, reject) => {
39
48
  const client = url.protocol === "https:" ? https : http;
40
49
  const request = client.request(url, {
41
50
  agent: false,
42
51
  headers: { Accept: "image/avif,image/webp,image/png,image/jpeg,image/gif" },
43
- lookup: (_hostname, _options, callback) => {
44
- callback(null, address.address, address.family);
45
- },
52
+ lookup: pinnedLookup(addresses),
46
53
  }, (response) => {
47
54
  const status = response.statusCode ?? 0;
48
55
  const headers = responseHeaders(response.headers);
@@ -116,11 +123,12 @@ async function resolvePublicUrl(value, lookup, deadline) {
116
123
  const addresses = isIP(hostname)
117
124
  ? [{ address: hostname, family: isIP(hostname) }]
118
125
  : await withDeadline(lookup(hostname), deadline);
119
- if (addresses.length === 0 ||
126
+ const [first, ...rest] = addresses;
127
+ if (!first ||
120
128
  addresses.some((entry) => !isPublicBoardRemoteAddress(entry.address))) {
121
129
  throw new Error("Image URL resolves to a private address");
122
130
  }
123
- return { url, address: addresses[0] };
131
+ return { url, addresses: [first, ...rest] };
124
132
  }
125
133
  export async function downloadPublicImage(input, options = {}) {
126
134
  const lookup = options.lookup ?? defaultLookup;
@@ -129,8 +137,8 @@ export async function downloadPublicImage(input, options = {}) {
129
137
  const deadline = Date.now() + (options.timeoutMs ?? REMOTE_IMAGE_TIMEOUT_MS);
130
138
  let current = input;
131
139
  for (let redirect = 0; redirect <= MAX_REDIRECTS; redirect += 1) {
132
- const { url, address } = await resolvePublicUrl(current, lookup, deadline);
133
- const response = await withDeadline(requester(url, address, remainingMs(deadline), maxBytes), deadline);
140
+ const { url, addresses } = await resolvePublicUrl(current, lookup, deadline);
141
+ const response = await withDeadline(requester(url, addresses, remainingMs(deadline), maxBytes), deadline);
134
142
  if (REDIRECT_STATUSES.has(response.status)) {
135
143
  const location = response.headers.get("location");
136
144
  if (!location)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neta-art/cohub-cli",
3
- "version": "9.0.0",
3
+ "version": "9.1.1",
4
4
  "description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -18,11 +18,11 @@
18
18
  "NOTICE"
19
19
  ],
20
20
  "dependencies": {
21
+ "@neta-art/cohub": "9.0.1",
21
22
  "@neta-art/generation": "^0.1.30",
22
23
  "commander": "^15.0.0",
23
24
  "sharp": "^0.35.4",
24
- "ws": "^8.21.3",
25
- "@neta-art/cohub": "9.0.0"
25
+ "ws": "^8.21.3"
26
26
  },
27
27
  "publishConfig": {
28
28
  "access": "public"
@@ -30,7 +30,7 @@
30
30
  "devDependencies": {
31
31
  "@fontsource/geist": "^5.3.0",
32
32
  "@fontsource/geist-mono": "^5.3.0",
33
- "@types/node": "^26.4.0",
33
+ "@types/node": "26.6.4",
34
34
  "@types/ws": "^8.18.1",
35
35
  "pixi.js": "^8.20.1",
36
36
  "tsdown": "^0.22.14",