@neta-art/cohub-cli 1.2.0 → 1.4.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.
@@ -1,4 +1,7 @@
1
- import { resolveToken } from "../auth.js";
1
+ import { randomUUID } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+ import { readdir, stat } from "node:fs/promises";
4
+ import { basename, dirname, relative, resolve, sep } from "node:path";
2
5
  import { createClient } from "../client.js";
3
6
  import { table, json as outJson, ok, error, handleHttp } from "../output.js";
4
7
  function requireSpace(program) {
@@ -11,6 +14,151 @@ function requireSpace(program) {
11
14
  }
12
15
  return error("Missing required option", "Add -s, --space <id> to target a space");
13
16
  }
17
+ const slashPath = (value) => value.split(sep).join("/");
18
+ const walkUploadPath = async (input, root, prefix = "") => {
19
+ const localPath = resolve(input);
20
+ const info = await stat(localPath);
21
+ const name = basename(localPath);
22
+ const relativePath = slashPath(prefix ? `${prefix}/${name}` : relative(root, localPath) || name);
23
+ if (info.isDirectory()) {
24
+ const children = await readdir(localPath);
25
+ const nested = await Promise.all(children.map((child) => walkUploadPath(resolve(localPath, child), root, relativePath)));
26
+ return nested.flat();
27
+ }
28
+ if (!info.isFile())
29
+ return [];
30
+ return [{
31
+ id: randomUploadEntryId(),
32
+ localPath,
33
+ relativePath,
34
+ name,
35
+ size: info.size,
36
+ mimeType: null,
37
+ }];
38
+ };
39
+ const randomUploadEntryId = () => randomUUID();
40
+ async function collectUploadFiles(paths) {
41
+ if (paths.length === 0)
42
+ return error("No files provided", "Pass one or more local files or directories.");
43
+ const roots = paths.map((path) => {
44
+ const resolved = resolve(path);
45
+ return dirname(resolved);
46
+ });
47
+ const nested = await Promise.all(paths.map((path, index) => walkUploadPath(path, roots[index] ?? process.cwd())));
48
+ const files = nested.flat();
49
+ if (files.length === 0)
50
+ return error("No regular files found");
51
+ return files;
52
+ }
53
+ async function putUploadEntry(entry, uploadUrl, headers) {
54
+ const response = await fetch(uploadUrl, {
55
+ method: "PUT",
56
+ headers,
57
+ body: createReadStream(entry.localPath),
58
+ duplex: "half",
59
+ });
60
+ if (!response.ok) {
61
+ const detail = await response.text().catch(() => "");
62
+ throw new Error(`Failed to upload ${entry.relativePath}: HTTP ${response.status}${detail ? ` — ${detail}` : ""}`);
63
+ }
64
+ }
65
+ async function uploadFiles(command, paths, opts) {
66
+ const spaceId = requireSpace(command);
67
+ const client = createClient();
68
+ try {
69
+ const files = await collectUploadFiles(paths);
70
+ const plan = await client.space(spaceId).files.createUpload({
71
+ targetDir: opts.dir,
72
+ entries: files.map((file) => ({
73
+ id: file.id,
74
+ name: file.name,
75
+ relativePath: file.relativePath,
76
+ size: file.size,
77
+ mimeType: file.mimeType,
78
+ })),
79
+ });
80
+ const byId = new Map(files.map((file) => [file.id, file]));
81
+ for (const entry of plan.entries) {
82
+ const file = byId.get(entry.id);
83
+ if (!file)
84
+ throw new Error(`Missing upload entry: ${entry.id}`);
85
+ await putUploadEntry(file, entry.uploadUrl, entry.headers);
86
+ }
87
+ const result = await client.space(spaceId).files.completeUpload(plan.uploadId, {
88
+ entries: plan.entries.map((entry) => ({ id: entry.id })),
89
+ });
90
+ if (opts.json)
91
+ return outJson({ ...result, uploadId: plan.uploadId, files: files.length });
92
+ ok(`Uploaded ${files.length} file${files.length === 1 ? "" : "s"} — taskRunId: ${result.taskRunId}`);
93
+ }
94
+ catch (e) {
95
+ handleHttp(e);
96
+ }
97
+ }
98
+ async function readPromptContent(words) {
99
+ let content = words.join(" ");
100
+ if (!content && !process.stdin.isTTY) {
101
+ const chunks = [];
102
+ for await (const chunk of process.stdin)
103
+ chunks.push(chunk);
104
+ content = Buffer.concat(chunks).toString().trim();
105
+ }
106
+ if (!content)
107
+ return error("No content", "Pass as argument or pipe via stdin");
108
+ return content;
109
+ }
110
+ async function sendPrompt(command, words, opts) {
111
+ const content = await readPromptContent(words);
112
+ const scheduleFlags = [opts.delayMs, opts.at, opts.cron].filter((value) => value !== undefined);
113
+ if (scheduleFlags.length > 1)
114
+ return error("Conflicting schedule", "Use only one of --delay-ms, --at, or --cron");
115
+ if (opts.cron && !opts.timezone)
116
+ return error("Missing timezone", "--timezone is required with --cron");
117
+ const spaceId = requireSpace(command);
118
+ const client = createClient();
119
+ try {
120
+ const schedule = opts.delayMs
121
+ ? { mode: "delay", delayMs: Number.parseInt(opts.delayMs, 10) }
122
+ : opts.at
123
+ ? { mode: "at", sendAt: opts.at }
124
+ : opts.cron
125
+ ? { mode: "repeat", cronExpression: opts.cron, timezone: opts.timezone }
126
+ : undefined;
127
+ const result = await client.space(spaceId).prompt({
128
+ sessionId: opts.session,
129
+ title: opts.title,
130
+ content: [{ type: "text", text: content }],
131
+ model: opts.model,
132
+ provider: opts.provider,
133
+ schedule,
134
+ });
135
+ if (opts.json)
136
+ return outJson(result);
137
+ if (result.mode === "immediate")
138
+ return ok(`Prompt sent — sessionId: ${result.sessionId}, turnId: ${result.turnId}`);
139
+ if (result.mode === "repeat")
140
+ return ok(`Prompt scheduled — cronJobId: ${result.cronJobId}, nextRunAt: ${result.nextRunAt}`);
141
+ return ok(`Prompt scheduled — taskRunId: ${result.taskRunId}, scheduledAt: ${result.scheduledAt}`);
142
+ }
143
+ catch (e) {
144
+ handleHttp(e);
145
+ }
146
+ }
147
+ export function registerPrompt(program) {
148
+ program
149
+ .command("prompt [content...]")
150
+ .description("Send or schedule a prompt in a space")
151
+ .option("--session <id>", "Target session ID")
152
+ .option("--title <title>", "Title for a newly created session or schedule")
153
+ .option("-m, --model <model>", "Model name")
154
+ .option("-p, --provider <provider>", "Provider name")
155
+ .option("--delay-ms <ms>", "Delay sending by milliseconds")
156
+ .option("--at <iso>", "Send once at an ISO 8601 time with timezone")
157
+ .option("--cron <expression>", "Repeat using a 5-field cron expression")
158
+ .option("--timezone <tz>", "IANA timezone for --cron, e.g. Asia/Shanghai")
159
+ .option("--json", "Output as JSON")
160
+ .action((words, opts) => sendPrompt(program, words, opts));
161
+ }
14
162
  export function registerSpaces(program) {
15
163
  const spacesCmd = program.command("spaces").description("Space management");
16
164
  // ── spaces ls ──
@@ -20,8 +168,7 @@ export function registerSpaces(program) {
20
168
  .description("List all spaces")
21
169
  .option("--json", "Output as JSON")
22
170
  .action(async (opts) => {
23
- const token = resolveToken() ?? missingAuth();
24
- const client = createClient(token);
171
+ const client = createClient();
25
172
  try {
26
173
  const items = await client.spaces.list();
27
174
  if (opts.json)
@@ -42,8 +189,7 @@ export function registerSpaces(program) {
42
189
  .description("Show space details")
43
190
  .option("--json", "Output as JSON")
44
191
  .action(async (id, opts) => {
45
- const token = resolveToken() ?? missingAuth();
46
- const client = createClient(token);
192
+ const client = createClient();
47
193
  try {
48
194
  const space = await client.spaces.get(id);
49
195
  if (opts.json)
@@ -68,8 +214,7 @@ export function registerSpaces(program) {
68
214
  .option("-d, --description <desc>", "Space description")
69
215
  .option("--json", "Output as JSON")
70
216
  .action(async (opts) => {
71
- const token = resolveToken() ?? missingAuth();
72
- const client = createClient(token);
217
+ const client = createClient();
73
218
  try {
74
219
  const result = await client.spaces.create({
75
220
  name: opts.name,
@@ -93,8 +238,7 @@ export function registerSpaces(program) {
93
238
  .command("rename <id> <name>")
94
239
  .description("Rename a space")
95
240
  .action(async (id, name) => {
96
- const token = resolveToken() ?? missingAuth();
97
- const client = createClient(token);
241
+ const client = createClient();
98
242
  try {
99
243
  await client.space(id).rename(name);
100
244
  ok(`Space renamed to "${name}"`);
@@ -105,7 +249,8 @@ export function registerSpaces(program) {
105
249
  });
106
250
  // ── spaces prompt ──
107
251
  spacesCmd
108
- .command("prompt [content...]")
252
+ .command("prompt [content...]", { hidden: true })
253
+ .alias("send")
109
254
  .description("Send or schedule a prompt in the target space")
110
255
  .option("--session <id>", "Target session ID")
111
256
  .option("--title <title>", "Title for a newly created session or schedule")
@@ -116,52 +261,7 @@ export function registerSpaces(program) {
116
261
  .option("--cron <expression>", "Repeat using a 5-field cron expression")
117
262
  .option("--timezone <tz>", "IANA timezone for --cron, e.g. Asia/Shanghai")
118
263
  .option("--json", "Output as JSON")
119
- .action(async (words, opts) => {
120
- const token = resolveToken() ?? missingAuth();
121
- let content = words.join(" ");
122
- if (!content && !process.stdin.isTTY) {
123
- const chunks = [];
124
- for await (const chunk of process.stdin)
125
- chunks.push(chunk);
126
- content = Buffer.concat(chunks).toString().trim();
127
- }
128
- if (!content)
129
- return error("No content", "Pass as argument or pipe via stdin");
130
- const scheduleFlags = [opts.delayMs, opts.at, opts.cron].filter((value) => value !== undefined);
131
- if (scheduleFlags.length > 1)
132
- return error("Conflicting schedule", "Use only one of --delay-ms, --at, or --cron");
133
- if (opts.cron && !opts.timezone)
134
- return error("Missing timezone", "--timezone is required with --cron");
135
- const spaceId = requireSpace(spacesCmd);
136
- const client = createClient(token);
137
- try {
138
- const schedule = opts.delayMs
139
- ? { mode: "delay", delayMs: Number.parseInt(opts.delayMs, 10) }
140
- : opts.at
141
- ? { mode: "at", sendAt: opts.at }
142
- : opts.cron
143
- ? { mode: "repeat", cronExpression: opts.cron, timezone: opts.timezone }
144
- : undefined;
145
- const result = await client.space(spaceId).prompt({
146
- sessionId: opts.session,
147
- title: opts.title,
148
- content: [{ type: "text", text: content }],
149
- model: opts.model,
150
- provider: opts.provider,
151
- schedule,
152
- });
153
- if (opts.json)
154
- return outJson(result);
155
- if (result.mode === "immediate")
156
- return ok(`Prompt sent — sessionId: ${result.sessionId}, turnId: ${result.turnId}`);
157
- if (result.mode === "repeat")
158
- return ok(`Prompt scheduled — cronJobId: ${result.cronJobId}, nextRunAt: ${result.nextRunAt}`);
159
- return ok(`Prompt scheduled — taskRunId: ${result.taskRunId}, scheduledAt: ${result.scheduledAt}`);
160
- }
161
- catch (e) {
162
- handleHttp(e);
163
- }
164
- });
264
+ .action((words, opts) => sendPrompt(spacesCmd, words, opts));
165
265
  // ── spaces files ──
166
266
  registerFiles(spacesCmd);
167
267
  // ── spaces sessions ──
@@ -178,9 +278,8 @@ export function registerSpaces(program) {
178
278
  .description("Space usage statistics (default: 30 days)")
179
279
  .option("--json", "Output as JSON")
180
280
  .action(async (days, opts) => {
181
- const token = resolveToken() ?? missingAuth();
182
281
  const spaceId = requireSpace(spacesCmd);
183
- const client = createClient(token);
282
+ const client = createClient();
184
283
  try {
185
284
  const usage = await client.space(spaceId).usage.get(Number.parseInt(days ?? "30", 10));
186
285
  if (opts.json)
@@ -211,9 +310,8 @@ function registerFiles(spacesCmd) {
211
310
  .description("List directory tree")
212
311
  .option("--json", "Output as JSON")
213
312
  .action(async (path, opts) => {
214
- const token = resolveToken() ?? missingAuth();
215
313
  const spaceId = requireSpace(spacesCmd);
216
- const client = createClient(token);
314
+ const client = createClient();
217
315
  try {
218
316
  const tree = await client.space(spaceId).files.list(path ?? "");
219
317
  if (opts.json)
@@ -237,11 +335,15 @@ function registerFiles(spacesCmd) {
237
335
  .command("cat <path>")
238
336
  .description("Read file content")
239
337
  .action(async (path) => {
240
- const token = resolveToken() ?? missingAuth();
241
338
  const spaceId = requireSpace(spacesCmd);
242
- const client = createClient(token);
339
+ const client = createClient();
243
340
  try {
244
341
  const file = await client.space(spaceId).files.read(path);
342
+ if (!("content" in file))
343
+ return error("File is being prepared. Please retry shortly.");
344
+ if (file.delivery === "url" && file.url) {
345
+ console.log(`[CDN] ${file.url}`);
346
+ }
245
347
  console.log(file.content);
246
348
  }
247
349
  catch (e) {
@@ -254,7 +356,6 @@ function registerFiles(spacesCmd) {
254
356
  .option("-c, --content <text>", "File content")
255
357
  .option("-e, --encoding <enc>", "Encoding (utf-8 or base64)", "utf-8")
256
358
  .action(async (path, opts) => {
257
- const token = resolveToken() ?? missingAuth();
258
359
  let content = opts.content ?? "";
259
360
  if (!content && !process.stdin.isTTY) {
260
361
  const chunks = [];
@@ -265,7 +366,7 @@ function registerFiles(spacesCmd) {
265
366
  if (!content)
266
367
  return error("No content provided", "Use -c or pipe via stdin");
267
368
  const spaceId = requireSpace(spacesCmd);
268
- const client = createClient(token);
369
+ const client = createClient();
269
370
  try {
270
371
  const result = await client.space(spaceId).files.write({
271
372
  path,
@@ -278,13 +379,18 @@ function registerFiles(spacesCmd) {
278
379
  handleHttp(e);
279
380
  }
280
381
  });
382
+ filesCmd
383
+ .command("upload <paths...>")
384
+ .description("Upload local files or directories")
385
+ .option("--dir <path>", "Target directory in the space")
386
+ .option("--json", "Output as JSON")
387
+ .action((paths, opts) => uploadFiles(spacesCmd, paths, opts));
281
388
  filesCmd
282
389
  .command("mkdir <path>")
283
390
  .description("Create a directory")
284
391
  .action(async (path) => {
285
- const token = resolveToken() ?? missingAuth();
286
392
  const spaceId = requireSpace(spacesCmd);
287
- const client = createClient(token);
393
+ const client = createClient();
288
394
  try {
289
395
  await client.space(spaceId).files.createDir(path);
290
396
  ok(`Directory created: ${path}`);
@@ -298,9 +404,8 @@ function registerFiles(spacesCmd) {
298
404
  .description("Delete a file or directory")
299
405
  .option("-r, --recursive", "Delete recursively")
300
406
  .action(async (path, opts) => {
301
- const token = resolveToken() ?? missingAuth();
302
407
  const spaceId = requireSpace(spacesCmd);
303
- const client = createClient(token);
408
+ const client = createClient();
304
409
  try {
305
410
  await client.space(spaceId).files.delete(path, opts.recursive ?? false);
306
411
  ok(`Deleted: ${path}`);
@@ -313,9 +418,8 @@ function registerFiles(spacesCmd) {
313
418
  .command("mv <from> <to>")
314
419
  .description("Move or rename")
315
420
  .action(async (from, to) => {
316
- const token = resolveToken() ?? missingAuth();
317
421
  const spaceId = requireSpace(spacesCmd);
318
- const client = createClient(token);
422
+ const client = createClient();
319
423
  try {
320
424
  await client.space(spaceId).files.move({ fromPath: from, toPath: to });
321
425
  ok(`Moved: ${from} → ${to}`);
@@ -324,19 +428,12 @@ function registerFiles(spacesCmd) {
324
428
  handleHttp(e);
325
429
  }
326
430
  });
327
- filesCmd
328
- .command("upload <files...>")
329
- .description("Upload files to a directory")
330
- .option("--dir <dir>", "Target directory", "")
331
- .action(async (_files) => {
332
- error("Upload requires browser File API", "Use the web interface for now");
333
- });
334
431
  }
335
432
  // ── Session operations ──
336
433
  function registerSessions(spacesCmd) {
337
434
  const sessionsCmd = spacesCmd
338
435
  .command("sessions")
339
- .description("Session operations")
436
+ .description("Browse sessions and turns")
340
437
  .hook("preAction", () => { requireSpace(spacesCmd); });
341
438
  sessionsCmd
342
439
  .command("ls")
@@ -344,9 +441,8 @@ function registerSessions(spacesCmd) {
344
441
  .description("List sessions")
345
442
  .option("--json", "Output as JSON")
346
443
  .action(async (opts) => {
347
- const token = resolveToken() ?? missingAuth();
348
444
  const spaceId = requireSpace(spacesCmd);
349
- const client = createClient(token);
445
+ const client = createClient();
350
446
  try {
351
447
  const result = await client.space(spaceId).sessions.list();
352
448
  if (opts.json)
@@ -371,9 +467,8 @@ function registerSessions(spacesCmd) {
371
467
  .description("Create a session")
372
468
  .option("--json", "Output as JSON")
373
469
  .action(async (title, opts) => {
374
- const token = resolveToken() ?? missingAuth();
375
470
  const spaceId = requireSpace(spacesCmd);
376
- const client = createClient(token);
471
+ const client = createClient();
377
472
  try {
378
473
  const result = await client.space(spaceId).sessions.create({ title });
379
474
  if (opts.json)
@@ -393,9 +488,8 @@ function registerSessions(spacesCmd) {
393
488
  .description("Session details")
394
489
  .option("--json", "Output as JSON")
395
490
  .action(async (id, opts) => {
396
- const token = resolveToken() ?? missingAuth();
397
491
  const spaceId = requireSpace(spacesCmd);
398
- const client = createClient(token);
492
+ const client = createClient();
399
493
  try {
400
494
  const result = await client.space(spaceId).session(id).get();
401
495
  if (opts.json)
@@ -416,9 +510,8 @@ function registerSessions(spacesCmd) {
416
510
  .command("rename <id> <name>")
417
511
  .description("Rename a session")
418
512
  .action(async (id, name) => {
419
- const token = resolveToken() ?? missingAuth();
420
513
  const spaceId = requireSpace(spacesCmd);
421
- const client = createClient(token);
514
+ const client = createClient();
422
515
  try {
423
516
  await client.space(spaceId).session(id).rename(name);
424
517
  ok(`Session renamed to "${name}"`);
@@ -427,17 +520,14 @@ function registerSessions(spacesCmd) {
427
520
  handleHttp(e);
428
521
  }
429
522
  });
430
- // ── sessions messages ──
431
- registerMessages(sessionsCmd);
432
523
  // ── sessions tail ──
433
524
  sessionsCmd
434
525
  .command("tail <id>")
435
526
  .description("Stream realtime session events")
436
527
  .option("--json", "Output as JSON")
437
528
  .action(async (id, opts) => {
438
- const token = resolveToken() ?? missingAuth();
439
529
  const spaceId = requireSpace(spacesCmd);
440
- const client = createClient(token);
530
+ const client = createClient();
441
531
  const session = client.space(spaceId).session(id);
442
532
  process.stdout.write(" Listening for events...\n\n");
443
533
  let lastAppendPath = null;
@@ -474,71 +564,202 @@ function registerSessions(spacesCmd) {
474
564
  process.exit(1);
475
565
  });
476
566
  });
567
+ // ── sessions turns ──
568
+ registerTurns(sessionsCmd);
569
+ // ── sessions access ──
570
+ registerSessionAccess(sessionsCmd);
477
571
  }
478
- // ── Message operations ──
479
- function registerMessages(sessionsCmd) {
480
- const msgsCmd = sessionsCmd.command("messages").description("Message operations");
481
- msgsCmd
572
+ // ── Turn operations ──
573
+ function registerTurns(sessionsCmd) {
574
+ const turnsCmd = sessionsCmd.command("turns").description("Inspect session turns");
575
+ turnsCmd
482
576
  .command("ls <sessionId>")
483
577
  .alias("list")
484
- .description("List session messages")
578
+ .description("List recent turns")
579
+ .option("--cursor <sequence>", "Turn sequence cursor")
580
+ .option("--direction <older|newer>", "Page direction", "older")
581
+ .option("--limit <n>", "Page size", "30")
485
582
  .option("--json", "Output as JSON")
486
- .option("--limit <n>", "Page size", "50")
487
583
  .action(async (sessionId, opts) => {
488
- const token = resolveToken() ?? missingAuth();
489
584
  const spaceId = requireSpace(sessionsCmd);
490
- const client = createClient(token);
585
+ const client = createClient();
491
586
  try {
492
- const result = await client.space(spaceId).session(sessionId).messages.listPaginated({
493
- limit: Number.parseInt(opts.limit ?? "50", 10),
587
+ const result = await client.space(spaceId).session(sessionId).turns.listPaginated({
588
+ cursor: opts.cursor === undefined ? undefined : Number.parseInt(opts.cursor, 10),
589
+ direction: opts.direction,
590
+ limit: Number.parseInt(opts.limit ?? "30", 10),
494
591
  });
495
592
  if (opts.json)
496
593
  return outJson(result);
497
- if (result.messages.length === 0) {
498
- console.log(" (empty)");
499
- return;
500
- }
501
- table(result.messages, [
594
+ if (result.turns.length === 0)
595
+ return console.log(" No turns found");
596
+ table(result.turns, [
597
+ { key: "sequence", label: "Seq" },
502
598
  { key: "id", label: "ID" },
503
- { key: "role", label: "Role" },
504
- { key: "createdAt", label: "Created" },
599
+ { key: "status", label: "Status" },
600
+ { key: "userText", label: "User" },
601
+ { key: "assistantText", label: "Assistant" },
602
+ { key: "updatedAt", label: "Updated" },
505
603
  ]);
506
- if (result.hasMore) {
507
- console.log(`\n (moreuse --cursor ${result.nextCursor} for next page)`);
508
- }
604
+ if (result.hasMore)
605
+ console.log(`\n More turns available next cursor: ${result.nextCursor}`);
509
606
  }
510
607
  catch (e) {
511
608
  handleHttp(e);
512
609
  }
513
610
  });
514
- msgsCmd
515
- .command("send <sessionId> [content...]")
516
- .description("Send a message to a session")
517
- .option("-m, --model <model>", "Model name")
518
- .option("-p, --provider <provider>", "Provider name")
611
+ turnsCmd
612
+ .command("get <sessionId> <turnId>")
613
+ .description("Show turn details")
519
614
  .option("--json", "Output as JSON")
520
- .action(async (sessionId, words, opts) => {
521
- const token = resolveToken() ?? missingAuth();
522
- let content = words.join(" ");
523
- if (!content && !process.stdin.isTTY) {
524
- const chunks = [];
525
- for await (const chunk of process.stdin)
526
- chunks.push(chunk);
527
- content = Buffer.concat(chunks).toString().trim();
615
+ .action(async (sessionId, turnId, opts) => {
616
+ const spaceId = requireSpace(sessionsCmd);
617
+ const client = createClient();
618
+ try {
619
+ const result = await client.space(spaceId).session(sessionId).turns.get(turnId);
620
+ if (opts.json)
621
+ return outJson(result);
622
+ table([result.turn], [
623
+ { key: "sequence", label: "Seq" },
624
+ { key: "id", label: "ID" },
625
+ { key: "status", label: "Status" },
626
+ { key: "provider", label: "Provider" },
627
+ { key: "model", label: "Model" },
628
+ { key: "stopReason", label: "Stop" },
629
+ { key: "errorMessage", label: "Error" },
630
+ ]);
631
+ if (result.turn.userText)
632
+ console.log(`\nUser:\n${result.turn.userText}`);
633
+ if (result.turn.assistantText)
634
+ console.log(`\nAssistant:\n${result.turn.assistantText}`);
528
635
  }
529
- if (!content)
530
- return error("No content", "Pass as argument or pipe via stdin");
636
+ catch (e) {
637
+ handleHttp(e);
638
+ }
639
+ });
640
+ turnsCmd
641
+ .command("index <sessionId>", { hidden: true })
642
+ .description("List lightweight turn index")
643
+ .option("--cursor <sequence>", "Turn sequence cursor")
644
+ .option("--limit <n>", "Page size", "100")
645
+ .option("--json", "Output as JSON")
646
+ .action(async (sessionId, opts) => {
647
+ const spaceId = requireSpace(sessionsCmd);
648
+ const client = createClient();
649
+ try {
650
+ const result = await client.space(spaceId).session(sessionId).turns.index({
651
+ cursor: opts.cursor === undefined ? undefined : Number.parseInt(opts.cursor, 10),
652
+ limit: Number.parseInt(opts.limit ?? "100", 10),
653
+ });
654
+ if (opts.json)
655
+ return outJson(result);
656
+ if (result.turns.length === 0)
657
+ return console.log(" No turns found");
658
+ table(result.turns, [
659
+ { key: "sequence", label: "Seq" },
660
+ { key: "id", label: "ID" },
661
+ { key: "status", label: "Status" },
662
+ { key: "userPreview", label: "User" },
663
+ { key: "assistantPreview", label: "Assistant" },
664
+ ]);
665
+ if (result.hasMore)
666
+ console.log(`\n More turns available — next cursor: ${result.nextCursor}`);
667
+ }
668
+ catch (e) {
669
+ handleHttp(e);
670
+ }
671
+ });
672
+ turnsCmd
673
+ .command("window <sessionId>", { hidden: true })
674
+ .description("Load turns around a sequence or turn ID")
675
+ .option("--sequence <n>", "Anchor turn sequence")
676
+ .option("--turn <id>", "Anchor turn ID")
677
+ .option("--before <n>", "Turns before anchor", "10")
678
+ .option("--after <n>", "Turns after anchor", "20")
679
+ .option("--json", "Output as JSON")
680
+ .action(async (sessionId, opts) => {
531
681
  const spaceId = requireSpace(sessionsCmd);
532
- const client = createClient(token);
682
+ if (!opts.sequence && !opts.turn)
683
+ return error("Missing anchor", "Use --sequence <n> or --turn <id>");
684
+ const client = createClient();
533
685
  try {
534
- const result = await client.space(spaceId).session(sessionId).messages.send({
535
- content: [{ type: "text", text: content }],
536
- model: opts.model,
537
- provider: opts.provider,
686
+ const result = await client.space(spaceId).session(sessionId).turns.window({
687
+ sequence: opts.sequence === undefined ? undefined : Number.parseInt(opts.sequence, 10),
688
+ turnId: opts.turn,
689
+ before: Number.parseInt(opts.before ?? "10", 10),
690
+ after: Number.parseInt(opts.after ?? "20", 10),
538
691
  });
539
692
  if (opts.json)
540
693
  return outJson(result);
541
- ok(`Message sent — userMessageId: ${result.userMessageId}`);
694
+ if (result.turns.length === 0)
695
+ return console.log(" No turns found");
696
+ table(result.turns, [
697
+ { key: "sequence", label: "Seq" },
698
+ { key: "id", label: "ID" },
699
+ { key: "status", label: "Status" },
700
+ { key: "userText", label: "User" },
701
+ { key: "assistantText", label: "Assistant" },
702
+ ]);
703
+ console.log(`\n Window — older: ${result.hasMoreOlder ? "yes" : "no"}, newer: ${result.hasMoreNewer ? "yes" : "no"}`);
704
+ }
705
+ catch (e) {
706
+ handleHttp(e);
707
+ }
708
+ });
709
+ }
710
+ // ── Session access operations ──
711
+ function registerSessionAccess(sessionsCmd) {
712
+ const accessCmd = sessionsCmd.command("access").description("Session access control");
713
+ accessCmd
714
+ .command("get <id>")
715
+ .description("Get session access policy")
716
+ .option("--json", "Output as JSON")
717
+ .action(async (id, opts) => {
718
+ const client = createClient();
719
+ try {
720
+ const policy = await client.sessionAccess.get(id);
721
+ if (opts.json)
722
+ return outJson(policy);
723
+ table([policy], [
724
+ { key: "signed_in_user", label: "Signed-in" },
725
+ { key: "anonymous_user", label: "Anonymous" },
726
+ ]);
727
+ }
728
+ catch (e) {
729
+ handleHttp(e);
730
+ }
731
+ });
732
+ accessCmd
733
+ .command("set <id>")
734
+ .description("Set session anonymous access")
735
+ .option("--anonymous <role>", "Anonymous role (host|builder|guest|null)")
736
+ .option("--json", "Output as JSON")
737
+ .action(async (id, opts) => {
738
+ const client = createClient();
739
+ try {
740
+ const policy = await client.sessionAccess.set(id, {
741
+ anonymous_user: (opts.anonymous ?? null),
742
+ });
743
+ if (opts.json)
744
+ return outJson(policy);
745
+ ok("Session access updated");
746
+ table([policy], [
747
+ { key: "signed_in_user", label: "Signed-in" },
748
+ { key: "anonymous_user", label: "Anonymous" },
749
+ ]);
750
+ }
751
+ catch (e) {
752
+ handleHttp(e);
753
+ }
754
+ });
755
+ accessCmd
756
+ .command("remove <id>")
757
+ .description("Remove session access override")
758
+ .action(async (id) => {
759
+ const client = createClient();
760
+ try {
761
+ await client.sessionAccess.remove(id);
762
+ ok(`Session access override removed: ${id}`);
542
763
  }
543
764
  catch (e) {
544
765
  handleHttp(e);
@@ -557,9 +778,8 @@ function registerMembers(spacesCmd) {
557
778
  .description("List space members")
558
779
  .option("--json", "Output as JSON")
559
780
  .action(async (opts) => {
560
- const token = resolveToken() ?? missingAuth();
561
781
  const spaceId = requireSpace(spacesCmd);
562
- const client = createClient(token);
782
+ const client = createClient();
563
783
  try {
564
784
  const result = await client.space(spaceId).members.list();
565
785
  if (opts.json)
@@ -582,9 +802,8 @@ function registerMembers(spacesCmd) {
582
802
  .command("update <userId> <role>")
583
803
  .description("Change member role (host | builder | guest)")
584
804
  .action(async (userId, role) => {
585
- const token = resolveToken() ?? missingAuth();
586
805
  const spaceId = requireSpace(spacesCmd);
587
- const client = createClient(token);
806
+ const client = createClient();
588
807
  try {
589
808
  await client.space(spaceId).members.update(userId, role);
590
809
  ok(`${userId} → ${role}`);
@@ -597,9 +816,8 @@ function registerMembers(spacesCmd) {
597
816
  .command("remove <userId>")
598
817
  .description("Remove a member")
599
818
  .action(async (userId) => {
600
- const token = resolveToken() ?? missingAuth();
601
819
  const spaceId = requireSpace(spacesCmd);
602
- const client = createClient(token);
820
+ const client = createClient();
603
821
  try {
604
822
  await client.space(spaceId).members.remove(userId);
605
823
  ok(`${userId} removed`);
@@ -620,9 +838,8 @@ function registerAccess(spacesCmd) {
620
838
  .description("Get access policy")
621
839
  .option("--json", "Output as JSON")
622
840
  .action(async (opts) => {
623
- const token = resolveToken() ?? missingAuth();
624
841
  const spaceId = requireSpace(spacesCmd);
625
- const client = createClient(token);
842
+ const client = createClient();
626
843
  try {
627
844
  const policy = await client.space(spaceId).access.get();
628
845
  if (opts.json)
@@ -643,9 +860,8 @@ function registerAccess(spacesCmd) {
643
860
  .option("--anonymous <role>", "Role for anonymous users (host|builder|guest|null)")
644
861
  .option("--json", "Output as JSON")
645
862
  .action(async (opts) => {
646
- const token = resolveToken() ?? missingAuth();
647
863
  const spaceId = requireSpace(spacesCmd);
648
- const client = createClient(token);
864
+ const client = createClient();
649
865
  try {
650
866
  const policy = await client.space(spaceId).access.set({
651
867
  signed_in_user: (opts.signedIn ?? null),
@@ -676,9 +892,8 @@ function registerCheckpoints(spacesCmd) {
676
892
  .description("List checkpoints")
677
893
  .option("--json", "Output as JSON")
678
894
  .action(async (opts) => {
679
- const token = resolveToken() ?? missingAuth();
680
895
  const spaceId = requireSpace(spacesCmd);
681
- const client = createClient(token);
896
+ const client = createClient();
682
897
  try {
683
898
  const result = await client.space(spaceId).checkpoints.list();
684
899
  if (opts.json)
@@ -703,9 +918,8 @@ function registerCheckpoints(spacesCmd) {
703
918
  .description("Checkpoint details")
704
919
  .option("--json", "Output as JSON")
705
920
  .action(async (id, opts) => {
706
- const token = resolveToken() ?? missingAuth();
707
921
  const spaceId = requireSpace(spacesCmd);
708
- const client = createClient(token);
922
+ const client = createClient();
709
923
  try {
710
924
  const result = await client.space(spaceId).checkpoints.get(id);
711
925
  if (opts.json)
@@ -727,9 +941,8 @@ function registerCheckpoints(spacesCmd) {
727
941
  .description("Create a checkpoint")
728
942
  .option("--json", "Output as JSON")
729
943
  .action(async (description, opts) => {
730
- const token = resolveToken() ?? missingAuth();
731
944
  const spaceId = requireSpace(spacesCmd);
732
- const client = createClient(token);
945
+ const client = createClient();
733
946
  try {
734
947
  const result = await client.space(spaceId).checkpoints.create(description ?? null);
735
948
  if (opts.json)
@@ -741,6 +954,3 @@ function registerCheckpoints(spacesCmd) {
741
954
  }
742
955
  });
743
956
  }
744
- function missingAuth() {
745
- return error("Not authenticated", "Run 'cohub auth login <token>'");
746
- }