@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.
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +30 -35
- package/dist/commands/artifact.js +472 -0
- package/dist/commands/global.js +24 -79
- package/dist/commands/personal.js +23 -76
- package/dist/commands/space.js +25 -80
- package/dist/commands/utils.js +0 -15
- package/dist/commands/vault.js +1 -1
- package/dist/main.js +2 -2
- package/package.json +1 -1
- package/skills/gobi-artifact/SKILL.md +133 -0
- package/skills/gobi-artifact/references/artifact.md +142 -0
- package/skills/gobi-core/SKILL.md +6 -7
- package/skills/gobi-homepage/SKILL.md +1 -1
- package/skills/gobi-media/SKILL.md +2 -2
- package/skills/gobi-sense/SKILL.md +2 -2
- package/skills/gobi-space/SKILL.md +12 -18
- package/skills/gobi-space/references/global.md +5 -12
- package/skills/gobi-space/references/personal.md +2 -8
- package/skills/gobi-space/references/space.md +4 -10
- package/skills/gobi-vault/SKILL.md +3 -3
- package/skills/gobi-vault/references/vault.md +2 -2
- package/dist/commands/draft.js +0 -230
- package/skills/gobi-draft/SKILL.md +0 -113
- package/skills/gobi-draft/references/draft.md +0 -114
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { apiGet, apiPost, apiPatch, apiDelete } from "../client.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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.
|
|
200
|
-
|
|
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
|
-
|
|
205
|
-
|
|
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 (
|
|
218
|
-
authorVaultSlug = resolveVaultSlug({ vaultSlug:
|
|
203
|
+
if (opts.vaultSlug) {
|
|
204
|
+
authorVaultSlug = resolveVaultSlug({ vaultSlug: opts.vaultSlug });
|
|
219
205
|
}
|
|
220
206
|
const body = {};
|
|
221
|
-
if (
|
|
222
|
-
body.title =
|
|
223
|
-
if (
|
|
224
|
-
|
|
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 = !!
|
|
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
|
|
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
|
-
|
|
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).
|
|
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
|
|
340
|
+
if (opts.vaultSlug) {
|
|
381
341
|
authorVaultSlug = resolveVaultSlug(opts);
|
|
382
342
|
}
|
|
383
343
|
const body = {};
|
|
384
344
|
if (opts.content != null) {
|
|
385
|
-
|
|
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).
|
|
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 = !!
|
|
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
|
|
386
|
+
if (opts.vaultSlug) {
|
|
434
387
|
authorVaultSlug = resolveVaultSlug(opts);
|
|
435
388
|
}
|
|
436
389
|
const body = {};
|
|
437
390
|
if (opts.content != null) {
|
|
438
|
-
|
|
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;
|
package/dist/commands/space.js
CHANGED
|
@@ -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 {
|
|
5
|
-
import {
|
|
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("--
|
|
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.
|
|
327
|
-
|
|
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
|
-
|
|
332
|
-
|
|
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 (
|
|
345
|
-
authorVaultSlug = resolveVaultSlug({ vaultSlug:
|
|
330
|
+
if (opts.vaultSlug) {
|
|
331
|
+
authorVaultSlug = resolveVaultSlug({ vaultSlug: opts.vaultSlug });
|
|
346
332
|
}
|
|
347
333
|
const body = {};
|
|
348
|
-
if (
|
|
349
|
-
body.title =
|
|
350
|
-
if (
|
|
351
|
-
|
|
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("--
|
|
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 = !!
|
|
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
|
|
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
|
-
|
|
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("--
|
|
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
|
|
466
|
+
if (opts.vaultSlug) {
|
|
509
467
|
authorVaultSlug = resolveVaultSlug(opts);
|
|
510
468
|
}
|
|
511
469
|
const body = {};
|
|
512
470
|
if (opts.content != null) {
|
|
513
|
-
|
|
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("--
|
|
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 = !!
|
|
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
|
|
516
|
+
if (opts.vaultSlug) {
|
|
566
517
|
authorVaultSlug = resolveVaultSlug(opts);
|
|
567
518
|
}
|
|
568
519
|
const body = {};
|
|
569
520
|
if (opts.content != null) {
|
|
570
|
-
|
|
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;
|
package/dist/commands/utils.js
CHANGED
|
@@ -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() {
|
package/dist/commands/vault.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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
|
-
|
|
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
|
@@ -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)
|