@gobi-ai/cli 2.0.24 → 2.0.25

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,7 +1,6 @@
1
1
  import { apiGet, apiPost, apiPatch, apiDelete } from "../client.js";
2
- import { fetchDraftSummary, isJsonMode, jsonOut, readStdin, resolveVaultSlug, unwrapResp, } from "./utils.js";
3
- import { extractWikiLinks, uploadAttachments, uploadPostAttachments, assertPostAttachmentMix, } from "../attachments.js";
4
- import { getValidToken } from "../auth/manager.js";
2
+ import { isJsonMode, jsonOut, readStdin, resolveVaultSlug, unwrapResp, } from "./utils.js";
3
+ import { uploadPostAttachments, assertPostAttachmentMix, } from "../attachments.js";
5
4
  function readContent(value) {
6
5
  if (value === "-")
7
6
  return readStdin();
@@ -191,43 +190,24 @@ export function registerPersonalCommand(program) {
191
190
  .option("--content <content>", "Post content (markdown supported, use \"-\" for stdin)")
192
191
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
193
192
  .option("--vault-slug <vaultSlug>", "Attribute the post to this vault (sets authorVaultSlug). Caller must own the vault.")
194
- .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before posting (also sets authorVaultSlug to that vault)")
195
- .option("--draft-id <draftId>", "Use this draft as the source of title and content (mutually exclusive with --title/--content/--rich-text). On success, links the post back via draft.metadata. The draft's vaultSlug seeds --vault-slug when not given explicitly.")
196
193
  .option("--attach <file>", "Local media file to attach. Repeatable. X-style mix rule: up to 4 photos OR 1 GIF OR 1 video. Size ceilings: 5MB photos / 15MB GIFs / 512MB video.", (value, prev = []) => [...prev, value], [])
197
194
  .option("--repost-post-id <postId>", "Wrap an existing top-level post as the embedded card on this new private post. The referenced post must be visible to you (your own personal-space post, a global-feed post, or a post in a space you're a member of). Reposting someone else's personal-space post returns 404.")
198
195
  .action(async (opts) => {
199
- if (opts.draftId) {
200
- if (opts.title || opts.content || opts.richText) {
201
- throw new Error("--draft-id sources title and content from the draft; --title, --content, and --rich-text are not allowed alongside it.");
202
- }
196
+ if (!opts.content && !opts.richText) {
197
+ throw new Error("Provide either --content or --rich-text.");
203
198
  }
204
- else {
205
- if (!opts.content && !opts.richText) {
206
- throw new Error("Provide either --content or --rich-text (or --draft-id).");
207
- }
208
- if (opts.content && opts.richText) {
209
- throw new Error("--content and --rich-text are mutually exclusive.");
210
- }
199
+ if (opts.content && opts.richText) {
200
+ throw new Error("--content and --rich-text are mutually exclusive.");
211
201
  }
212
- const draft = opts.draftId ? await fetchDraftSummary(opts.draftId) : null;
213
- const effectiveTitle = draft ? draft.title : opts.title;
214
- const effectiveContent = draft ? draft.content : opts.content;
215
- const effectiveVaultSlugOpt = opts.vaultSlug ?? draft?.vaultSlug ?? undefined;
216
202
  let authorVaultSlug;
217
- if (effectiveVaultSlugOpt || opts.autoAttachments) {
218
- authorVaultSlug = resolveVaultSlug({ vaultSlug: effectiveVaultSlugOpt });
203
+ if (opts.vaultSlug) {
204
+ authorVaultSlug = resolveVaultSlug({ vaultSlug: opts.vaultSlug });
219
205
  }
220
206
  const body = {};
221
- if (effectiveTitle != null)
222
- body.title = effectiveTitle;
223
- if (effectiveContent != null) {
224
- const content = draft ? effectiveContent : readContent(effectiveContent);
225
- if (opts.autoAttachments && authorVaultSlug) {
226
- const token = await getValidToken();
227
- const links = extractWikiLinks(content);
228
- await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
229
- }
230
- body.content = content;
207
+ if (opts.title != null)
208
+ body.title = opts.title;
209
+ if (opts.content != null) {
210
+ body.content = readContent(opts.content);
231
211
  }
232
212
  if (opts.richText != null) {
233
213
  let parsed;
@@ -254,17 +234,6 @@ export function registerPersonalCommand(program) {
254
234
  }
255
235
  const resp = (await apiPost(`/posts/personal-space`, body));
256
236
  const post = unwrapResp(resp);
257
- if (opts.draftId && post.id != null) {
258
- try {
259
- await apiPatch(`/app/drafts/${opts.draftId}/metadata`, {
260
- postId: typeof post.id === "number" ? post.id : Number(post.id),
261
- spaceSlug: null,
262
- });
263
- }
264
- catch (e) {
265
- console.error(`Warning: failed to link post to draft ${opts.draftId}: ${e.message}`);
266
- }
267
- }
268
237
  if (isJsonMode(personal)) {
269
238
  jsonOut(post);
270
239
  return;
@@ -273,8 +242,7 @@ export function registerPersonalCommand(program) {
273
242
  ` ID: ${post.id}\n` +
274
243
  (post.title ? ` Title: ${post.title}\n` : "") +
275
244
  ` Created: ${post.createdAt}\n` +
276
- ` Visibility: private (only you can see this)` +
277
- (opts.draftId ? `\n Linked to draft: ${opts.draftId}` : ""));
245
+ ` Visibility: private (only you can see this)`);
278
246
  });
279
247
  // ── Edit post ──
280
248
  //
@@ -288,10 +256,9 @@ export function registerPersonalCommand(program) {
288
256
  .option("--content <content>", "New content (markdown supported, use \"-\" for stdin)")
289
257
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
290
258
  .option("--vault-slug <vaultSlug>", "Attribute the post to this vault (sets authorVaultSlug).")
291
- .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before editing (uses --vault-slug or .gobi vault)")
292
259
  .option("--attach <file>", "Replace the post's media attachments with the given files (existing attachments are removed). Repeatable. X-style mix rule: up to 4 photos OR 1 GIF OR 1 video. Size ceilings: 5MB photos / 15MB GIFs / 512MB video. Omit to leave attachments unchanged.", (value, prev = []) => [...prev, value], [])
293
260
  .action(async (postId, opts) => {
294
- const wantsVaultChange = !!(opts.vaultSlug || opts.autoAttachments);
261
+ const wantsVaultChange = !!opts.vaultSlug;
295
262
  const wantsAttachChange = !!(opts.attach && opts.attach.length > 0);
296
263
  if (opts.title == null &&
297
264
  opts.content == null &&
@@ -304,20 +271,14 @@ export function registerPersonalCommand(program) {
304
271
  throw new Error("--content and --rich-text are mutually exclusive.");
305
272
  }
306
273
  let authorVaultSlug;
307
- if (opts.vaultSlug || opts.autoAttachments) {
274
+ if (opts.vaultSlug) {
308
275
  authorVaultSlug = resolveVaultSlug(opts);
309
276
  }
310
277
  const body = {};
311
278
  if (opts.title != null)
312
279
  body.title = opts.title;
313
280
  if (opts.content != null) {
314
- const content = readContent(opts.content);
315
- if (opts.autoAttachments && authorVaultSlug) {
316
- const token = await getValidToken();
317
- const links = extractWikiLinks(content);
318
- await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
319
- }
320
- body.content = content;
281
+ body.content = readContent(opts.content);
321
282
  }
322
283
  if (opts.richText != null) {
323
284
  let parsed;
@@ -366,8 +327,7 @@ export function registerPersonalCommand(program) {
366
327
  .description("Reply to a personal-space post. The reply inherits the parent's private scope automatically.")
367
328
  .option("--content <content>", "Reply content (markdown supported, use \"-\" for stdin)")
368
329
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
369
- .option("--vault-slug <vaultSlug>", "Attribute the reply to this vault (sets authorVaultSlug). Also used as upload destination for --auto-attachments.")
370
- .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before posting (also attributes the reply to that vault)")
330
+ .option("--vault-slug <vaultSlug>", "Attribute the reply to this vault (sets authorVaultSlug).")
371
331
  .option("--attach <file>", "Local media file to attach to this reply. Repeatable. X-style mix rule: up to 4 photos OR 1 GIF OR 1 video. Size ceilings: 5MB photos / 15MB GIFs / 512MB video.", (value, prev = []) => [...prev, value], [])
372
332
  .action(async (postId, opts) => {
373
333
  if (!opts.content && !opts.richText) {
@@ -377,18 +337,12 @@ export function registerPersonalCommand(program) {
377
337
  throw new Error("--content and --rich-text are mutually exclusive.");
378
338
  }
379
339
  let authorVaultSlug;
380
- if (opts.vaultSlug || opts.autoAttachments) {
340
+ if (opts.vaultSlug) {
381
341
  authorVaultSlug = resolveVaultSlug(opts);
382
342
  }
383
343
  const body = {};
384
344
  if (opts.content != null) {
385
- const content = readContent(opts.content);
386
- if (opts.autoAttachments && authorVaultSlug) {
387
- const token = await getValidToken();
388
- const links = extractWikiLinks(content);
389
- await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
390
- }
391
- body.content = content;
345
+ body.content = readContent(opts.content);
392
346
  }
393
347
  if (opts.richText != null) {
394
348
  let parsed;
@@ -419,10 +373,9 @@ export function registerPersonalCommand(program) {
419
373
  .description("Edit a reply you authored in your personal space.")
420
374
  .option("--content <content>", "New reply content (markdown supported, use \"-\" for stdin)")
421
375
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
422
- .option("--vault-slug <vaultSlug>", "Attribute the reply to this vault (sets authorVaultSlug). Also used as upload destination for --auto-attachments.")
423
- .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before editing (also attributes the reply to that vault)")
376
+ .option("--vault-slug <vaultSlug>", "Attribute the reply to this vault (sets authorVaultSlug).")
424
377
  .action(async (replyId, opts) => {
425
- const wantsVaultChange = !!(opts.vaultSlug || opts.autoAttachments);
378
+ const wantsVaultChange = !!opts.vaultSlug;
426
379
  if (opts.content == null && opts.richText == null && !wantsVaultChange) {
427
380
  throw new Error("Provide at least --content, --rich-text, or --vault-slug to update.");
428
381
  }
@@ -430,18 +383,12 @@ export function registerPersonalCommand(program) {
430
383
  throw new Error("--content and --rich-text are mutually exclusive.");
431
384
  }
432
385
  let authorVaultSlug;
433
- if (opts.vaultSlug || opts.autoAttachments) {
386
+ if (opts.vaultSlug) {
434
387
  authorVaultSlug = resolveVaultSlug(opts);
435
388
  }
436
389
  const body = {};
437
390
  if (opts.content != null) {
438
- const content = readContent(opts.content);
439
- if (opts.autoAttachments && authorVaultSlug) {
440
- const token = await getValidToken();
441
- const links = extractWikiLinks(content);
442
- await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
443
- }
444
- body.content = content;
391
+ body.content = readContent(opts.content);
445
392
  }
446
393
  if (opts.richText != null) {
447
394
  let parsed;
@@ -1,9 +1,8 @@
1
1
  import { WEB_BASE_URL } from "../constants.js";
2
2
  import { apiGet, apiPost, apiPatch, apiDelete } from "../client.js";
3
3
  import { requireSpace, selectSpace, setSpaceRequirement, writeSpaceSetting, } from "./init.js";
4
- import { fetchDraftSummary, isJsonMode, jsonOut, readStdin, resolveSpaceSlug, resolveVaultSlug, unwrapResp, } from "./utils.js";
5
- import { extractWikiLinks, uploadAttachments, uploadPostAttachments, assertPostAttachmentMix, } from "../attachments.js";
6
- import { getValidToken } from "../auth/manager.js";
4
+ import { isJsonMode, jsonOut, readStdin, resolveSpaceSlug, resolveVaultSlug, unwrapResp, } from "./utils.js";
5
+ import { uploadPostAttachments, assertPostAttachmentMix, } from "../attachments.js";
7
6
  function readContent(value) {
8
7
  if (value === "-")
9
8
  return readStdin();
@@ -316,45 +315,26 @@ export function registerSpaceCommand(program) {
316
315
  .option("--title <title>", "Title of the post")
317
316
  .option("--content <content>", "Post content (markdown supported, use \"-\" for stdin)")
318
317
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
319
- .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before posting (also attributes the post to that vault)")
320
- .option("--vault-slug <vaultSlug>", "Attribute the post to this vault (sets authorVaultSlug). Also used as upload destination for --auto-attachments.")
318
+ .option("--vault-slug <vaultSlug>", "Attribute the post to this vault (sets authorVaultSlug). Caller must own the vault.")
321
319
  .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
322
- .option("--draft-id <draftId>", "Use this draft as the source of title and content (mutually exclusive with --title/--content/--rich-text). On success, links the post back by recording postId/spaceSlug on draft.metadata so the client can render an 'Open post' button. The draft's vaultSlug seeds --vault-slug when not given explicitly.")
323
320
  .option("--attach <file>", "Local media file to attach. Repeatable. X-style mix rule: up to 4 photos OR 1 GIF OR 1 video. Size ceilings: 5MB photos / 15MB GIFs / 512MB video.", (value, prev = []) => [...prev, value], [])
324
321
  .option("--repost-post-id <postId>", "Wrap an existing top-level post as the embedded card on this new post. Composes with --content / --rich-text / --attach (the wrapping author's text + media render above the embedded card). Reposts-of-reposts are collapsed to the transitive root server-side. The referenced post must exist, not be deleted, and not itself be a reply.")
325
322
  .action(async (opts) => {
326
- if (opts.draftId) {
327
- if (opts.title || opts.content || opts.richText) {
328
- throw new Error("--draft-id sources title and content from the draft; --title, --content, and --rich-text are not allowed alongside it.");
329
- }
323
+ if (!opts.content && !opts.richText) {
324
+ throw new Error("Provide either --content or --rich-text.");
330
325
  }
331
- else {
332
- if (!opts.content && !opts.richText) {
333
- throw new Error("Provide either --content or --rich-text (or --draft-id).");
334
- }
335
- if (opts.content && opts.richText) {
336
- throw new Error("--content and --rich-text are mutually exclusive.");
337
- }
326
+ if (opts.content && opts.richText) {
327
+ throw new Error("--content and --rich-text are mutually exclusive.");
338
328
  }
339
- const draft = opts.draftId ? await fetchDraftSummary(opts.draftId) : null;
340
- const effectiveTitle = draft ? draft.title : opts.title;
341
- const effectiveContent = draft ? draft.content : opts.content;
342
- const effectiveVaultSlugOpt = opts.vaultSlug ?? draft?.vaultSlug ?? undefined;
343
329
  let authorVaultSlug;
344
- if (effectiveVaultSlugOpt || opts.autoAttachments) {
345
- authorVaultSlug = resolveVaultSlug({ vaultSlug: effectiveVaultSlugOpt });
330
+ if (opts.vaultSlug) {
331
+ authorVaultSlug = resolveVaultSlug({ vaultSlug: opts.vaultSlug });
346
332
  }
347
333
  const body = {};
348
- if (effectiveTitle != null)
349
- body.title = effectiveTitle;
350
- if (effectiveContent != null) {
351
- const content = draft ? effectiveContent : readContent(effectiveContent);
352
- if (opts.autoAttachments) {
353
- const token = await getValidToken();
354
- const links = extractWikiLinks(content);
355
- await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
356
- }
357
- body.content = content;
334
+ if (opts.title != null)
335
+ body.title = opts.title;
336
+ if (opts.content != null) {
337
+ body.content = readContent(opts.content);
358
338
  }
359
339
  if (opts.richText != null) {
360
340
  let parsed;
@@ -382,19 +362,6 @@ export function registerSpaceCommand(program) {
382
362
  const spaceSlug = resolveSpaceSlug(space, opts);
383
363
  const resp = (await apiPost(`/spaces/${spaceSlug}/posts`, body));
384
364
  const post = unwrapResp(resp);
385
- if (opts.draftId && post.id != null) {
386
- try {
387
- await apiPatch(`/app/drafts/${opts.draftId}/metadata`, {
388
- postId: typeof post.id === "number" ? post.id : Number(post.id),
389
- spaceSlug,
390
- });
391
- }
392
- catch (e) {
393
- // Don't fail the create if linking fails — the post is live; just
394
- // surface a warning so the agent can mention it.
395
- console.error(`Warning: failed to link post to draft ${opts.draftId}: ${e.message}`);
396
- }
397
- }
398
365
  const shareUrl = `${WEB_BASE_URL}/spaces/${spaceSlug}/posts/${post.id}`;
399
366
  if (isJsonMode(space)) {
400
367
  jsonOut({ ...post, shareUrl });
@@ -404,8 +371,7 @@ export function registerSpaceCommand(program) {
404
371
  ` ID: ${post.id}\n` +
405
372
  (post.title ? ` Title: ${post.title}\n` : "") +
406
373
  ` Created: ${post.createdAt}\n` +
407
- ` URL: ${shareUrl}` +
408
- (opts.draftId ? `\n Linked to draft: ${opts.draftId}` : ""));
374
+ ` URL: ${shareUrl}`);
409
375
  });
410
376
  space
411
377
  .command("edit-post <postId>")
@@ -413,12 +379,11 @@ export function registerSpaceCommand(program) {
413
379
  .option("--title <title>", "New title for the post")
414
380
  .option("--content <content>", "New content for the post (markdown supported, use \"-\" for stdin)")
415
381
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
416
- .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before editing (also attributes the post to that vault)")
417
- .option("--vault-slug <vaultSlug>", "Attribute the post to this vault (sets authorVaultSlug). Also used as upload destination for --auto-attachments.")
382
+ .option("--vault-slug <vaultSlug>", "Attribute the post to this vault (sets authorVaultSlug). Caller must own the vault.")
418
383
  .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
419
384
  .option("--attach <file>", "Replace the post's media attachments with the given files (existing attachments are removed). Repeatable. X-style mix rule: up to 4 photos OR 1 GIF OR 1 video. Size ceilings: 5MB photos / 15MB GIFs / 512MB video. Omit to leave attachments unchanged.", (value, prev = []) => [...prev, value], [])
420
385
  .action(async (postId, opts) => {
421
- const wantsVaultChange = !!(opts.vaultSlug || opts.autoAttachments);
386
+ const wantsVaultChange = !!opts.vaultSlug;
422
387
  const wantsAttachChange = !!(opts.attach && opts.attach.length > 0);
423
388
  if (opts.title == null &&
424
389
  opts.content == null &&
@@ -432,20 +397,14 @@ export function registerSpaceCommand(program) {
432
397
  }
433
398
  const spaceSlug = resolveSpaceSlug(space, opts);
434
399
  let authorVaultSlug;
435
- if (opts.vaultSlug || opts.autoAttachments) {
400
+ if (opts.vaultSlug) {
436
401
  authorVaultSlug = resolveVaultSlug(opts);
437
402
  }
438
403
  const body = {};
439
404
  if (opts.title != null)
440
405
  body.title = opts.title;
441
406
  if (opts.content != null) {
442
- const content = readContent(opts.content);
443
- if (opts.autoAttachments) {
444
- const token = await getValidToken();
445
- const links = extractWikiLinks(content);
446
- await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
447
- }
448
- body.content = content;
407
+ body.content = readContent(opts.content);
449
408
  }
450
409
  if (opts.richText != null) {
451
410
  let parsed;
@@ -493,8 +452,7 @@ export function registerSpaceCommand(program) {
493
452
  .description("Create a reply to a post in a space.")
494
453
  .option("--content <content>", "Reply content (markdown supported, use \"-\" for stdin)")
495
454
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
496
- .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before posting (also attributes the reply to that vault)")
497
- .option("--vault-slug <vaultSlug>", "Attribute the reply to this vault (sets authorVaultSlug). Also used as upload destination for --auto-attachments.")
455
+ .option("--vault-slug <vaultSlug>", "Attribute the reply to this vault (sets authorVaultSlug). Caller must own the vault.")
498
456
  .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
499
457
  .option("--attach <file>", "Local media file to attach to this reply. Repeatable. X-style mix rule: up to 4 photos OR 1 GIF OR 1 video. Size ceilings: 5MB photos / 15MB GIFs / 512MB video.", (value, prev = []) => [...prev, value], [])
500
458
  .action(async (postId, opts) => {
@@ -505,18 +463,12 @@ export function registerSpaceCommand(program) {
505
463
  throw new Error("--content and --rich-text are mutually exclusive.");
506
464
  }
507
465
  let authorVaultSlug;
508
- if (opts.vaultSlug || opts.autoAttachments) {
466
+ if (opts.vaultSlug) {
509
467
  authorVaultSlug = resolveVaultSlug(opts);
510
468
  }
511
469
  const body = {};
512
470
  if (opts.content != null) {
513
- const content = readContent(opts.content);
514
- if (opts.autoAttachments) {
515
- const token = await getValidToken();
516
- const links = extractWikiLinks(content);
517
- await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
518
- }
519
- body.content = content;
471
+ body.content = readContent(opts.content);
520
472
  }
521
473
  if (opts.richText != null) {
522
474
  let parsed;
@@ -549,11 +501,10 @@ export function registerSpaceCommand(program) {
549
501
  .description("Edit a reply you authored in a space.")
550
502
  .option("--content <content>", "New content for the reply (markdown supported, use \"-\" for stdin)")
551
503
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
552
- .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before editing (also attributes the reply to that vault)")
553
- .option("--vault-slug <vaultSlug>", "Attribute the reply to this vault (sets authorVaultSlug). Also used as upload destination for --auto-attachments.")
504
+ .option("--vault-slug <vaultSlug>", "Attribute the reply to this vault (sets authorVaultSlug). Caller must own the vault.")
554
505
  .option("--space-slug <spaceSlug>", "Space slug (overrides .gobi/settings.yaml)")
555
506
  .action(async (replyId, opts) => {
556
- const wantsVaultChange = !!(opts.vaultSlug || opts.autoAttachments);
507
+ const wantsVaultChange = !!opts.vaultSlug;
557
508
  if (opts.content == null && opts.richText == null && !wantsVaultChange) {
558
509
  throw new Error("Provide at least --content, --rich-text, or --vault-slug to update.");
559
510
  }
@@ -562,18 +513,12 @@ export function registerSpaceCommand(program) {
562
513
  }
563
514
  const spaceSlug = resolveSpaceSlug(space, opts);
564
515
  let authorVaultSlug;
565
- if (opts.vaultSlug || opts.autoAttachments) {
516
+ if (opts.vaultSlug) {
566
517
  authorVaultSlug = resolveVaultSlug(opts);
567
518
  }
568
519
  const body = {};
569
520
  if (opts.content != null) {
570
- const content = readContent(opts.content);
571
- if (opts.autoAttachments) {
572
- const token = await getValidToken();
573
- const links = extractWikiLinks(content);
574
- await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
575
- }
576
- body.content = content;
521
+ body.content = readContent(opts.content);
577
522
  }
578
523
  if (opts.richText != null) {
579
524
  let parsed;
@@ -1,20 +1,5 @@
1
1
  import { readFileSync } from "fs";
2
- import { apiGet } from "../client.js";
3
2
  import { getSpaceSlug, getVaultSlug } from "./init.js";
4
- // Fetch a draft and return just the fields a post-creation flow needs. Used
5
- // by `space create-post` and `global create-post` when --draft-id is passed:
6
- // title/content come from the draft, vaultSlug seeds the post's authorship
7
- // when the caller did not pass --vault-slug.
8
- export async function fetchDraftSummary(draftId) {
9
- const resp = (await apiGet(`/app/drafts/${draftId}`));
10
- const data = unwrapResp(resp);
11
- return {
12
- draftId: String(data.draftId ?? draftId),
13
- title: String(data.title ?? ""),
14
- content: String(data.content ?? ""),
15
- vaultSlug: data.vaultSlug ?? null,
16
- };
17
- }
18
3
  // Reads all of stdin synchronously. Uses fd 0 (cross-platform) instead of
19
4
  // "/dev/stdin", which doesn't exist on Windows.
20
5
  export function readStdin() {
@@ -103,7 +103,7 @@ export function registerVaultCommand(program) {
103
103
  });
104
104
  const statusCmd = vault
105
105
  .command("status")
106
- .description("Show the configured vault's publish state and metadata (use before posting with --auto-attachments to confirm the vault is public).")
106
+ .description("Show the configured vault's publish state and metadata (use before authoring a markdown artifact with --auto-attachments to confirm the vault is public).")
107
107
  .option("--vault-slug <vaultSlug>", "Vault slug to inspect (defaults to .gobi/settings.yaml)")
108
108
  .action(async (opts) => {
109
109
  const slug = opts.vaultSlug ?? getVaultSlug();
package/dist/main.js CHANGED
@@ -11,7 +11,7 @@ import { registerVaultCommand } from "./commands/vault.js";
11
11
  import { registerSenseCommand } from "./commands/sense.js";
12
12
  import { registerUpdateCommand } from "./commands/update.js";
13
13
  import { registerMediaCommand } from "./commands/media.js";
14
- import { registerDraftCommand } from "./commands/draft.js";
14
+ import { registerArtifactCommand } from "./commands/artifact.js";
15
15
  const require = createRequire(import.meta.url);
16
16
  const { version } = require("../package.json");
17
17
  function hasParentOption(cmd, key) {
@@ -64,7 +64,7 @@ export async function cli() {
64
64
  registerSenseCommand(program);
65
65
  registerUpdateCommand(program);
66
66
  registerMediaCommand(program);
67
- registerDraftCommand(program);
67
+ registerArtifactCommand(program);
68
68
  // Propagate helpWidth to all subcommands
69
69
  const helpWidth = process.stdout.columns || 200;
70
70
  for (const cmd of program.commands) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobi-ai/cli",
3
- "version": "2.0.24",
3
+ "version": "2.0.25",
4
4
  "description": "CLI client for the Gobi collaborative knowledge platform",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,133 @@
1
+ ---
2
+ name: gobi-artifact
3
+ description: >-
4
+ Gobi artifact commands for versioned creations attached to posts: create,
5
+ revise, publish, revert, history, download, delete, get, list. An artifact
6
+ is a human-owned creation (image, video, gif, markdown, or meeting_summary)
7
+ whose revisions form a draft/published tree. Use when the user wants to
8
+ author, version, publish, or attach an artifact to a post.
9
+ allowed-tools: Bash(gobi:*)
10
+ metadata:
11
+ author: gobi-ai
12
+ version: "2.0.25"
13
+ ---
14
+
15
+ # gobi-artifact
16
+
17
+ Gobi artifact commands for versioned, post-attachable creations (v2.0.25).
18
+
19
+ Requires gobi-cli installed and authenticated. See gobi-core skill for setup.
20
+
21
+ ## What is an artifact?
22
+
23
+ An artifact is a versioned creation that can be attached to one or more posts. Each artifact has:
24
+
25
+ - **kind** — one of `image | video | gif | markdown | meeting_summary`. `markdown` and `meeting_summary` carry a markdown **body**; `image`, `gif`, and `video` carry an uploaded **media file**.
26
+ - **title** — optional display title.
27
+ - **owner** — always a human (the calling user). Even when an agent runs the CLI, the artifact is owned by the agent's owner.
28
+ - **revisions** — a draft/published tree. New revisions start as `draft`; publishing one makes it the artifact's single `published` revision (at most one published per artifact). `revise --from <revisionId>` branches off an earlier revision instead of the latest, so the history can fork.
29
+ - **metadata** — per-kind extras. For markdown kinds, `metadata.vaultSlug` is the anchor vault used to resolve `[[wikilinks]]` in the body.
30
+
31
+ Markdown bodies can reference vault notes with `[[wikilinks]]`. Resolution against the anchor vault (`--vault-slug` on create) only works for viewers who can read that vault.
32
+
33
+ ## Important: JSON Mode
34
+
35
+ For programmatic/agent usage, always pass `--json` as a **global** option (before the subcommand):
36
+
37
+ ```bash
38
+ gobi --json artifact list --limit 20
39
+ gobi --json artifact create --kind markdown --content "# Notes" --title "My note"
40
+ gobi --json artifact get <artifactId>
41
+ ```
42
+
43
+ JSON mode wraps the response as `{"success": true, "data": <artifact|revision|...>}` (or `{"success": false, "error": "..."}`).
44
+
45
+ ## Typical Workflow (markdown artifact)
46
+
47
+ Create a markdown artifact, attaching it to a post in the same call:
48
+
49
+ ```bash
50
+ gobi --json artifact create --kind markdown --file notes.md --title "Design notes" \
51
+ --vault-slug my-vault --post-id 12345
52
+ ```
53
+
54
+ The body can come from `--file <path>`, `--content <md>` inline, or stdin (`--content -`). `--vault-slug` anchors `[[wikilink]]` resolution and is stored as `metadata.vaultSlug`.
55
+
56
+ Add `--auto-attachments` (markdown kinds only) to upload any `[[wiki-linked files]]` in the body to the `--vault-slug` vault on webdrive before creating:
57
+
58
+ ```bash
59
+ gobi --json artifact create --kind markdown --file notes.md --vault-slug my-vault --auto-attachments
60
+ ```
61
+
62
+ Revise it (adds a new draft revision), then publish that revision:
63
+
64
+ ```bash
65
+ gobi --json artifact revise <artifactId> --file notes-v2.md --change-note "Tighten intro"
66
+ gobi --json artifact publish <artifactId> --revision <revisionId>
67
+ ```
68
+
69
+ `revise --auto-attachments` reuses the artifact's stored `metadata.vaultSlug` (it GETs the artifact first), so you don't repeat `--vault-slug`.
70
+
71
+ Inspect and roll back:
72
+
73
+ ```bash
74
+ gobi --json artifact history <artifactId> # full revision tree (owner only)
75
+ gobi --json artifact revert <artifactId> --to <revisionId>
76
+ ```
77
+
78
+ ## Typical Workflow (media artifact)
79
+
80
+ Image / gif / video kinds upload a local file (init → PUT → create) instead of a body:
81
+
82
+ ```bash
83
+ gobi --json artifact create --kind image --file diagram.png --title "Architecture" --post-id 12345
84
+ ```
85
+
86
+ Media-file size ceilings mirror post media: 5MB photos / 15MB GIFs / 512MB video, derived from the file's content type. Revise a media artifact by uploading a replacement file:
87
+
88
+ ```bash
89
+ gobi --json artifact revise <artifactId> --file diagram-v2.png --change-note "Add cache layer"
90
+ ```
91
+
92
+ ## Download
93
+
94
+ `download` defaults to the artifact's current (published, else latest) revision; pass `--revision` to pick one.
95
+
96
+ - markdown → writes the body to `--out <path>`, or prints to stdout when `--out` is omitted.
97
+ - media → fetches the `mediaUrl` bytes to `--out <path>` (defaults to `<artifactId>.<ext>`).
98
+
99
+ ```bash
100
+ gobi artifact download <artifactId> --out notes.md
101
+ gobi artifact download <artifactId> --revision <revisionId> --out image.png
102
+ ```
103
+
104
+ ## Attaching to a post
105
+
106
+ `create --post-id <id>` attaches the new artifact to a post **without clobbering** the post's existing artifacts: the CLI reads the post's current artifact attachments, appends the new id, and writes the merged set via the post edit endpoint (`PATCH /posts/:id` with `artifactIds`). The post API's `artifactIds` is otherwise a full replacement.
107
+
108
+ Attaching is done at artifact-create time via `--post-id`. The post commands (`create-post` / `edit-post`) do **not** expose an artifact-attach flag — to attach to an existing post, create the artifact with `--post-id <that post's id>`.
109
+
110
+ ## Available Commands
111
+
112
+ - `gobi artifact create` — Create an artifact (markdown body or uploaded media). `--post-id` attaches it to a post; `--auto-attachments` (markdown) uploads `[[wikilinks]]`.
113
+ - `gobi artifact revise` — Add a draft revision (new body or media file). `--from` branches off a specific revision.
114
+ - `gobi artifact publish` — Publish a revision (becomes the single published revision).
115
+ - `gobi artifact revert` — Move the published pointer to an earlier revision.
116
+ - `gobi artifact history` — List the full revision tree (owner only).
117
+ - `gobi artifact download` — Download a revision's content (markdown body or media bytes).
118
+ - `gobi artifact delete` — Delete an artifact and its revision tree.
119
+ - `gobi artifact get` — Get one artifact with its current revision.
120
+ - `gobi artifact list` — List your artifacts (`--kind`, `--limit`).
121
+
122
+ ## Confirm before mutating
123
+
124
+ Artifacts are user-owned creations. The authoring commands (`create`, `revise`) are the normal flow and run without extra confirmation. Two commands change what's live or destroy data — confirm first:
125
+
126
+ - `publish <id> --revision <id>` / `revert <id> --to <id>` — change which revision is published (visible on attached posts). Confirm the target revision with the user.
127
+ - `delete <id>` — irreversible (removes the artifact and its whole revision tree). Confirm the target id before running.
128
+
129
+ Read-only commands (`get`, `list`, `history`) and `download` run without confirmation.
130
+
131
+ ## Reference Documentation
132
+
133
+ - [gobi artifact](references/artifact.md)