@neta-art/cohub-cli 8.4.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +25 -2
  2. package/dist/avatar.js +3 -1
  3. package/dist/board-export.d.ts +22 -41
  4. package/dist/board-export.js +178 -115
  5. package/dist/board-fonts.d.ts +7 -0
  6. package/dist/board-fonts.js +10 -0
  7. package/dist/board-kernel/browserAll.js +2760 -0
  8. package/dist/board-kernel/getPo2TextureFromSource.js +13138 -0
  9. package/dist/board-kernel/init.js +867 -0
  10. package/dist/board-kernel/webworkerAll.js +3 -0
  11. package/dist/board-kernel.d.ts +2 -0
  12. package/dist/board-kernel.js +28812 -0
  13. package/dist/board-time.d.ts +3 -0
  14. package/dist/board-time.js +28 -0
  15. package/dist/commands/apps.js +2 -2
  16. package/dist/commands/auth.js +2 -2
  17. package/dist/commands/boards/examples.d.ts +8 -2
  18. package/dist/commands/boards/examples.js +79 -411
  19. package/dist/commands/boards.d.ts +4 -9
  20. package/dist/commands/boards.js +356 -286
  21. package/dist/commands/desktop.d.ts +1 -5
  22. package/dist/commands/desktop.js +10 -19
  23. package/dist/commands/generations.js +1 -1
  24. package/dist/commands/me.js +1 -1
  25. package/dist/commands/public.js +3 -3
  26. package/dist/commands/references.js +3 -0
  27. package/dist/commands/referrals.js +2 -2
  28. package/dist/commands/run.js +3 -3
  29. package/dist/commands/runtime.d.ts +1 -1
  30. package/dist/commands/runtime.js +12 -8
  31. package/dist/commands/search.js +15 -7
  32. package/dist/commands/space-activity.js +3 -5
  33. package/dist/commands/space-displays.d.ts +2 -0
  34. package/dist/commands/space-displays.js +347 -0
  35. package/dist/commands/space-files.d.ts +5 -0
  36. package/dist/commands/space-files.js +215 -0
  37. package/dist/commands/space-invitations.js +3 -3
  38. package/dist/commands/spaces.d.ts +3 -11
  39. package/dist/commands/spaces.js +146 -75
  40. package/dist/fonts/geist-latin-400-normal.woff2 +0 -0
  41. package/dist/fonts/geist-latin-500-normal.woff2 +0 -0
  42. package/dist/fonts/geist-latin-600-normal.woff2 +0 -0
  43. package/dist/fonts/geist-latin-700-normal.woff2 +0 -0
  44. package/dist/fonts/geist-mono-latin-500-normal.woff2 +0 -0
  45. package/dist/fonts/geist-mono-latin-600-normal.woff2 +0 -0
  46. package/dist/fonts/geist-mono-latin-700-normal.woff2 +0 -0
  47. package/dist/index.js +7 -4
  48. package/dist/runtime/launch.d.ts +2 -0
  49. package/dist/runtime/launch.js +35 -8
  50. package/dist/runtime/presentation.d.ts +1 -0
  51. package/dist/runtime/presentation.js +3 -2
  52. package/dist/runtime/supervisor.d.ts +1 -0
  53. package/dist/runtime/supervisor.js +2 -1
  54. package/dist/self-update.d.ts +1 -0
  55. package/dist/self-update.js +52 -16
  56. package/dist/space-files/limit.d.ts +2 -0
  57. package/dist/space-files/limit.js +16 -0
  58. package/dist/space-files/operands.d.ts +16 -0
  59. package/dist/space-files/operands.js +33 -0
  60. package/dist/space-files/transfer.d.ts +53 -0
  61. package/dist/space-files/transfer.js +296 -0
  62. package/dist/space-files/upload.d.ts +14 -0
  63. package/dist/space-files/upload.js +53 -0
  64. package/dist/space.d.ts +18 -16
  65. package/dist/space.js +86 -39
  66. package/dist/web.d.ts +1 -0
  67. package/dist/web.js +6 -0
  68. package/package.json +8 -4
  69. package/dist/commands/board-domain.d.ts +0 -2
  70. package/dist/commands/board-domain.js +0 -16
  71. package/dist/commands/boards/animation.d.ts +0 -2
  72. package/dist/commands/boards/animation.js +0 -171
  73. package/dist/commands/boards/appearance.d.ts +0 -2
  74. package/dist/commands/boards/appearance.js +0 -127
  75. package/dist/commands/boards/batch.d.ts +0 -4
  76. package/dist/commands/boards/batch.js +0 -62
  77. package/dist/commands/boards/connections.d.ts +0 -2
  78. package/dist/commands/boards/connections.js +0 -95
  79. package/dist/commands/boards/context.d.ts +0 -31
  80. package/dist/commands/boards/context.js +0 -71
  81. package/dist/commands/boards/items.d.ts +0 -2
  82. package/dist/commands/boards/items.js +0 -184
  83. package/dist/commands/boards/transactions.d.ts +0 -12
  84. package/dist/commands/boards/transactions.js +0 -69
@@ -1,14 +1,15 @@
1
- import { randomUUID } from "node:crypto";
2
1
  import { readdir, stat } from "node:fs/promises";
3
2
  import { basename, dirname, relative, resolve, sep } from "node:path";
4
- import { resolveCohubEnvironment } from "@neta-art/cohub";
3
+ import { readSentTurns, readSessionTurnOrigin, resolveCohubEnvironment } from "@neta-art/cohub";
5
4
  import { uploadAvatarAsset, uploadChatImageAsset } from "../avatar.js";
6
5
  import { createClient } from "../client.js";
7
- import { putLocalFile } from "../http-put.js";
8
6
  import { table, json as outJson, jsonRequested, ok, error, handleHttp, formatEpochMs, truncateText } from "../output.js";
9
7
  import { resolveSpace } from "../space.js";
8
+ import { uploadLocalFiles } from "../space-files/upload.js";
10
9
  import { registerSpaceCommerce } from "./space-commerce.js";
10
+ import { registerSpaceFileTransfer } from "./space-files.js";
11
11
  import { registerSpaceActivity } from "./space-activity.js";
12
+ import { registerSpaceDisplays } from "./space-displays.js";
12
13
  import { registerSpaceInvitations } from "./space-invitations.js";
13
14
  import { registerSpaceTurns } from "./space-turns.js";
14
15
  import { registerSpaceWebhooks } from "./space-webhooks.js";
@@ -121,16 +122,8 @@ const walkUploadPath = async (localPath, root) => {
121
122
  }
122
123
  if (!info.isFile())
123
124
  return [];
124
- return [{
125
- id: randomUploadEntryId(),
126
- localPath,
127
- relativePath,
128
- name,
129
- size: info.size,
130
- mimeType: null,
131
- }];
125
+ return [{ localPath, relativePath, size: info.size }];
132
126
  };
133
- const randomUploadEntryId = () => randomUUID();
134
127
  // Upload semantics: a file keeps its name, and a directory contributes its
135
128
  // contents directly under the target dir — like `aws s3 cp dir remote:path`
136
129
  // or `rclone copy`.
@@ -158,45 +151,14 @@ export async function collectUploadFiles(paths) {
158
151
  }
159
152
  return files;
160
153
  }
161
- async function putUploadEntry(entry, uploadUrl, headers) {
162
- await putLocalFile({
163
- url: uploadUrl,
164
- filePath: entry.localPath,
165
- size: entry.size,
166
- headers,
167
- label: entry.relativePath,
168
- });
169
- }
170
154
  async function uploadFiles(command, paths, opts) {
171
155
  const spaceId = await resolveSpace(command);
172
156
  const client = createClient();
173
157
  try {
174
158
  const files = await collectUploadFiles(paths);
175
- const plan = await client.space(spaceId).files.createUpload({
176
- destination: { kind: "workspace", targetDir: opts.dir },
177
- entries: files.map((file) => ({
178
- id: file.id,
179
- name: file.name,
180
- relativePath: file.relativePath,
181
- size: file.size,
182
- mimeType: file.mimeType,
183
- })),
184
- });
185
- const byId = new Map(files.map((file) => [file.id, file]));
186
- for (const entry of plan.entries) {
187
- const file = byId.get(entry.id);
188
- if (!file)
189
- throw new Error(`Missing upload entry: ${entry.id}`);
190
- // Remote downloadUrl entries have no uploadUrl; complete pulls them server-side.
191
- if (!entry.uploadUrl)
192
- continue;
193
- await putUploadEntry(file, entry.uploadUrl, entry.headers);
194
- }
195
- const result = await client.space(spaceId).files.completeUpload(plan.uploadId, {
196
- entries: plan.entries.map((entry) => ({ id: entry.id })),
197
- });
159
+ const { uploaded, uploadIds } = await uploadLocalFiles(client.space(spaceId).files, files, { targetDir: opts.dir });
198
160
  if (jsonRequested(opts))
199
- return outJson({ ...result, uploadId: plan.uploadId, files: files.length });
161
+ return outJson({ ok: true, uploaded, uploadId: uploadIds[0], uploadIds, files: files.length });
200
162
  ok(`Uploaded ${files.length} file${files.length === 1 ? "" : "s"}`);
201
163
  }
202
164
  catch (e) {
@@ -242,7 +204,7 @@ async function sendPrompt(command, words, opts) {
242
204
  && !new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]).has(thinkingLevel)) {
243
205
  return error("Invalid thinking level", "Use off|minimal|low|medium|high|xhigh|max");
244
206
  }
245
- const spaceId = await resolveSpace(command);
207
+ const spaceId = await resolveSpace(command, { home: true });
246
208
  const client = createClient();
247
209
  try {
248
210
  const schedule = opts.delayMs
@@ -305,7 +267,7 @@ async function sendPrompt(command, words, opts) {
305
267
  }
306
268
  }
307
269
  async function runCompletionCommand(command, words, opts) {
308
- const spaceId = await resolveSpace(command);
270
+ const spaceId = await resolveSpace(command, { home: true });
309
271
  const content = words.join(" ").trim();
310
272
  if (!content && process.stdin.isTTY) {
311
273
  return error("Message required", "Pass content args or pipe via stdin");
@@ -447,9 +409,66 @@ export function registerSpaceCreate(spacesCmd, dependencies = {}) {
447
409
  }
448
410
  });
449
411
  }
412
+ function registerUserLabels(program) {
413
+ const labels = program.command("labels").description("Manage personal labels");
414
+ labels.command("ls").description("List personal labels").option("--json", "Output as JSON").action(async (opts) => {
415
+ const client = createClient();
416
+ try {
417
+ const result = await client.user.labels.list();
418
+ const flatten = (items, parent = "") => items.flatMap((item) => [
419
+ { id: item.id, ref: parent ? `${parent}/${item.name}` : item.name, source: item.source, rank: item.rank },
420
+ ...flatten(item.children ?? [], parent ? `${parent}/${item.name}` : item.name),
421
+ ]);
422
+ const rows = flatten(result.labels);
423
+ if (jsonRequested(opts))
424
+ return outJson(rows);
425
+ table(rows, [{ key: "ref", label: "Label" }, { key: "source", label: "Source" }, { key: "rank", label: "Rank" }]);
426
+ }
427
+ catch (e) {
428
+ handleHttp(e);
429
+ }
430
+ });
431
+ labels.command("create <ref>").description("Create a personal label").action(async (ref) => {
432
+ try {
433
+ await createClient().user.labels.create(ref);
434
+ ok(`Created label: ${ref}`);
435
+ }
436
+ catch (e) {
437
+ handleHttp(e);
438
+ }
439
+ });
440
+ labels.command("rename <ref> <name>").description("Rename a personal label").action(async (ref, name) => {
441
+ try {
442
+ await createClient().user.labels.update(ref, { name });
443
+ ok(`Renamed label: ${ref}`);
444
+ }
445
+ catch (e) {
446
+ handleHttp(e);
447
+ }
448
+ });
449
+ labels.command("rm <ref>").description("Delete a personal label").action(async (ref) => {
450
+ try {
451
+ await createClient().user.labels.delete(ref);
452
+ ok(`Deleted label: ${ref}`);
453
+ }
454
+ catch (e) {
455
+ handleHttp(e);
456
+ }
457
+ });
458
+ labels.command("reorder <refs...>").description("Set personal label order").action(async (refs) => {
459
+ try {
460
+ await createClient().user.labels.reorder(refs);
461
+ ok(`Reordered ${refs.length} label(s)`);
462
+ }
463
+ catch (e) {
464
+ handleHttp(e);
465
+ }
466
+ });
467
+ }
450
468
  export function registerSpaces(program) {
451
469
  const spacesCmd = program.command("spaces").description("Space management");
452
470
  registerSpaceInvitations(spacesCmd);
471
+ registerUserLabels(program);
453
472
  // ── spaces ls ──
454
473
  spacesCmd
455
474
  .command("ls")
@@ -457,20 +476,27 @@ export function registerSpaces(program) {
457
476
  .description("List all spaces")
458
477
  .option("--mine", "Only spaces you own")
459
478
  .option("--pinned", "Only pinned spaces")
479
+ .option("--archived", "Only archived spaces")
460
480
  .option("--json", "Output as JSON")
461
481
  .action(async (opts) => {
462
482
  const client = createClient();
463
483
  try {
464
- const [items, me] = await Promise.all([
465
- client.spaces.list(),
466
- opts.mine ? client.user.getMe() : Promise.resolve(null),
467
- ]);
468
- const myUuid = me?.uuid ?? null;
484
+ const items = [];
485
+ let cursor = null;
486
+ do {
487
+ const page = await client.spaces.list({
488
+ limit: 100,
489
+ cursor,
490
+ filter: opts.archived ? "archived" : opts.pinned ? "pinned" : opts.mine ? "mine" : "all",
491
+ });
492
+ items.push(...page.items);
493
+ cursor = page.pageInfo.nextCursor;
494
+ } while (cursor);
469
495
  const filtered = items.filter((item) => {
470
- if (opts.mine && myUuid && item.userUuid !== myUuid)
471
- return false;
472
496
  if (opts.pinned && !item.isPinned)
473
497
  return false;
498
+ if (opts.archived && !item.isArchived)
499
+ return false;
474
500
  return true;
475
501
  });
476
502
  if (jsonRequested(opts))
@@ -487,6 +513,28 @@ export function registerSpaces(program) {
487
513
  handleHttp(e);
488
514
  }
489
515
  });
516
+ for (const archive of [true, false]) {
517
+ spacesCmd
518
+ .command(archive ? "archive <ids...>" : "unarchive <ids...>")
519
+ .description(archive ? "Archive spaces in your personal view" : "Restore archived spaces")
520
+ .option("--json", "Output as JSON")
521
+ .action(async (ids, opts) => {
522
+ const client = createClient();
523
+ try {
524
+ const chunks = Array.from({ length: Math.ceil(ids.length / 200) }, (_, index) => ids.slice(index * 200, (index + 1) * 200));
525
+ for (const chunk of chunks)
526
+ await client.user.labels.patchResources(chunk, archive
527
+ ? { addLabelRefs: ["Archived"], removeLabelRefs: ["Pinned"] }
528
+ : { removeLabelRefs: ["Archived"] });
529
+ if (jsonRequested(opts))
530
+ return outJson({ ids, archived: archive });
531
+ ok(`${archive ? "Archived" : "Unarchived"} ${ids.length} space(s)`);
532
+ }
533
+ catch (e) {
534
+ handleHttp(e);
535
+ }
536
+ });
537
+ }
490
538
  // ── spaces get ──
491
539
  spacesCmd
492
540
  .command("get [id]")
@@ -652,6 +700,7 @@ export function registerSpaces(program) {
652
700
  .action((words, opts) => runCompletionCommand(spacesCmd, words, opts));
653
701
  // ── spaces files ──
654
702
  registerFiles(spacesCmd);
703
+ registerSpaceDisplays(spacesCmd);
655
704
  // ── spaces sessions ──
656
705
  registerSessions(spacesCmd);
657
706
  // ── spaces turns ──
@@ -712,9 +761,9 @@ export function registerSpaces(program) {
712
761
  if (jsonRequested(opts))
713
762
  return outJson(usage);
714
763
  console.log("\n Summary:");
715
- table([usage.summary], [
764
+ table([usage.totals], [
716
765
  { key: "totalTokens", label: "Tokens" },
717
- { key: "costTotal", label: "Cost ($)" },
766
+ ...(usage.totals.costTotal ? [{ key: "costTotal", label: "Cost ($)" }] : []),
718
767
  { key: "requestCount", label: "Requests" },
719
768
  { key: "successCount", label: "Success" },
720
769
  { key: "errorCount", label: "Errors" },
@@ -1145,25 +1194,7 @@ function registerFiles(spacesCmd) {
1145
1194
  handleHttp(e);
1146
1195
  }
1147
1196
  });
1148
- filesCmd
1149
- .command("cat <path>")
1150
- .description("Read file content")
1151
- .action(async (path) => {
1152
- const spaceId = await resolveSpace(spacesCmd);
1153
- const client = createClient();
1154
- try {
1155
- const file = await client.space(spaceId).files.read(path);
1156
- if (!("content" in file))
1157
- return error("File is being prepared. Please retry shortly.");
1158
- if (file.delivery === "url" && file.url) {
1159
- console.log(`[CDN] ${file.url}`);
1160
- }
1161
- console.log(file.content);
1162
- }
1163
- catch (e) {
1164
- handleHttp(e);
1165
- }
1166
- });
1197
+ registerSpaceFileTransfer(filesCmd, spacesCmd);
1167
1198
  filesCmd
1168
1199
  .command("write <path>")
1169
1200
  .description("Write file content")
@@ -1354,6 +1385,37 @@ function registerSessions(spacesCmd) {
1354
1385
  handleHttp(e);
1355
1386
  }
1356
1387
  });
1388
+ sessionsCmd
1389
+ .command("stats <id>")
1390
+ .description("Session statistics for settled turns (including separate inherited history)")
1391
+ .option("--json", "Output as JSON")
1392
+ .action(async (id, opts) => {
1393
+ const spaceId = await resolveSpace(spacesCmd);
1394
+ try {
1395
+ const { stats } = await createClient().space(spaceId).session(id).stats();
1396
+ if (jsonRequested(opts))
1397
+ return outJson({ stats });
1398
+ const own = stats.own;
1399
+ const rows = [
1400
+ { metric: "Turns", value: own.turns },
1401
+ { metric: "Model calls", value: own.calls },
1402
+ { metric: "Tool calls", value: own.toolCalls },
1403
+ { metric: "Compactions", value: own.compactions },
1404
+ { metric: "Tokens", value: own.usage?.totalTokens },
1405
+ { metric: "Execution time (ms)", value: own.elapsedMs },
1406
+ { metric: "Model time (ms)", value: own.modelMs == null ? null : Math.round(own.modelMs) },
1407
+ { metric: "Model cost (USD)", value: own.modelCostUsd },
1408
+ { metric: "Content generation cost (USD)", value: own.generationCostUsd },
1409
+ { metric: "Inherited turns", value: stats.inherited.turns },
1410
+ ].filter((row) => row.value != null);
1411
+ table(rows, [{ key: "metric", label: "Metric" }, { key: "value", label: "Value" }]);
1412
+ if (own.partial)
1413
+ console.log("Some execution details were not recorded.");
1414
+ }
1415
+ catch (e) {
1416
+ handleHttp(e);
1417
+ }
1418
+ });
1357
1419
  sessionsCmd
1358
1420
  .command("files <id>")
1359
1421
  .description("List Space files changed by a session")
@@ -1514,6 +1576,15 @@ function registerTurns(sessionsCmd) {
1514
1576
  { key: "stopReason", label: "Stop" },
1515
1577
  { key: "errorMessage", label: "Error" },
1516
1578
  ]);
1579
+ const origin = readSessionTurnOrigin(result.turn.meta, spaceId);
1580
+ if (origin)
1581
+ console.log(`\nOrigin (${origin.kind}): space=${origin.spaceId} session=${origin.sessionId} turn=${origin.turnId}${origin.toolCallId ? ` toolCall=${origin.toolCallId}` : ""}`);
1582
+ const sent = readSentTurns(result.turn.meta);
1583
+ if (sent.length > 0) {
1584
+ console.log(`\nSent (${sent.length}):`);
1585
+ for (const ref of sent)
1586
+ console.log(` ${ref.spaceId ? `space=${ref.spaceId} ` : ""}session=${ref.sessionId} turn=${ref.turnId}${ref.toolCallId ? ` toolCall=${ref.toolCallId}` : ""}`);
1587
+ }
1517
1588
  if (result.turn.userText)
1518
1589
  console.log(`\nUser:\n${result.turn.userText}`);
1519
1590
  if (result.turn.assistantText)
package/dist/index.js CHANGED
@@ -37,7 +37,7 @@ program
37
37
  .summary("Work with Cohub from your terminal")
38
38
  .description("Send prompts, manage Space files, and publish public output.")
39
39
  .version(VERSION, "-v, --version", "Show version")
40
- .option("-s, --space <id>", "Target Space ID")
40
+ .option("-s, --space <space>", "Target Space: ID, slug, or username/slug")
41
41
  .option("--json", "Print machine-readable JSON when supported")
42
42
  .helpOption("-h, --help", "Show help")
43
43
  .addHelpText("after", `
@@ -59,7 +59,7 @@ Common commands:
59
59
  cohub -s <space-id> boards inspect <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
- cohub -s <space-id> spaces files ls
62
+ cohub -s home spaces files ls
63
63
  cohub -s <space-id> public upload ./dist demo
64
64
  cohub -s <space-id> apps publish demo --file dist/index.html
65
65
  cohub desktop open <app-id> --call selection.get
@@ -69,10 +69,13 @@ Common commands:
69
69
  cohub generate "A calm lake at sunrise" --model <model> --output lake.png
70
70
 
71
71
  Target space:
72
- -s <space-id>, then COHUB_SPACE_ID, then the current directory Runtime binding, then Home
72
+ -s <space>, then COHUB_SPACE_ID, then the current directory Runtime binding.
73
+ <space> is an ID, a slug you own such as home, or username/slug.
74
+ prompt, completion, generate, apps, and public fall back to your Home Space;
75
+ other Space commands need a target.
73
76
 
74
77
  Environment:
75
- COHUB_SPACE_ID Target Space ID when -s is omitted
78
+ COHUB_SPACE_ID Target Space when -s is omitted
76
79
  COHUB_EXECUTION_TOKEN Use this token instead of the stored Logto session
77
80
  ENV=dev Use the development Cohub environment
78
81
  HTTPS_PROXY Honored for API and uploads (also HTTP_PROXY, NO_PROXY)
@@ -8,6 +8,7 @@ export type RuntimeUpOptions = {
8
8
  harness: string[];
9
9
  pi?: string;
10
10
  codex?: string;
11
+ display?: string | boolean;
11
12
  yes?: boolean;
12
13
  json?: boolean;
13
14
  detach?: boolean;
@@ -15,6 +16,7 @@ export type RuntimeUpOptions = {
15
16
  };
16
17
  export declare const resolveLocalSpaceName: (root: string, name?: string) => string;
17
18
  export declare function parseRuntimeHarnesses(values: string[]): ("pi" | "codex")[];
19
+ export declare function parseRuntimeDisplay(value: string | boolean | undefined): string | undefined;
18
20
  export declare function resolveRuntimeTarget(program: Command, target?: string): Promise<string>;
19
21
  export declare function startBackgroundRuntime(config: RuntimeLaunch): Promise<RuntimeSummary>;
20
22
  export declare function runtimeUp(program: Command, dir: string | undefined, options: RuntimeUpOptions): Promise<void>;
@@ -7,7 +7,7 @@ import { isLocalHarness } from "@neta-art/cohub";
7
7
  import { discoverHarnesses, installedHarnesses } from "./harness.js";
8
8
  import { requireAccessToken } from "../auth.js";
9
9
  import { createClient } from "../client.js";
10
- import { currentIdentityKey, explicitSpace } from "../space.js";
10
+ import { currentIdentityKey, explicitSpace, resolveSpaceRef } from "../space.js";
11
11
  import { canonicalRuntimeRoot, getRuntimeSpaceBinding, resolveRuntimeSpace } from "./space-binding.js";
12
12
  import { controlRuntimeInstance, requestRuntimeInstance, runtimeInstanceDirectory } from "./instance.js";
13
13
  import { ensureNativeSync, repairIntegrations } from "./native/attach.js";
@@ -20,10 +20,30 @@ 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)
25
+ return undefined;
26
+ const spec = value === true ? "auto" : value.trim();
27
+ if (!/^(auto|macos|x11(:[\w.:-]+)?|xvfb(:\d{3,4}x\d{3,4})?)$/.test(spec)) {
28
+ throw new Error("Display must be auto, macos, x11[:display] or xvfb[:WIDTHxHEIGHT]");
29
+ }
30
+ if (process.platform === "linux" && spec === "auto" && !process.env.DISPLAY) {
31
+ throw new Error(process.env.WAYLAND_DISPLAY
32
+ ? "Wayland sessions cannot be shared yet: log in with an X11 session, or use --display xvfb for a virtual screen"
33
+ : "No X11 session to share (DISPLAY is unset): use --display xvfb for a virtual screen");
34
+ }
35
+ return spec;
36
+ }
37
+ const isVirtualDisplay = (spec) => spec.startsWith("xvfb");
38
+ async function requestedRuntimeSpace(program, target) {
39
+ const ref = target?.trim() || explicitSpace(program);
40
+ return ref ? resolveSpaceRef(ref) : null;
41
+ }
23
42
  export async function resolveRuntimeTarget(program, target) {
24
- const spaceId = target?.trim() || explicitSpace(program) || (await getRuntimeSpaceBinding(process.cwd(), currentIdentityKey()))?.spaceId;
43
+ const spaceId = await requestedRuntimeSpace(program, target)
44
+ ?? (await getRuntimeSpaceBinding(process.cwd(), currentIdentityKey()))?.spaceId;
25
45
  if (!spaceId)
26
- throw new Error("No directory binding. Use --space <id> or runtime up");
46
+ throw new Error("No directory binding. Use --space <space> or runtime up");
27
47
  return spaceId;
28
48
  }
29
49
  export async function startBackgroundRuntime(config) {
@@ -72,11 +92,12 @@ export async function runtimeUp(program, dir, options) {
72
92
  if (!(await stat(requestedRoot)).isDirectory())
73
93
  throw new Error("Workspace is not a directory");
74
94
  const root = await canonicalRuntimeRoot(requestedRoot);
75
- const requested = options.space?.trim() || explicitSpace(program);
76
- if (options.new && requested)
95
+ const requestedRef = options.space?.trim() || explicitSpace(program);
96
+ if (options.new && requestedRef)
77
97
  throw new Error("--new cannot be combined with --space or COHUB_SPACE_ID");
78
- if (options.name && requested)
98
+ if (options.name && requestedRef)
79
99
  throw new Error("--name only applies to a new Space");
100
+ const requested = await requestedRuntimeSpace(program, options.space);
80
101
  const identity = currentIdentityKey();
81
102
  if (!identity) {
82
103
  await requireAccessToken();
@@ -85,10 +106,11 @@ export async function runtimeUp(program, dir, options) {
85
106
  const binding = await getRuntimeSpaceBinding(root, identity);
86
107
  const existingId = requested || binding?.spaceId;
87
108
  let harnesses = parseRuntimeHarnesses(options.harness);
109
+ let display = parseRuntimeDisplay(options.display);
88
110
  if (existingId && !options.new) {
89
111
  const existing = await requestRuntimeInstance(runtimeInstanceDirectory(identity, existingId));
90
112
  if (existing) {
91
- if (existing.root !== root || options.harness.length && [...existing.harnesses].sort().join() !== [...harnesses].sort().join() || options.pi || options.codex) {
113
+ if (existing.root !== root || options.harness.length && [...existing.harnesses].sort().join() !== [...harnesses].sort().join() || options.pi || options.codex || display && display !== existing.display) {
92
114
  throw new Error("Runtime is running with a different configuration. Use down first");
93
115
  }
94
116
  // `up` is the single idempotent entry: a reused instance adopts its running
@@ -134,6 +156,11 @@ export async function runtimeUp(program, dir, options) {
134
156
  const answer = await rl.question("Collaborators can execute as your OS user, beyond this folder. Allow? [Y/n] ");
135
157
  if (/^n(o)?$/i.test(answer.trim()))
136
158
  return;
159
+ 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()))
162
+ display = undefined;
163
+ }
137
164
  }
138
165
  finally {
139
166
  rl.close();
@@ -160,7 +187,7 @@ export async function runtimeUp(program, dir, options) {
160
187
  });
161
188
  // Consent (or --yes) precedes any user-level integration install; failure never blocks startup.
162
189
  const native = await ensureNativeSync({ root, spaceId, identity, harnesses, yes: options.yes, executables: { pi: options.pi, codex: options.codex } });
163
- const config = { spaceId, root, identity, harnesses, capabilities, executables: { pi: options.pi, codex: options.codex }, background: Boolean(options.detach), verbose: options.verbose, importHistory: native.importHistory };
190
+ const config = { spaceId, root, identity, harnesses, capabilities, executables: { pi: options.pi, codex: options.codex }, background: Boolean(options.detach), verbose: options.verbose, importHistory: native.importHistory, display };
164
191
  const existing = await requestRuntimeInstance(runtimeInstanceDirectory(identity, spaceId));
165
192
  if (existing) {
166
193
  if (existing.root !== root)
@@ -18,6 +18,7 @@ export type RuntimeSummary = {
18
18
  workspaceConnected: boolean;
19
19
  diagnosticsPath: string;
20
20
  background: boolean;
21
+ display?: string;
21
22
  native?: NativeStatus;
22
23
  };
23
24
  export declare function printRuntimeSummary(summary: RuntimeSummary, json?: boolean, reused?: boolean): void;
@@ -1,7 +1,7 @@
1
- import { resolveCohubEnvironment } from "@neta-art/cohub";
1
+ import { webUrl } from "../web.js";
2
2
  export const diagnosticLevels = ["debug", "info", "warn", "error"];
3
3
  export const atLeastLevel = (level, minimum) => diagnosticLevels.indexOf(level) >= diagnosticLevels.indexOf(minimum);
4
- export const runtimeWebUrl = (spaceId) => `https://${resolveCohubEnvironment() === "prod" ? "" : "dev."}cohub.live/spaces/${spaceId}`;
4
+ export const runtimeWebUrl = (spaceId) => webUrl(`/spaces/${spaceId}`);
5
5
  const messages = {
6
6
  "runtime.ready": "Harness connected",
7
7
  "runtime.available": "Runtime ready",
@@ -69,6 +69,7 @@ export function printRuntimeSummary(summary, json = false, reused = false) {
69
69
  ["Directory", summary.root],
70
70
  ["Harness", summary.harnesses.join(" · ")],
71
71
  ["Mode", summary.background ? "Background" : "Foreground"],
72
+ ...(summary.display ? [["Screen", summary.display === "auto" ? "This computer" : summary.display]] : []),
72
73
  ["PID", String(summary.pid)],
73
74
  ["Logs", summary.diagnosticsPath],
74
75
  ];
@@ -13,6 +13,7 @@ export type RuntimeLaunch = {
13
13
  verbose?: boolean;
14
14
  /** Start importing earlier conversations as soon as the Runtime is connected. */
15
15
  importHistory?: boolean;
16
+ display?: string;
16
17
  };
17
18
  export declare function sandboxOutputLevel(value: unknown, stream: "stdout" | "stderr"): RuntimeDiagnosticLevel;
18
19
  export declare function runRuntime(config: RuntimeLaunch, onState: (status: RuntimeSummary) => void, externalSignal?: AbortSignal, onDiagnostic?: (event: RuntimeDiagnostic) => void): Promise<void>;
@@ -48,6 +48,7 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
48
48
  let status = {
49
49
  spaceId: config.spaceId, root: config.root, runtimeId: diagnostics.runtimeId,
50
50
  pid: process.pid, harnesses: config.harnesses, background: config.background,
51
+ ...(config.display ? { display: config.display } : {}),
51
52
  state: "starting", harnessConnected: false, workspaceConnected: false,
52
53
  diagnosticsPath: diagnostics.directory,
53
54
  };
@@ -123,7 +124,7 @@ export async function runRuntime(config, onState, externalSignal, onDiagnostic)
123
124
  signal.throwIfAborted();
124
125
  const bridge = spawn(binary, ["--local", "--space", config.spaceId, "--root", config.root, "--relay", process.env.COHUB_RELAY_URL?.trim() || relay.toString()], {
125
126
  stdio: ["pipe", "pipe", "pipe", "pipe"],
126
- env: { ...process.env, COHUB_RELAY_TOKEN: initialToken, COHUB_RUNTIME_ID: status.runtimeId, COHUB_LOG_FORMAT: "json", COHUB_RUNTIME_MANAGED: "1" },
127
+ env: { ...process.env, COHUB_RELAY_TOKEN: initialToken, COHUB_RUNTIME_ID: status.runtimeId, COHUB_LOG_FORMAT: "json", COHUB_RUNTIME_MANAGED: "1", COHUB_DISPLAY: config.display ?? "" },
127
128
  });
128
129
  bridge.stdin?.on("error", () => undefined);
129
130
  const closed = new Promise((resolve) => {
@@ -1,5 +1,6 @@
1
1
  export declare const SELF_UPDATE_WORKER_ENV = "COHUB_CLI_SELF_UPDATE_WORKER";
2
2
  export type CliSelfUpdateResult = "current" | "updated" | "updated-by-peer";
3
+ export declare function isNewerVersion(candidate: string, installed: string | undefined): boolean;
3
4
  export declare function resolveSelfUpdateResult(beforeVersion: string | undefined, afterVersion: string | undefined, updatedByPeer: boolean): CliSelfUpdateResult;
4
5
  export declare const isCliSelfUpdateDue: () => boolean;
5
6
  export declare function startCliSelfUpdate(entrypoint: string): void;