@gobi-ai/cli 2.0.4 → 2.0.6

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,5 +1,5 @@
1
1
  import { apiGet, apiPost, apiPatch, apiDelete } from "../client.js";
2
- import { selectSpace, writeSpaceSetting } from "./init.js";
2
+ import { requireSpace, selectSpace, setSpaceRequirement, writeSpaceSetting, } from "./init.js";
3
3
  import { isJsonMode, jsonOut, readStdin, resolveSpaceSlug, resolveVaultSlug, unwrapResp, } from "./utils.js";
4
4
  import { extractWikiLinks, uploadAttachments } from "../attachments.js";
5
5
  import { getValidToken } from "../auth/manager.js";
@@ -29,9 +29,12 @@ export function registerSpaceCommand(program) {
29
29
  const space = program
30
30
  .command("space")
31
31
  .description("Space commands (posts, replies). Space and member admin is web-UI only.")
32
- .option("--space-slug <slug>", "Space slug (overrides .gobi/settings.yaml)");
32
+ .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)");
33
+ // Default: every space subcommand needs a configured space (or --space-slug).
34
+ // `list`, `warp`, and `get` opt out below.
35
+ requireSpace(space);
33
36
  // ── List spaces ──
34
- space
37
+ const listCmd = space
35
38
  .command("list")
36
39
  .description("List spaces you are a member of.")
37
40
  .action(async () => {
@@ -47,17 +50,22 @@ export function registerSpaceCommand(program) {
47
50
  }
48
51
  const lines = [];
49
52
  for (const s of items) {
50
- const desc = s.description ? ` - ${s.description}` : "";
51
- lines.push(`- [${s.slug}] ${s.name}${desc}`);
53
+ lines.push(`- [${s.slug}] ${s.name}`);
54
+ if (s.description)
55
+ lines.push(` Description: ${s.description}`);
56
+ if (s.rules)
57
+ lines.push(` Rules: ${s.rules}`);
52
58
  }
53
59
  console.log(`Spaces (${items.length}):\n` + lines.join("\n"));
54
60
  });
61
+ setSpaceRequirement(listCmd, false);
55
62
  // ── Get space ──
56
- space
63
+ const getCmd = space
57
64
  .command("get [spaceSlug]")
58
65
  .description("Get details for a space. Pass a slug or omit to use the current space (from .gobi/settings.yaml or --space-slug).")
59
- .action(async (spaceSlug) => {
60
- const slug = spaceSlug || resolveSpaceSlug(space);
66
+ .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
67
+ .action(async (spaceSlug, opts) => {
68
+ const slug = spaceSlug || resolveSpaceSlug(space, opts);
61
69
  const resp = (await apiGet(`/spaces/${slug}`));
62
70
  const s = unwrapResp(resp);
63
71
  if (isJsonMode(space)) {
@@ -69,15 +77,18 @@ export function registerSpaceCommand(program) {
69
77
  ` ID: ${s.id}\n` +
70
78
  ` Created: ${s.createdAt}`);
71
79
  });
80
+ // `get [spaceSlug]` accepts a positional arg; treat as not requiring space
81
+ // config so the no-arg case falls through to the action's own error.
82
+ setSpaceRequirement(getCmd, false);
72
83
  // ── Warp (space selection) ──
73
- space
84
+ const warpCmd = space
74
85
  .command("warp [spaceSlug]")
75
86
  .description("Select the active space. Pass a slug to warp directly, or omit for interactive selection.")
76
87
  .action(async (spaceSlug) => {
77
88
  if (spaceSlug) {
78
89
  writeSpaceSetting(spaceSlug);
79
90
  if (isJsonMode(space)) {
80
- jsonOut({ spaceSlug });
91
+ jsonOut({ spaceSlug, spaceName: null });
81
92
  return;
82
93
  }
83
94
  console.log(`Warped to space "${spaceSlug}"`);
@@ -95,13 +106,15 @@ export function registerSpaceCommand(program) {
95
106
  }
96
107
  console.log(`Warped to space "${result.name}" (${result.slug})`);
97
108
  });
109
+ setSpaceRequirement(warpCmd, false);
98
110
  // ── Topics ──
99
111
  space
100
112
  .command("list-topics")
101
113
  .description("List topics in a space, ordered by most recent content linkage.")
102
- .option("--limit <number>", "Max topics to return (0 = all)", "50")
114
+ .option("--limit <number>", "Items per page", "20")
115
+ .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
103
116
  .action(async (opts) => {
104
- const spaceSlug = resolveSpaceSlug(space);
117
+ const spaceSlug = resolveSpaceSlug(space, opts);
105
118
  const params = {
106
119
  limit: parseInt(opts.limit, 10),
107
120
  };
@@ -127,8 +140,9 @@ export function registerSpaceCommand(program) {
127
140
  .description("List posts tagged with a topic in a space (cursor-paginated).")
128
141
  .option("--limit <number>", "Items per page", "20")
129
142
  .option("--cursor <string>", "Pagination cursor from previous response")
143
+ .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
130
144
  .action(async (topicSlug, opts) => {
131
- const spaceSlug = resolveSpaceSlug(space);
145
+ const spaceSlug = resolveSpaceSlug(space, opts);
132
146
  const params = {
133
147
  limit: parseInt(opts.limit, 10),
134
148
  };
@@ -165,8 +179,9 @@ export function registerSpaceCommand(program) {
165
179
  .description("List the unified feed (posts and replies, newest first) in a space.")
166
180
  .option("--limit <number>", "Items per page", "20")
167
181
  .option("--cursor <string>", "Pagination cursor from previous response")
182
+ .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
168
183
  .action(async (opts) => {
169
- const spaceSlug = resolveSpaceSlug(space);
184
+ const spaceSlug = resolveSpaceSlug(space, opts);
170
185
  const params = {
171
186
  limit: parseInt(opts.limit, 10),
172
187
  };
@@ -194,10 +209,12 @@ export function registerSpaceCommand(program) {
194
209
  space
195
210
  .command("get-post <postId>")
196
211
  .description("Get a post with its ancestors and replies (paginated).")
197
- .option("--limit <number>", "Replies per page", "20")
212
+ .option("--limit <number>", "Items per page", "20")
198
213
  .option("--cursor <string>", "Pagination cursor from previous response")
214
+ .option("--full", "Show full reply content without truncation")
215
+ .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
199
216
  .action(async (postId, opts) => {
200
- const spaceSlug = resolveSpaceSlug(space);
217
+ const spaceSlug = resolveSpaceSlug(space, opts);
201
218
  const params = {
202
219
  limit: parseInt(opts.limit, 10),
203
220
  };
@@ -231,8 +248,10 @@ export function registerSpaceCommand(program) {
231
248
  const rAuthor = r.author?.name ||
232
249
  `User ${r.authorId}`;
233
250
  const text = r.content;
234
- const truncated = text && text.length > 200 ? text.slice(0, 200) + "…" : text;
235
- replyLines.push(` - ${rAuthor}: ${truncated} (${r.createdAt})`);
251
+ const body = opts.full || !text || text.length <= 200
252
+ ? text
253
+ : text.slice(0, 200) + "…";
254
+ replyLines.push(` - ${rAuthor}: ${body} (${r.createdAt})`);
236
255
  }
237
256
  const isReplyPost = post.parentPostId != null;
238
257
  const heading = isReplyPost
@@ -258,8 +277,9 @@ export function registerSpaceCommand(program) {
258
277
  .description("List posts in a space (paginated).")
259
278
  .option("--limit <number>", "Items per page", "20")
260
279
  .option("--cursor <string>", "Pagination cursor from previous response")
280
+ .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
261
281
  .action(async (opts) => {
262
- const spaceSlug = resolveSpaceSlug(space);
282
+ const spaceSlug = resolveSpaceSlug(space, opts);
263
283
  const params = {
264
284
  limit: parseInt(opts.limit, 10),
265
285
  };
@@ -292,28 +312,48 @@ export function registerSpaceCommand(program) {
292
312
  space
293
313
  .command("create-post")
294
314
  .description("Create a post in a space.")
295
- .requiredOption("--title <title>", "Title of the post")
296
- .requiredOption("--content <content>", "Post content (markdown supported)")
315
+ .option("--title <title>", "Title of the post")
316
+ .option("--content <content>", "Post content (markdown supported, use \"-\" for stdin)")
317
+ .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
297
318
  .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before posting (also attributes the post to that vault)")
298
319
  .option("--vault-slug <vaultSlug>", "Attribute the post to this vault (sets authorVaultId). Also used as upload destination for --auto-attachments.")
320
+ .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
299
321
  .action(async (opts) => {
300
- const content = readContent(opts.content);
322
+ if (!opts.content && !opts.richText) {
323
+ throw new Error("Provide either --content or --rich-text.");
324
+ }
325
+ if (opts.content && opts.richText) {
326
+ throw new Error("--content and --rich-text are mutually exclusive.");
327
+ }
301
328
  let authorVaultSlug;
302
329
  if (opts.vaultSlug || opts.autoAttachments) {
303
330
  authorVaultSlug = resolveVaultSlug(opts);
304
331
  }
305
- if (opts.autoAttachments) {
306
- const token = await getValidToken();
307
- const links = extractWikiLinks(content);
308
- await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
332
+ const body = {};
333
+ if (opts.title != null)
334
+ body.title = opts.title;
335
+ if (opts.content != null) {
336
+ const content = readContent(opts.content);
337
+ if (opts.autoAttachments) {
338
+ const token = await getValidToken();
339
+ const links = extractWikiLinks(content);
340
+ await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
341
+ }
342
+ body.content = content;
343
+ }
344
+ if (opts.richText != null) {
345
+ let parsed;
346
+ try {
347
+ parsed = JSON.parse(opts.richText);
348
+ }
349
+ catch {
350
+ throw new Error("Invalid --rich-text JSON.");
351
+ }
352
+ body.richText = parsed;
309
353
  }
310
- const spaceSlug = resolveSpaceSlug(space);
311
- const body = {
312
- title: opts.title,
313
- content,
314
- };
315
354
  if (authorVaultSlug)
316
355
  body.authorVaultSlug = authorVaultSlug;
356
+ const spaceSlug = resolveSpaceSlug(space, opts);
317
357
  const resp = (await apiPost(`/spaces/${spaceSlug}/posts`, body));
318
358
  const post = unwrapResp(resp);
319
359
  if (isJsonMode(space)) {
@@ -322,28 +362,32 @@ export function registerSpaceCommand(program) {
322
362
  }
323
363
  console.log(`Post created!\n` +
324
364
  ` ID: ${post.id}\n` +
325
- ` Title: ${post.title}\n` +
365
+ (post.title ? ` Title: ${post.title}\n` : "") +
326
366
  ` Created: ${post.createdAt}`);
327
367
  });
328
368
  space
329
369
  .command("edit-post <postId>")
330
- .description("Edit a post. You must be the author.")
370
+ .description("Edit a post you authored in a space.")
331
371
  .option("--title <title>", "New title for the post")
332
- .option("--content <content>", "New content for the post (markdown supported)")
372
+ .option("--content <content>", "New content for the post (markdown supported, use \"-\" for stdin)")
373
+ .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
333
374
  .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before editing (also attributes the post to that vault)")
334
- .option("--vault-slug <vaultSlug>", "Attribute the post to this vault (sets authorVaultId). Also used as upload destination for --auto-attachments. Pass an empty string to detach.")
375
+ .option("--vault-slug <vaultSlug>", "Attribute the post to this vault (sets authorVaultId). Also used as upload destination for --auto-attachments.")
376
+ .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
335
377
  .action(async (postId, opts) => {
336
- const wantsVaultChange = opts.vaultSlug !== undefined || opts.autoAttachments;
337
- if (!opts.title && !opts.content && !wantsVaultChange) {
338
- throw new Error("Provide at least --title, --content, or --vault-slug to update.");
378
+ const wantsVaultChange = !!(opts.vaultSlug || opts.autoAttachments);
379
+ if (opts.title == null &&
380
+ opts.content == null &&
381
+ opts.richText == null &&
382
+ !wantsVaultChange) {
383
+ throw new Error("Provide at least --title, --content, --rich-text, or --vault-slug to update.");
339
384
  }
340
- const spaceSlug = resolveSpaceSlug(space);
341
- let authorVaultSlug;
342
- if (opts.vaultSlug !== undefined) {
343
- // Empty string detaches; non-empty resolves through settings fallback.
344
- authorVaultSlug = opts.vaultSlug === "" ? "" : resolveVaultSlug(opts);
385
+ if (opts.content && opts.richText) {
386
+ throw new Error("--content and --rich-text are mutually exclusive.");
345
387
  }
346
- else if (opts.autoAttachments) {
388
+ const spaceSlug = resolveSpaceSlug(space, opts);
389
+ let authorVaultSlug;
390
+ if (opts.vaultSlug || opts.autoAttachments) {
347
391
  authorVaultSlug = resolveVaultSlug(opts);
348
392
  }
349
393
  const body = {};
@@ -358,6 +402,16 @@ export function registerSpaceCommand(program) {
358
402
  }
359
403
  body.content = content;
360
404
  }
405
+ if (opts.richText != null) {
406
+ let parsed;
407
+ try {
408
+ parsed = JSON.parse(opts.richText);
409
+ }
410
+ catch {
411
+ throw new Error("Invalid --rich-text JSON.");
412
+ }
413
+ body.richText = parsed;
414
+ }
361
415
  if (authorVaultSlug !== undefined)
362
416
  body.authorVaultSlug = authorVaultSlug;
363
417
  const resp = (await apiPatch(`/spaces/${spaceSlug}/posts/${postId}`, body));
@@ -368,14 +422,15 @@ export function registerSpaceCommand(program) {
368
422
  }
369
423
  console.log(`Post edited!\n` +
370
424
  ` ID: ${post.id}\n` +
371
- ` Title: ${post.title}\n` +
425
+ (post.title ? ` Title: ${post.title}\n` : "") +
372
426
  ` Edited: ${post.editedAt}`);
373
427
  });
374
428
  space
375
429
  .command("delete-post <postId>")
376
- .description("Delete a post. You must be the author.")
377
- .action(async (postId) => {
378
- const spaceSlug = resolveSpaceSlug(space);
430
+ .description("Delete a post you authored in a space.")
431
+ .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
432
+ .action(async (postId, opts) => {
433
+ const spaceSlug = resolveSpaceSlug(space, opts);
379
434
  await apiDelete(`/spaces/${spaceSlug}/posts/${postId}`);
380
435
  if (isJsonMode(space)) {
381
436
  jsonOut({ id: postId });
@@ -387,10 +442,46 @@ export function registerSpaceCommand(program) {
387
442
  space
388
443
  .command("create-reply <postId>")
389
444
  .description("Create a reply to a post in a space.")
390
- .requiredOption("--content <content>", "Reply content (markdown supported)")
445
+ .option("--content <content>", "Reply content (markdown supported, use \"-\" for stdin)")
446
+ .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
447
+ .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before posting (also attributes the reply to that vault)")
448
+ .option("--vault-slug <vaultSlug>", "Attribute the reply to this vault (sets authorVaultSlug). Also used as upload destination for --auto-attachments.")
449
+ .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
391
450
  .action(async (postId, opts) => {
392
- const spaceSlug = resolveSpaceSlug(space);
393
- const resp = (await apiPost(`/spaces/${spaceSlug}/posts/${postId}/replies`, { content: readContent(opts.content) }));
451
+ if (!opts.content && !opts.richText) {
452
+ throw new Error("Provide either --content or --rich-text.");
453
+ }
454
+ if (opts.content && opts.richText) {
455
+ throw new Error("--content and --rich-text are mutually exclusive.");
456
+ }
457
+ let authorVaultSlug;
458
+ if (opts.vaultSlug || opts.autoAttachments) {
459
+ authorVaultSlug = resolveVaultSlug(opts);
460
+ }
461
+ const body = {};
462
+ if (opts.content != null) {
463
+ const content = readContent(opts.content);
464
+ if (opts.autoAttachments) {
465
+ const token = await getValidToken();
466
+ const links = extractWikiLinks(content);
467
+ await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
468
+ }
469
+ body.content = content;
470
+ }
471
+ if (opts.richText != null) {
472
+ let parsed;
473
+ try {
474
+ parsed = JSON.parse(opts.richText);
475
+ }
476
+ catch {
477
+ throw new Error("Invalid --rich-text JSON.");
478
+ }
479
+ body.richText = parsed;
480
+ }
481
+ if (authorVaultSlug)
482
+ body.authorVaultSlug = authorVaultSlug;
483
+ const spaceSlug = resolveSpaceSlug(space, opts);
484
+ const resp = (await apiPost(`/spaces/${spaceSlug}/posts/${postId}/replies`, body));
394
485
  const msg = unwrapResp(resp);
395
486
  const mentions = (resp.mentions || {});
396
487
  if (isJsonMode(space)) {
@@ -401,20 +492,48 @@ export function registerSpaceCommand(program) {
401
492
  });
402
493
  space
403
494
  .command("edit-reply <replyId>")
404
- .description("Edit a reply. You must be the author.")
405
- .requiredOption("--content <content>", "New content for the reply (markdown supported)")
406
- .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before editing")
407
- .option("--vault-slug <vaultSlug>", "Vault slug for attachment uploads (overrides .gobi/settings.yaml)")
495
+ .description("Edit a reply you authored in a space.")
496
+ .option("--content <content>", "New content for the reply (markdown supported, use \"-\" for stdin)")
497
+ .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
498
+ .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before editing (also attributes the reply to that vault)")
499
+ .option("--vault-slug <vaultSlug>", "Attribute the reply to this vault (sets authorVaultSlug). Also used as upload destination for --auto-attachments.")
500
+ .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
408
501
  .action(async (replyId, opts) => {
409
- const spaceSlug = resolveSpaceSlug(space);
410
- const content = readContent(opts.content);
411
- if (opts.autoAttachments) {
412
- const vaultSlug = resolveVaultSlug(opts);
413
- const token = await getValidToken();
414
- const links = extractWikiLinks(content);
415
- await uploadAttachments(vaultSlug, links, token, { addToSyncfiles: true });
416
- }
417
- const resp = (await apiPatch(`/spaces/${spaceSlug}/replies/${replyId}`, { content }));
502
+ const wantsVaultChange = !!(opts.vaultSlug || opts.autoAttachments);
503
+ if (opts.content == null && opts.richText == null && !wantsVaultChange) {
504
+ throw new Error("Provide at least --content, --rich-text, or --vault-slug to update.");
505
+ }
506
+ if (opts.content && opts.richText) {
507
+ throw new Error("--content and --rich-text are mutually exclusive.");
508
+ }
509
+ const spaceSlug = resolveSpaceSlug(space, opts);
510
+ let authorVaultSlug;
511
+ if (opts.vaultSlug || opts.autoAttachments) {
512
+ authorVaultSlug = resolveVaultSlug(opts);
513
+ }
514
+ const body = {};
515
+ if (opts.content != null) {
516
+ const content = readContent(opts.content);
517
+ if (opts.autoAttachments) {
518
+ const token = await getValidToken();
519
+ const links = extractWikiLinks(content);
520
+ await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
521
+ }
522
+ body.content = content;
523
+ }
524
+ if (opts.richText != null) {
525
+ let parsed;
526
+ try {
527
+ parsed = JSON.parse(opts.richText);
528
+ }
529
+ catch {
530
+ throw new Error("Invalid --rich-text JSON.");
531
+ }
532
+ body.richText = parsed;
533
+ }
534
+ if (authorVaultSlug !== undefined)
535
+ body.authorVaultSlug = authorVaultSlug;
536
+ const resp = (await apiPatch(`/spaces/${spaceSlug}/replies/${replyId}`, body));
418
537
  const msg = unwrapResp(resp);
419
538
  if (isJsonMode(space)) {
420
539
  jsonOut(msg);
@@ -424,12 +543,13 @@ export function registerSpaceCommand(program) {
424
543
  });
425
544
  space
426
545
  .command("delete-reply <replyId>")
427
- .description("Delete a reply. You must be the author.")
428
- .action(async (replyId) => {
429
- const spaceSlug = resolveSpaceSlug(space);
546
+ .description("Delete a reply you authored in a space.")
547
+ .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
548
+ .action(async (replyId, opts) => {
549
+ const spaceSlug = resolveSpaceSlug(space, opts);
430
550
  await apiDelete(`/spaces/${spaceSlug}/replies/${replyId}`);
431
551
  if (isJsonMode(space)) {
432
- jsonOut({ replyId });
552
+ jsonOut({ id: replyId });
433
553
  return;
434
554
  }
435
555
  console.log(`Reply ${replyId} deleted.`);
@@ -11,8 +11,13 @@ export function isJsonMode(cmd) {
11
11
  export function jsonOut(data) {
12
12
  console.log(JSON.stringify({ success: true, data }));
13
13
  }
14
- export function resolveSpaceSlug(cmd) {
15
- return cmd.opts().spaceSlug || getSpaceSlug();
14
+ // Resolves the space slug from (in order): the leaf subcommand's --space-slug
15
+ // option, the parent `gobi space` command's --space-slug option, then
16
+ // `.gobi/settings.yaml`. Either side of the subcommand works:
17
+ // gobi space --space-slug foo list-posts (parent-level)
18
+ // gobi space list-posts --space-slug foo (leaf-level)
19
+ export function resolveSpaceSlug(parent, leafOpts) {
20
+ return leafOpts?.spaceSlug || parent.opts().spaceSlug || getSpaceSlug();
16
21
  }
17
22
  export function resolveVaultSlug(opts) {
18
23
  return opts.vaultSlug || getVaultSlug();
@@ -3,15 +3,48 @@ import { join, resolve as pathResolve } from "path";
3
3
  import { WEBDRIVE_BASE_URL } from "../constants.js";
4
4
  import { getValidToken } from "../auth/manager.js";
5
5
  import { GobiError } from "../errors.js";
6
- import { getVaultSlug } from "./init.js";
6
+ import { apiGet } from "../client.js";
7
+ import { getVaultSlug, requireVault, runVaultInitFlow } from "./init.js";
7
8
  import { isJsonMode, jsonOut } from "./utils.js";
8
9
  import { runSync } from "./sync.js";
9
10
  export const PUBLISH_FILENAME = "PUBLISH.md";
10
11
  export function registerVaultCommand(program) {
11
12
  const vault = program
12
13
  .command("vault")
13
- .description("Vault commands (publish/unpublish profile, sync files).");
14
+ .description("Vault commands (init, list, publish/unpublish profile, sync files).");
14
15
  vault
16
+ .command("init")
17
+ .description("Select or create the vault for the current directory. Writes .gobi/settings.yaml and seeds PUBLISH.md.")
18
+ .action(async () => {
19
+ await runVaultInitFlow();
20
+ });
21
+ vault
22
+ .command("list")
23
+ .description("List vaults you own.")
24
+ .action(async () => {
25
+ const resp = (await apiGet("/vaults"));
26
+ const items = (Array.isArray(resp)
27
+ ? resp
28
+ : Array.isArray(resp?.data)
29
+ ? resp.data
30
+ : []);
31
+ if (isJsonMode(vault)) {
32
+ jsonOut(items);
33
+ return;
34
+ }
35
+ if (!items.length) {
36
+ console.log("No vaults found.");
37
+ return;
38
+ }
39
+ const lines = [];
40
+ for (const v of items) {
41
+ const slug = (v.vaultId || v.slug);
42
+ const isPrimary = v.isPrimary ? " (primary)" : "";
43
+ lines.push(`- [${slug}] ${v.name}${isPrimary}`);
44
+ }
45
+ console.log(`Vaults (${items.length}):\n` + lines.join("\n"));
46
+ });
47
+ const publishCmd = vault
15
48
  .command("publish")
16
49
  .description(`Upload ${PUBLISH_FILENAME} to the vault root on webdrive. Triggers post-processing (vault sync, metadata update, Discord notification).`)
17
50
  .action(async () => {
@@ -40,7 +73,8 @@ export function registerVaultCommand(program) {
40
73
  }
41
74
  console.log(`Published ${PUBLISH_FILENAME} to vault "${vaultId}"`);
42
75
  });
43
- vault
76
+ requireVault(publishCmd);
77
+ const unpublishCmd = vault
44
78
  .command("unpublish")
45
79
  .description(`Delete ${PUBLISH_FILENAME} from the vault on webdrive.`)
46
80
  .action(async () => {
@@ -60,7 +94,8 @@ export function registerVaultCommand(program) {
60
94
  }
61
95
  console.log(`Deleted ${PUBLISH_FILENAME} from vault "${vaultId}"`);
62
96
  });
63
- vault
97
+ requireVault(unpublishCmd);
98
+ const syncCmd = vault
64
99
  .command("sync")
65
100
  .description("Sync local vault files with Gobi Webdrive.")
66
101
  .option("--upload-only", "Only upload local changes to server")
@@ -110,4 +145,5 @@ export function registerVaultCommand(program) {
110
145
  jsonMode: isJsonMode(this),
111
146
  });
112
147
  });
148
+ requireVault(syncCmd);
113
149
  }
package/dist/main.js CHANGED
@@ -3,7 +3,7 @@ import { Command } from "commander";
3
3
  import { initCredentials } from "./auth/manager.js";
4
4
  import { ApiError, GobiError } from "./errors.js";
5
5
  import { registerAuthCommand } from "./commands/auth.js";
6
- import { registerInitCommand, printContext } from "./commands/init.js";
6
+ import { commandRequiresSpace, commandRequiresVault, readSettings, } from "./commands/init.js";
7
7
  import { registerSpaceCommand } from "./commands/space.js";
8
8
  import { registerGlobalCommand } from "./commands/global.js";
9
9
  import { registerVaultCommand } from "./commands/vault.js";
@@ -15,12 +15,38 @@ import { registerMediaCommand } from "./commands/media.js";
15
15
  import { registerDraftCommand } from "./commands/draft.js";
16
16
  const require = createRequire(import.meta.url);
17
17
  const { version } = require("../package.json");
18
- const SKIP_BANNER_COMMANDS = new Set(["auth", "init", "update"]);
19
- function shouldShowBanner() {
20
- const args = process.argv.slice(2);
21
- if (args.length === 0)
22
- return true;
23
- return !SKIP_BANNER_COMMANDS.has(args[0]);
18
+ function hasParentOption(cmd, key) {
19
+ let cur = cmd;
20
+ while (cur) {
21
+ const v = cur.opts()[key];
22
+ if (v !== undefined && v !== "" && v !== false)
23
+ return true;
24
+ cur = cur.parent;
25
+ }
26
+ return false;
27
+ }
28
+ function maybePrintRequirementWarnings(actionCommand) {
29
+ const needsVault = commandRequiresVault(actionCommand);
30
+ const needsSpace = commandRequiresSpace(actionCommand);
31
+ if (!needsVault && !needsSpace)
32
+ return;
33
+ const settings = readSettings();
34
+ const hasVault = !!settings?.vaultSlug;
35
+ const hasSpace = !!settings?.selectedSpaceSlug;
36
+ const vaultOverride = hasParentOption(actionCommand, "vaultSlug");
37
+ const spaceOverride = hasParentOption(actionCommand, "spaceSlug");
38
+ const warnings = [];
39
+ if (needsVault && !hasVault && !vaultOverride) {
40
+ warnings.push("Vault not set. Run 'gobi vault init' first, or pass --vault-slug.");
41
+ }
42
+ if (needsSpace && !hasSpace && !spaceOverride) {
43
+ warnings.push("Space not set. Run 'gobi space warp' first, or pass --space-slug.");
44
+ }
45
+ if (warnings.length) {
46
+ for (const w of warnings)
47
+ console.log(w);
48
+ console.log("");
49
+ }
24
50
  }
25
51
  export async function cli() {
26
52
  const program = new Command();
@@ -32,7 +58,6 @@ export async function cli() {
32
58
  .configureHelp({ helpWidth: process.stdout.columns || 200 });
33
59
  // Register all command groups
34
60
  registerAuthCommand(program);
35
- registerInitCommand(program);
36
61
  registerSpaceCommand(program);
37
62
  registerGlobalCommand(program);
38
63
  registerVaultCommand(program);
@@ -50,12 +75,11 @@ export async function cli() {
50
75
  sub.configureHelp({ helpWidth });
51
76
  }
52
77
  }
53
- // Hook into the pre-action to init credentials and show banner
54
- program.hook("preAction", async () => {
78
+ // Hook into the pre-action to init credentials and show requirement warnings
79
+ program.hook("preAction", async (_thisCommand, actionCommand) => {
55
80
  await initCredentials();
56
- if (!program.opts().json && shouldShowBanner()) {
57
- printContext();
58
- console.log("");
81
+ if (!program.opts().json) {
82
+ maybePrintRequirementWarnings(actionCommand);
59
83
  }
60
84
  });
61
85
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobi-ai/cli",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "description": "CLI client for the Gobi collaborative knowledge platform",
5
5
  "license": "MIT",
6
6
  "type": "module",