@neta-art/cohub-cli 1.7.0 → 1.8.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.
@@ -2,21 +2,36 @@ import { randomUUID } from "node:crypto";
2
2
  import { createReadStream } from "node:fs";
3
3
  import { readdir, stat } from "node:fs/promises";
4
4
  import { basename, dirname, relative, resolve, sep } from "node:path";
5
- import { createClient } from "../client.js";
5
+ import { resolveCohubEnvironment } from "@neta-art/cohub";
6
6
  import { uploadAvatarAsset } from "../avatar.js";
7
- import { table, json as outJson, ok, error, handleHttp } from "../output.js";
8
- function requireSpace(program) {
9
- let current = program;
10
- while (current) {
11
- const opts = current.opts();
12
- if (opts.space)
13
- return String(opts.space);
14
- current = current.parent ?? null;
15
- }
16
- return error("Missing required option", "Add -s, --space <id> to target a space");
17
- }
18
- const cliEnv = (process.env.ENV === "prod" ? "prod" : "dev");
7
+ import { createClient } from "../client.js";
8
+ import { table, json as outJson, jsonRequested, ok, error, handleHttp } from "../output.js";
9
+ import { resolveSpace } from "../space.js";
10
+ const cliEnv = resolveCohubEnvironment();
19
11
  const defaultIdleTtlSeconds = cliEnv === "prod" ? 12 * 60 * 60 : 10 * 60;
12
+ const SPACE_ROLES = ["host", "builder", "guest"];
13
+ function parseInteger(value, name, options = {}) {
14
+ if (!/^-?\d+$/.test(value.trim()))
15
+ return error(`Invalid ${name}`, `${name} must be an integer`);
16
+ const parsed = Number.parseInt(value, 10);
17
+ if (!Number.isSafeInteger(parsed))
18
+ return error(`Invalid ${name}`, `${name} must be a safe integer`);
19
+ if (options.min !== undefined && parsed < options.min)
20
+ return error(`Invalid ${name}`, `${name} must be at least ${options.min}`);
21
+ if (options.max !== undefined && parsed > options.max)
22
+ return error(`Invalid ${name}`, `${name} must be at most ${options.max}`);
23
+ return parsed;
24
+ }
25
+ function parseChoice(value, name, choices) {
26
+ if (choices.includes(value))
27
+ return value;
28
+ return error(`Invalid ${name}`, `Use one of: ${choices.join(", ")}`);
29
+ }
30
+ function parseNullableRole(value, name) {
31
+ if (value === undefined || value === "null")
32
+ return null;
33
+ return parseChoice(value, name, SPACE_ROLES);
34
+ }
20
35
  const parseAutoDestroy = (opts) => {
21
36
  const mode = opts.autoDestroy ?? (opts.idleTtl ? "idle" : undefined);
22
37
  if (!mode)
@@ -25,10 +40,7 @@ const parseAutoDestroy = (opts) => {
25
40
  return { mode: "never" };
26
41
  if (mode !== "idle")
27
42
  return error("Invalid auto destroy mode", "Use --auto-destroy idle or --auto-destroy never");
28
- const ttlSeconds = Number.parseInt(opts.idleTtl ?? String(defaultIdleTtlSeconds), 10);
29
- if (!Number.isSafeInteger(ttlSeconds) || ttlSeconds < 60 || ttlSeconds > 30 * 24 * 60 * 60) {
30
- return error("Invalid idle TTL", "--idle-ttl must be an integer between 60 and 2592000 seconds");
31
- }
43
+ const ttlSeconds = parseInteger(opts.idleTtl ?? String(defaultIdleTtlSeconds), "idle TTL", { min: 60, max: 30 * 24 * 60 * 60 });
32
44
  return { mode: "idle", ttlSeconds };
33
45
  };
34
46
  const formatAutoDestroy = (policy) => {
@@ -93,7 +105,7 @@ async function putUploadEntry(entry, uploadUrl, headers) {
93
105
  }
94
106
  }
95
107
  async function uploadFiles(command, paths, opts) {
96
- const spaceId = requireSpace(command);
108
+ const spaceId = resolveSpace(command);
97
109
  const client = createClient();
98
110
  try {
99
111
  const files = await collectUploadFiles(paths);
@@ -117,7 +129,7 @@ async function uploadFiles(command, paths, opts) {
117
129
  const result = await client.space(spaceId).files.completeUpload(plan.uploadId, {
118
130
  entries: plan.entries.map((entry) => ({ id: entry.id })),
119
131
  });
120
- if (opts.json)
132
+ if (jsonRequested(opts))
121
133
  return outJson({ ...result, uploadId: plan.uploadId, files: files.length });
122
134
  ok(`Uploaded ${files.length} file${files.length === 1 ? "" : "s"}`);
123
135
  }
@@ -125,6 +137,21 @@ async function uploadFiles(command, paths, opts) {
125
137
  handleHttp(e);
126
138
  }
127
139
  }
140
+ async function confirmRestart(opts) {
141
+ if (opts.yes)
142
+ return;
143
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
144
+ return error("Confirmation required", "Pass --yes to restart the sandbox automatically.");
145
+ process.stdout.write("Changing mods restarts the sandbox and may interrupt running work. Continue? [y/N] ");
146
+ const chunks = [];
147
+ for await (const chunk of process.stdin) {
148
+ chunks.push(chunk);
149
+ break;
150
+ }
151
+ const answer = Buffer.concat(chunks).toString().trim().toLowerCase();
152
+ if (answer !== "y" && answer !== "yes")
153
+ return error("Cancelled");
154
+ }
128
155
  async function readPromptContent(words) {
129
156
  let content = words.join(" ");
130
157
  if (!content && !process.stdin.isTTY) {
@@ -144,11 +171,11 @@ async function sendPrompt(command, words, opts) {
144
171
  return error("Conflicting schedule", "Use only one of --delay-ms, --at, or --cron");
145
172
  if (opts.cron && !opts.timezone)
146
173
  return error("Missing timezone", "--timezone is required with --cron");
147
- const spaceId = requireSpace(command);
174
+ const spaceId = resolveSpace(command);
148
175
  const client = createClient();
149
176
  try {
150
177
  const schedule = opts.delayMs
151
- ? { mode: "delay", delayMs: Number.parseInt(opts.delayMs, 10) }
178
+ ? { mode: "delay", delayMs: parseInteger(opts.delayMs, "delay", { min: 1 }) }
152
179
  : opts.at
153
180
  ? { mode: "at", sendAt: opts.at }
154
181
  : opts.cron
@@ -162,7 +189,7 @@ async function sendPrompt(command, words, opts) {
162
189
  provider: opts.provider,
163
190
  schedule,
164
191
  });
165
- if (opts.json)
192
+ if (jsonRequested(opts))
166
193
  return outJson(result);
167
194
  if (result.mode === "immediate")
168
195
  return ok(`Prompt sent — sessionId: ${result.sessionId}, turnId: ${result.turnId}`);
@@ -201,7 +228,7 @@ export function registerSpaces(program) {
201
228
  const client = createClient();
202
229
  try {
203
230
  const items = await client.spaces.list();
204
- if (opts.json)
231
+ if (jsonRequested(opts))
205
232
  return outJson(items);
206
233
  table(items, [
207
234
  { key: "id", label: "ID" },
@@ -222,7 +249,7 @@ export function registerSpaces(program) {
222
249
  const client = createClient();
223
250
  try {
224
251
  const space = await client.spaces.get(id);
225
- if (opts.json)
252
+ if (jsonRequested(opts))
226
253
  return outJson(space);
227
254
  table([space], [
228
255
  { key: "id", label: "ID" },
@@ -254,7 +281,7 @@ export function registerSpaces(program) {
254
281
  description: opts.description,
255
282
  ...(autoDestroy ? { config: { sandbox: { autoDestroy } } } : {}),
256
283
  });
257
- if (opts.json)
284
+ if (jsonRequested(opts))
258
285
  return outJson(result);
259
286
  ok(`Space created: ${result.space.id}`);
260
287
  table([result.space], [
@@ -287,12 +314,12 @@ export function registerSpaces(program) {
287
314
  .description("Upload the space avatar")
288
315
  .option("--json", "Output as JSON")
289
316
  .action(async (path, opts) => {
290
- const spaceId = requireSpace(spacesCmd);
317
+ const spaceId = resolveSpace(spacesCmd);
291
318
  const client = createClient();
292
319
  try {
293
320
  const asset = await uploadAvatarAsset({ client, purpose: "space_avatar", spaceId, path });
294
321
  const result = await client.space(spaceId).profile({ avatarUrl: asset.publicUrl });
295
- if (opts.json)
322
+ if (jsonRequested(opts))
296
323
  return outJson({ ...result, asset });
297
324
  ok("Space avatar updated");
298
325
  }
@@ -313,13 +340,13 @@ export function registerSpaces(program) {
313
340
  const autoDestroy = parseAutoDestroy(opts);
314
341
  if (autoDestroy) {
315
342
  const result = await client.space(id).updateConfig({ sandbox: { autoDestroy } });
316
- if (opts.json)
343
+ if (jsonRequested(opts))
317
344
  return outJson(result);
318
345
  ok(`Space config updated — sandbox auto destroy: ${formatAutoDestroy(autoDestroy)}`);
319
346
  return;
320
347
  }
321
348
  const result = await client.space(id).getConfig();
322
- if (opts.json)
349
+ if (jsonRequested(opts))
323
350
  return outJson(result);
324
351
  table([{ key: "sandbox.autoDestroy", value: formatAutoDestroy(result.config.sandbox.autoDestroy) }], [
325
352
  { key: "key", label: "Key" },
@@ -363,11 +390,11 @@ export function registerSpaces(program) {
363
390
  .description("Space usage statistics (default: 30 days)")
364
391
  .option("--json", "Output as JSON")
365
392
  .action(async (days, opts) => {
366
- const spaceId = requireSpace(spacesCmd);
393
+ const spaceId = resolveSpace(spacesCmd);
367
394
  const client = createClient();
368
395
  try {
369
- const usage = await client.space(spaceId).usage.get(Number.parseInt(days ?? "30", 10));
370
- if (opts.json)
396
+ const usage = await client.space(spaceId).usage.get(parseInteger(days ?? "30", "days", { min: 1 }));
397
+ if (jsonRequested(opts))
371
398
  return outJson(usage);
372
399
  console.log("\n Summary:");
373
400
  table([usage.summary], [
@@ -387,23 +414,23 @@ function registerMods(spacesCmd) {
387
414
  const modsCmd = spacesCmd
388
415
  .command("mods")
389
416
  .description("Manage space mods")
390
- .hook("preAction", () => { requireSpace(spacesCmd); });
417
+ .hook("preAction", () => { resolveSpace(spacesCmd); });
391
418
  modsCmd
392
419
  .command("ls")
393
420
  .alias("list")
394
421
  .description("List mods")
395
422
  .option("--json", "Output as JSON")
396
423
  .action(async (opts) => {
397
- const spaceId = requireSpace(spacesCmd);
424
+ const spaceId = resolveSpace(spacesCmd);
398
425
  const client = createClient();
399
426
  try {
400
427
  const result = await client.space(spaceId).mods.list();
401
- if (opts.json)
428
+ if (jsonRequested(opts))
402
429
  return outJson(result.items);
403
430
  table(result.items, [
404
431
  { key: "id", label: "ID" },
405
432
  { key: "modSpaceName", label: "Name" },
406
- { key: "modSpaceId", label: "Space" },
433
+ { key: "mountPath", label: "Mount" },
407
434
  { key: "enabled", label: "On" },
408
435
  ]);
409
436
  }
@@ -414,15 +441,19 @@ function registerMods(spacesCmd) {
414
441
  modsCmd
415
442
  .command("add <modSpaceId>")
416
443
  .description("Add a mod")
444
+ .option("--name <name>", "Display name")
445
+ .option("--slug <slug>", "Mount slug")
446
+ .option("-y, --yes", "Confirm sandbox restart")
417
447
  .option("--json", "Output as JSON")
418
448
  .action(async (modSpaceId, opts) => {
419
- const spaceId = requireSpace(spacesCmd);
449
+ await confirmRestart(opts);
450
+ const spaceId = resolveSpace(spacesCmd);
420
451
  const client = createClient();
421
452
  try {
422
- const result = await client.space(spaceId).mods.create({ modSpaceId });
423
- if (opts.json)
453
+ const result = await client.space(spaceId).mods.create({ modSpaceId, name: opts.name, mountSlug: opts.slug });
454
+ if (jsonRequested(opts))
424
455
  return outJson(result);
425
- ok("Mod added");
456
+ ok(`Mod added — ${result.item.mountPath}; sandbox restarting`);
426
457
  }
427
458
  catch (e) {
428
459
  handleHttp(e);
@@ -431,15 +462,17 @@ function registerMods(spacesCmd) {
431
462
  modsCmd
432
463
  .command("enable <modId>")
433
464
  .description("Enable a mod")
465
+ .option("-y, --yes", "Confirm sandbox restart")
434
466
  .option("--json", "Output as JSON")
435
467
  .action(async (modId, opts) => {
436
- const spaceId = requireSpace(spacesCmd);
468
+ await confirmRestart(opts);
469
+ const spaceId = resolveSpace(spacesCmd);
437
470
  const client = createClient();
438
471
  try {
439
472
  const result = await client.space(spaceId).mods.update(modId, { enabled: true });
440
- if (opts.json)
473
+ if (jsonRequested(opts))
441
474
  return outJson(result);
442
- ok("Mod enabled");
475
+ ok("Mod enabled; sandbox restarting");
443
476
  }
444
477
  catch (e) {
445
478
  handleHttp(e);
@@ -448,15 +481,17 @@ function registerMods(spacesCmd) {
448
481
  modsCmd
449
482
  .command("disable <modId>")
450
483
  .description("Disable a mod")
484
+ .option("-y, --yes", "Confirm sandbox restart")
451
485
  .option("--json", "Output as JSON")
452
486
  .action(async (modId, opts) => {
453
- const spaceId = requireSpace(spacesCmd);
487
+ await confirmRestart(opts);
488
+ const spaceId = resolveSpace(spacesCmd);
454
489
  const client = createClient();
455
490
  try {
456
491
  const result = await client.space(spaceId).mods.update(modId, { enabled: false });
457
- if (opts.json)
492
+ if (jsonRequested(opts))
458
493
  return outJson(result);
459
- ok("Mod disabled");
494
+ ok("Mod disabled; sandbox restarting");
460
495
  }
461
496
  catch (e) {
462
497
  handleHttp(e);
@@ -466,15 +501,17 @@ function registerMods(spacesCmd) {
466
501
  .command("rm <modId>")
467
502
  .alias("remove")
468
503
  .description("Remove a mod")
504
+ .option("-y, --yes", "Confirm sandbox restart")
469
505
  .option("--json", "Output as JSON")
470
506
  .action(async (modId, opts) => {
471
- const spaceId = requireSpace(spacesCmd);
507
+ await confirmRestart(opts);
508
+ const spaceId = resolveSpace(spacesCmd);
472
509
  const client = createClient();
473
510
  try {
474
511
  const result = await client.space(spaceId).mods.remove(modId);
475
- if (opts.json)
512
+ if (jsonRequested(opts))
476
513
  return outJson(result);
477
- ok("Mod removed");
514
+ ok("Mod removed; sandbox restarting");
478
515
  }
479
516
  catch (e) {
480
517
  handleHttp(e);
@@ -486,18 +523,18 @@ function registerFiles(spacesCmd) {
486
523
  const filesCmd = spacesCmd
487
524
  .command("files")
488
525
  .description("File operations")
489
- .hook("preAction", () => { requireSpace(spacesCmd); });
526
+ .hook("preAction", () => { resolveSpace(spacesCmd); });
490
527
  filesCmd
491
528
  .command("ls [path]")
492
529
  .alias("list")
493
530
  .description("List directory tree")
494
531
  .option("--json", "Output as JSON")
495
532
  .action(async (path, opts) => {
496
- const spaceId = requireSpace(spacesCmd);
533
+ const spaceId = resolveSpace(spacesCmd);
497
534
  const client = createClient();
498
535
  try {
499
536
  const tree = await client.space(spaceId).files.list(path ?? "");
500
- if (opts.json)
537
+ if (jsonRequested(opts))
501
538
  return outJson(tree);
502
539
  if (tree.entries.length === 0) {
503
540
  console.log(" (empty)");
@@ -518,7 +555,7 @@ function registerFiles(spacesCmd) {
518
555
  .command("cat <path>")
519
556
  .description("Read file content")
520
557
  .action(async (path) => {
521
- const spaceId = requireSpace(spacesCmd);
558
+ const spaceId = resolveSpace(spacesCmd);
522
559
  const client = createClient();
523
560
  try {
524
561
  const file = await client.space(spaceId).files.read(path);
@@ -548,7 +585,7 @@ function registerFiles(spacesCmd) {
548
585
  }
549
586
  if (!content)
550
587
  return error("No content provided", "Use -c or pipe via stdin");
551
- const spaceId = requireSpace(spacesCmd);
588
+ const spaceId = resolveSpace(spacesCmd);
552
589
  const client = createClient();
553
590
  try {
554
591
  const result = await client.space(spaceId).files.write({
@@ -572,7 +609,7 @@ function registerFiles(spacesCmd) {
572
609
  .command("mkdir <path>")
573
610
  .description("Create a directory")
574
611
  .action(async (path) => {
575
- const spaceId = requireSpace(spacesCmd);
612
+ const spaceId = resolveSpace(spacesCmd);
576
613
  const client = createClient();
577
614
  try {
578
615
  await client.space(spaceId).files.createDir(path);
@@ -587,7 +624,7 @@ function registerFiles(spacesCmd) {
587
624
  .description("Delete a file or directory")
588
625
  .option("-r, --recursive", "Delete recursively")
589
626
  .action(async (path, opts) => {
590
- const spaceId = requireSpace(spacesCmd);
627
+ const spaceId = resolveSpace(spacesCmd);
591
628
  const client = createClient();
592
629
  try {
593
630
  await client.space(spaceId).files.delete(path, opts.recursive ?? false);
@@ -601,7 +638,7 @@ function registerFiles(spacesCmd) {
601
638
  .command("mv <from> <to>")
602
639
  .description("Move or rename")
603
640
  .action(async (from, to) => {
604
- const spaceId = requireSpace(spacesCmd);
641
+ const spaceId = resolveSpace(spacesCmd);
605
642
  const client = createClient();
606
643
  try {
607
644
  await client.space(spaceId).files.move({ fromPath: from, toPath: to });
@@ -617,18 +654,18 @@ function registerSessions(spacesCmd) {
617
654
  const sessionsCmd = spacesCmd
618
655
  .command("sessions")
619
656
  .description("Browse sessions and turns")
620
- .hook("preAction", () => { requireSpace(spacesCmd); });
657
+ .hook("preAction", () => { resolveSpace(spacesCmd); });
621
658
  sessionsCmd
622
659
  .command("ls")
623
660
  .alias("list")
624
661
  .description("List sessions")
625
662
  .option("--json", "Output as JSON")
626
663
  .action(async (opts) => {
627
- const spaceId = requireSpace(spacesCmd);
664
+ const spaceId = resolveSpace(spacesCmd);
628
665
  const client = createClient();
629
666
  try {
630
667
  const result = await client.space(spaceId).sessions.list();
631
- if (opts.json)
668
+ if (jsonRequested(opts))
632
669
  return outJson(result);
633
670
  if (result.sessions.length === 0) {
634
671
  console.log(" (empty)");
@@ -650,11 +687,11 @@ function registerSessions(spacesCmd) {
650
687
  .description("Create a session")
651
688
  .option("--json", "Output as JSON")
652
689
  .action(async (title, opts) => {
653
- const spaceId = requireSpace(spacesCmd);
690
+ const spaceId = resolveSpace(spacesCmd);
654
691
  const client = createClient();
655
692
  try {
656
693
  const result = await client.space(spaceId).sessions.create({ title });
657
- if (opts.json)
694
+ if (jsonRequested(opts))
658
695
  return outJson(result);
659
696
  ok(`Session created: ${result.session.id}`);
660
697
  table([result.session], [
@@ -671,11 +708,11 @@ function registerSessions(spacesCmd) {
671
708
  .description("Session details")
672
709
  .option("--json", "Output as JSON")
673
710
  .action(async (id, opts) => {
674
- const spaceId = requireSpace(spacesCmd);
711
+ const spaceId = resolveSpace(spacesCmd);
675
712
  const client = createClient();
676
713
  try {
677
714
  const result = await client.space(spaceId).session(id).get();
678
- if (opts.json)
715
+ if (jsonRequested(opts))
679
716
  return outJson(result);
680
717
  table([result.session], [
681
718
  { key: "id", label: "ID" },
@@ -693,7 +730,7 @@ function registerSessions(spacesCmd) {
693
730
  .command("rename <id> <name>")
694
731
  .description("Rename a session")
695
732
  .action(async (id, name) => {
696
- const spaceId = requireSpace(spacesCmd);
733
+ const spaceId = resolveSpace(spacesCmd);
697
734
  const client = createClient();
698
735
  try {
699
736
  await client.space(spaceId).session(id).rename(name);
@@ -709,13 +746,13 @@ function registerSessions(spacesCmd) {
709
746
  .description("Stream realtime session events")
710
747
  .option("--json", "Output as JSON")
711
748
  .action(async (id, opts) => {
712
- const spaceId = requireSpace(spacesCmd);
749
+ const spaceId = resolveSpace(spacesCmd);
713
750
  const client = createClient();
714
751
  const session = client.space(spaceId).session(id);
715
752
  process.stdout.write(" Listening for events...\n\n");
716
753
  let lastAppendPath = null;
717
754
  session.on("turn.patch", (e) => {
718
- if (opts.json) {
755
+ if (jsonRequested(opts)) {
719
756
  console.log(JSON.stringify(e));
720
757
  }
721
758
  else {
@@ -742,7 +779,7 @@ function registerSessions(spacesCmd) {
742
779
  });
743
780
  session.on("turn.error", (e) => {
744
781
  process.stderr.write(`\n ✗ Error\n`);
745
- if (opts.json)
782
+ if (jsonRequested(opts))
746
783
  process.stderr.write(`${JSON.stringify(e)}\n`);
747
784
  process.exit(1);
748
785
  });
@@ -764,15 +801,15 @@ function registerTurns(sessionsCmd) {
764
801
  .option("--limit <n>", "Page size", "30")
765
802
  .option("--json", "Output as JSON")
766
803
  .action(async (sessionId, opts) => {
767
- const spaceId = requireSpace(sessionsCmd);
804
+ const spaceId = resolveSpace(sessionsCmd);
768
805
  const client = createClient();
769
806
  try {
770
807
  const result = await client.space(spaceId).session(sessionId).turns.listPaginated({
771
- cursor: opts.cursor === undefined ? undefined : Number.parseInt(opts.cursor, 10),
772
- direction: opts.direction,
773
- limit: Number.parseInt(opts.limit ?? "30", 10),
808
+ cursor: opts.cursor === undefined ? undefined : parseInteger(opts.cursor, "cursor", { min: 0 }),
809
+ direction: parseChoice(opts.direction ?? "older", "direction", ["older", "newer"]),
810
+ limit: parseInteger(opts.limit ?? "30", "limit", { min: 1, max: 100 }),
774
811
  });
775
- if (opts.json)
812
+ if (jsonRequested(opts))
776
813
  return outJson(result);
777
814
  if (result.turns.length === 0)
778
815
  return console.log(" No turns found");
@@ -796,11 +833,11 @@ function registerTurns(sessionsCmd) {
796
833
  .description("Show turn details")
797
834
  .option("--json", "Output as JSON")
798
835
  .action(async (sessionId, turnId, opts) => {
799
- const spaceId = requireSpace(sessionsCmd);
836
+ const spaceId = resolveSpace(sessionsCmd);
800
837
  const client = createClient();
801
838
  try {
802
839
  const result = await client.space(spaceId).session(sessionId).turns.get(turnId);
803
- if (opts.json)
840
+ if (jsonRequested(opts))
804
841
  return outJson(result);
805
842
  table([result.turn], [
806
843
  { key: "sequence", label: "Seq" },
@@ -827,14 +864,14 @@ function registerTurns(sessionsCmd) {
827
864
  .option("--limit <n>", "Page size", "100")
828
865
  .option("--json", "Output as JSON")
829
866
  .action(async (sessionId, opts) => {
830
- const spaceId = requireSpace(sessionsCmd);
867
+ const spaceId = resolveSpace(sessionsCmd);
831
868
  const client = createClient();
832
869
  try {
833
870
  const result = await client.space(spaceId).session(sessionId).turns.index({
834
- cursor: opts.cursor === undefined ? undefined : Number.parseInt(opts.cursor, 10),
835
- limit: Number.parseInt(opts.limit ?? "100", 10),
871
+ cursor: opts.cursor === undefined ? undefined : parseInteger(opts.cursor, "cursor", { min: 0 }),
872
+ limit: parseInteger(opts.limit ?? "100", "limit", { min: 1, max: 500 }),
836
873
  });
837
- if (opts.json)
874
+ if (jsonRequested(opts))
838
875
  return outJson(result);
839
876
  if (result.turns.length === 0)
840
877
  return console.log(" No turns found");
@@ -861,18 +898,18 @@ function registerTurns(sessionsCmd) {
861
898
  .option("--after <n>", "Turns after anchor", "20")
862
899
  .option("--json", "Output as JSON")
863
900
  .action(async (sessionId, opts) => {
864
- const spaceId = requireSpace(sessionsCmd);
901
+ const spaceId = resolveSpace(sessionsCmd);
865
902
  if (!opts.sequence && !opts.turn)
866
903
  return error("Missing anchor", "Use --sequence <n> or --turn <id>");
867
904
  const client = createClient();
868
905
  try {
869
906
  const result = await client.space(spaceId).session(sessionId).turns.window({
870
- sequence: opts.sequence === undefined ? undefined : Number.parseInt(opts.sequence, 10),
907
+ sequence: opts.sequence === undefined ? undefined : parseInteger(opts.sequence, "sequence", { min: 0 }),
871
908
  turnId: opts.turn,
872
- before: Number.parseInt(opts.before ?? "10", 10),
873
- after: Number.parseInt(opts.after ?? "20", 10),
909
+ before: parseInteger(opts.before ?? "10", "before", { min: 0, max: 200 }),
910
+ after: parseInteger(opts.after ?? "20", "after", { min: 0, max: 200 }),
874
911
  });
875
- if (opts.json)
912
+ if (jsonRequested(opts))
876
913
  return outJson(result);
877
914
  if (result.turns.length === 0)
878
915
  return console.log(" No turns found");
@@ -901,7 +938,7 @@ function registerSessionAccess(sessionsCmd) {
901
938
  const client = createClient();
902
939
  try {
903
940
  const policy = await client.sessionAccess.get(id);
904
- if (opts.json)
941
+ if (jsonRequested(opts))
905
942
  return outJson(policy);
906
943
  table([policy], [
907
944
  { key: "signed_in_user", label: "Signed-in" },
@@ -921,9 +958,9 @@ function registerSessionAccess(sessionsCmd) {
921
958
  const client = createClient();
922
959
  try {
923
960
  const policy = await client.sessionAccess.set(id, {
924
- anonymous_user: (opts.anonymous ?? null),
961
+ anonymous_user: parseNullableRole(opts.anonymous, "anonymous role"),
925
962
  });
926
- if (opts.json)
963
+ if (jsonRequested(opts))
927
964
  return outJson(policy);
928
965
  ok("Session access updated");
929
966
  table([policy], [
@@ -954,18 +991,18 @@ function registerMembers(spacesCmd) {
954
991
  const memCmd = spacesCmd
955
992
  .command("members")
956
993
  .description("Member management")
957
- .hook("preAction", () => { requireSpace(spacesCmd); });
994
+ .hook("preAction", () => { resolveSpace(spacesCmd); });
958
995
  memCmd
959
996
  .command("ls")
960
997
  .alias("list")
961
998
  .description("List space members")
962
999
  .option("--json", "Output as JSON")
963
1000
  .action(async (opts) => {
964
- const spaceId = requireSpace(spacesCmd);
1001
+ const spaceId = resolveSpace(spacesCmd);
965
1002
  const client = createClient();
966
1003
  try {
967
1004
  const result = await client.space(spaceId).members.list();
968
- if (opts.json)
1005
+ if (jsonRequested(opts))
969
1006
  return outJson(result);
970
1007
  if (result.items.length === 0) {
971
1008
  console.log(" (empty)");
@@ -985,10 +1022,10 @@ function registerMembers(spacesCmd) {
985
1022
  .command("update <userId> <role>")
986
1023
  .description("Change member role (host | builder | guest)")
987
1024
  .action(async (userId, role) => {
988
- const spaceId = requireSpace(spacesCmd);
1025
+ const spaceId = resolveSpace(spacesCmd);
989
1026
  const client = createClient();
990
1027
  try {
991
- await client.space(spaceId).members.update(userId, role);
1028
+ await client.space(spaceId).members.update(userId, parseChoice(role, "role", SPACE_ROLES));
992
1029
  ok(`${userId} → ${role}`);
993
1030
  }
994
1031
  catch (e) {
@@ -999,7 +1036,7 @@ function registerMembers(spacesCmd) {
999
1036
  .command("remove <userId>")
1000
1037
  .description("Remove a member")
1001
1038
  .action(async (userId) => {
1002
- const spaceId = requireSpace(spacesCmd);
1039
+ const spaceId = resolveSpace(spacesCmd);
1003
1040
  const client = createClient();
1004
1041
  try {
1005
1042
  await client.space(spaceId).members.remove(userId);
@@ -1015,17 +1052,17 @@ function registerAccess(spacesCmd) {
1015
1052
  const accCmd = spacesCmd
1016
1053
  .command("access")
1017
1054
  .description("Access control")
1018
- .hook("preAction", () => { requireSpace(spacesCmd); });
1055
+ .hook("preAction", () => { resolveSpace(spacesCmd); });
1019
1056
  accCmd
1020
1057
  .command("get")
1021
1058
  .description("Get access policy")
1022
1059
  .option("--json", "Output as JSON")
1023
1060
  .action(async (opts) => {
1024
- const spaceId = requireSpace(spacesCmd);
1061
+ const spaceId = resolveSpace(spacesCmd);
1025
1062
  const client = createClient();
1026
1063
  try {
1027
1064
  const policy = await client.space(spaceId).access.get();
1028
- if (opts.json)
1065
+ if (jsonRequested(opts))
1029
1066
  return outJson(policy);
1030
1067
  table([policy], [
1031
1068
  { key: "signed_in_user", label: "Signed-in" },
@@ -1043,14 +1080,14 @@ function registerAccess(spacesCmd) {
1043
1080
  .option("--anonymous <role>", "Role for anonymous users (host|builder|guest|null)")
1044
1081
  .option("--json", "Output as JSON")
1045
1082
  .action(async (opts) => {
1046
- const spaceId = requireSpace(spacesCmd);
1083
+ const spaceId = resolveSpace(spacesCmd);
1047
1084
  const client = createClient();
1048
1085
  try {
1049
1086
  const policy = await client.space(spaceId).access.set({
1050
- signed_in_user: (opts.signedIn ?? null),
1051
- anonymous_user: (opts.anonymous ?? null),
1087
+ signed_in_user: parseNullableRole(opts.signedIn, "signed-in role"),
1088
+ anonymous_user: parseNullableRole(opts.anonymous, "anonymous role"),
1052
1089
  });
1053
- if (opts.json)
1090
+ if (jsonRequested(opts))
1054
1091
  return outJson(policy);
1055
1092
  ok("Access policy updated");
1056
1093
  table([policy], [
@@ -1068,18 +1105,18 @@ function registerCheckpoints(spacesCmd) {
1068
1105
  const cpCmd = spacesCmd
1069
1106
  .command("checkpoints")
1070
1107
  .description("Checkpoint management")
1071
- .hook("preAction", () => { requireSpace(spacesCmd); });
1108
+ .hook("preAction", () => { resolveSpace(spacesCmd); });
1072
1109
  cpCmd
1073
1110
  .command("ls")
1074
1111
  .alias("list")
1075
1112
  .description("List checkpoints")
1076
1113
  .option("--json", "Output as JSON")
1077
1114
  .action(async (opts) => {
1078
- const spaceId = requireSpace(spacesCmd);
1115
+ const spaceId = resolveSpace(spacesCmd);
1079
1116
  const client = createClient();
1080
1117
  try {
1081
1118
  const result = await client.space(spaceId).checkpoints.list();
1082
- if (opts.json)
1119
+ if (jsonRequested(opts))
1083
1120
  return outJson(result);
1084
1121
  if (result.checkpoints.length === 0) {
1085
1122
  console.log(" (empty)");
@@ -1101,11 +1138,11 @@ function registerCheckpoints(spacesCmd) {
1101
1138
  .description("Checkpoint details")
1102
1139
  .option("--json", "Output as JSON")
1103
1140
  .action(async (id, opts) => {
1104
- const spaceId = requireSpace(spacesCmd);
1141
+ const spaceId = resolveSpace(spacesCmd);
1105
1142
  const client = createClient();
1106
1143
  try {
1107
1144
  const result = await client.space(spaceId).checkpoints.get(id);
1108
- if (opts.json)
1145
+ if (jsonRequested(opts))
1109
1146
  return outJson(result);
1110
1147
  table([result.checkpoint], [
1111
1148
  { key: "id", label: "ID" },
@@ -1124,11 +1161,11 @@ function registerCheckpoints(spacesCmd) {
1124
1161
  .description("Create a checkpoint")
1125
1162
  .option("--json", "Output as JSON")
1126
1163
  .action(async (description, opts) => {
1127
- const spaceId = requireSpace(spacesCmd);
1164
+ const spaceId = resolveSpace(spacesCmd);
1128
1165
  const client = createClient();
1129
1166
  try {
1130
1167
  const result = await client.space(spaceId).checkpoints.create(description ?? null);
1131
- if (opts.json)
1168
+ if (jsonRequested(opts))
1132
1169
  return outJson(result);
1133
1170
  ok(`Checkpoint created — taskRunId: ${result.taskRunId}`);
1134
1171
  }