@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.
@@ -0,0 +1,472 @@
1
+ import { existsSync, readFileSync, statSync, } from "fs";
2
+ import { basename, extname, isAbsolute, resolve } from "path";
3
+ import { apiDelete, apiGet, apiPatch, apiPost } from "../client.js";
4
+ import { isJsonMode, jsonOut, readStdin, unwrapResp } from "./utils.js";
5
+ import { extractWikiLinks, uploadAttachments } from "../attachments.js";
6
+ import { getValidToken } from "../auth/manager.js";
7
+ // Artifact kinds, mirrored from the backend (entities/artifact.entity.ts).
8
+ const MARKDOWN_KINDS = ["markdown", "meeting_summary"];
9
+ const MEDIA_KINDS = ["image", "video", "gif"];
10
+ const ALL_KINDS = [...MEDIA_KINDS, ...MARKDOWN_KINDS];
11
+ function isMarkdownKind(kind) {
12
+ return MARKDOWN_KINDS.includes(kind);
13
+ }
14
+ function isMediaKind(kind) {
15
+ return MEDIA_KINDS.includes(kind);
16
+ }
17
+ // Best-effort extension → MIME mapping for artifact media uploads. Mirrors the
18
+ // post-attachment map in attachments.ts; the backend derives the per-tier size
19
+ // ceiling (5MB photos / 15MB GIFs / 512MB video) from the content type.
20
+ const ARTIFACT_MIME_MAP = {
21
+ ".jpg": "image/jpeg",
22
+ ".jpeg": "image/jpeg",
23
+ ".png": "image/png",
24
+ ".gif": "image/gif",
25
+ ".webp": "image/webp",
26
+ ".heic": "image/heic",
27
+ ".heif": "image/heif",
28
+ ".avif": "image/avif",
29
+ ".svg": "image/svg+xml",
30
+ ".bmp": "image/bmp",
31
+ ".tiff": "image/tiff",
32
+ ".mp4": "video/mp4",
33
+ ".mov": "video/quicktime",
34
+ ".webm": "video/webm",
35
+ ".m4v": "video/x-m4v",
36
+ };
37
+ function readContent(value) {
38
+ if (value === "-")
39
+ return readStdin();
40
+ return value;
41
+ }
42
+ // Resolve the markdown body for create/revise from --file | --content | stdin.
43
+ // Returns undefined when none is given (revise may carry media instead).
44
+ function resolveBody(opts) {
45
+ if (opts.file && opts.content) {
46
+ throw new Error("--file and --content are mutually exclusive.");
47
+ }
48
+ if (opts.file) {
49
+ const abs = isAbsolute(opts.file) ? opts.file : resolve(process.cwd(), opts.file);
50
+ if (!existsSync(abs))
51
+ throw new Error(`File not found: ${opts.file}`);
52
+ return readFileSync(abs, "utf8");
53
+ }
54
+ if (opts.content != null)
55
+ return readContent(opts.content);
56
+ return undefined;
57
+ }
58
+ // Upload a local media file via POST /artifacts/upload-url → PUT to S3 →
59
+ // return { mediaUrl, mediaKey } for the artifact create/revise body. Mirrors
60
+ // uploadPostAttachment in attachments.ts but against the artifact endpoint.
61
+ async function uploadArtifactMedia(filePath) {
62
+ const abs = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
63
+ if (!existsSync(abs))
64
+ throw new Error(`File not found: ${filePath}`);
65
+ const ext = extname(abs).toLowerCase();
66
+ const contentType = ARTIFACT_MIME_MAP[ext] || "application/octet-stream";
67
+ const fileSize = statSync(abs).size;
68
+ const initResp = (await apiPost("/artifacts/upload-url", {
69
+ fileName: basename(abs),
70
+ contentType,
71
+ fileSize,
72
+ }));
73
+ const data = unwrapResp(initResp);
74
+ const uploadUrl = data.uploadUrl;
75
+ const mediaUrl = data.mediaUrl;
76
+ const mediaKey = data.mediaKey;
77
+ if (!uploadUrl || !mediaUrl || !mediaKey) {
78
+ throw new Error("Upload init returned an incomplete payload");
79
+ }
80
+ const putRes = await fetch(uploadUrl, {
81
+ method: "PUT",
82
+ headers: { "Content-Type": contentType },
83
+ body: readFileSync(abs),
84
+ });
85
+ if (!putRes.ok) {
86
+ throw new Error(`Failed to PUT ${filePath} to S3: HTTP ${putRes.status}`);
87
+ }
88
+ return { mediaUrl, mediaKey };
89
+ }
90
+ // Append an artifact to a post without clobbering its existing artifacts. The
91
+ // post edit endpoint treats `artifactIds` as a full replacement of the post's
92
+ // artifact attachments, so we read the post's current artifact attachments,
93
+ // append the new id, and write the merged set.
94
+ async function attachArtifactToPost(postId, artifactId) {
95
+ const resp = (await apiGet(`/posts/${postId}`));
96
+ const data = unwrapResp(resp);
97
+ const post = (data.update || data.post || data);
98
+ const attachments = (post.attachments || []);
99
+ const existing = attachments
100
+ .map((a) => a.artifact?.artifactId)
101
+ .filter((x) => typeof x === "string");
102
+ const merged = existing.includes(artifactId)
103
+ ? existing
104
+ : [...existing, artifactId];
105
+ await apiPatch(`/posts/${postId}`, { artifactIds: merged });
106
+ }
107
+ function formatRevisionLine(r) {
108
+ const marker = r.state === "published" ? "✓ published" : " draft";
109
+ const note = r.changeNote ? ` — ${r.changeNote}` : "";
110
+ const parent = r.parentRevisionId != null ? ` (from ${r.parentRevisionId.slice(0, 8)})` : "";
111
+ return ` seq ${r.seq} [${marker}] ${r.revisionId.slice(0, 8)} by user ${r.authorUserId}${parent}${note} ${r.createdAt}`;
112
+ }
113
+ function printArtifact(a) {
114
+ console.log(`Artifact ${a.artifactId}`);
115
+ console.log(` kind: ${a.kind}`);
116
+ if (a.title)
117
+ console.log(` title: ${a.title}`);
118
+ console.log(` owner: user ${a.ownerUserId}`);
119
+ if (a.ownerVault) {
120
+ console.log(` vault: ${a.ownerVault.slug} (${a.ownerVault.public ? "public" : "private"})`);
121
+ }
122
+ console.log(` published: ${a.isPublished ? "yes" : "no"}`);
123
+ console.log(` canWrite: ${a.canWrite ? "yes" : "no"}`);
124
+ console.log(` created: ${a.createdAt}`);
125
+ console.log(` updated: ${a.updatedAt}`);
126
+ const rev = a.revision;
127
+ if (rev) {
128
+ console.log("");
129
+ console.log(`Current revision (seq ${rev.seq}, ${rev.state}):`);
130
+ if (rev.content != null) {
131
+ console.log("");
132
+ console.log(rev.content);
133
+ }
134
+ if (rev.mediaUrl)
135
+ console.log(` mediaUrl: ${rev.mediaUrl}`);
136
+ }
137
+ }
138
+ export function registerArtifactCommand(program) {
139
+ const artifact = program
140
+ .command("artifact")
141
+ .description("Versioned creations attached to posts. Kinds: image | video | gif | markdown | meeting_summary. " +
142
+ "Always human-owned; revisions form a draft/published tree (at most one published per artifact).");
143
+ // ── Create ──
144
+ artifact
145
+ .command("create")
146
+ .description("Create an artifact. markdown/meeting_summary kinds take a body via --file, --content, or stdin (\"-\"). image/gif/video kinds upload --file. Pass --post-id to attach the new artifact to a post.")
147
+ .requiredOption("--kind <kind>", `Artifact kind: ${ALL_KINDS.join(" | ")}`)
148
+ .option("--file <path>", "Local file: markdown body (markdown kinds) or media file (media kinds)")
149
+ .option("--content <md>", "Markdown body inline (markdown kinds; pass \"-\" for stdin)")
150
+ .option("--title <t>", "Display title")
151
+ .option("--vault-slug <slug>", "Anchor vault for [[wikilink]] resolution (markdown kinds). Stored in metadata.vaultSlug.")
152
+ .option("--post-id <id>", "Attach the created artifact to this post afterward")
153
+ .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before creating (markdown kinds; uses --vault-slug)")
154
+ .option("--change-note <note>", "Note describing this revision")
155
+ .action(async (opts) => {
156
+ const kind = opts.kind;
157
+ if (!ALL_KINDS.includes(kind)) {
158
+ throw new Error(`--kind must be one of: ${ALL_KINDS.join(", ")}`);
159
+ }
160
+ const body = { kind };
161
+ if (opts.title != null)
162
+ body.title = opts.title;
163
+ if (opts.vaultSlug)
164
+ body.vaultSlug = opts.vaultSlug;
165
+ if (opts.changeNote != null)
166
+ body.changeNote = opts.changeNote;
167
+ if (isMarkdownKind(kind)) {
168
+ const content = resolveBody(opts);
169
+ if (content == null) {
170
+ throw new Error("markdown/meeting_summary kinds require a body via --file, --content, or stdin.");
171
+ }
172
+ if (opts.autoAttachments) {
173
+ if (!opts.vaultSlug) {
174
+ throw new Error("--auto-attachments requires --vault-slug.");
175
+ }
176
+ const token = await getValidToken();
177
+ const links = extractWikiLinks(content);
178
+ await uploadAttachments(opts.vaultSlug, links, token, {
179
+ addToSyncfiles: true,
180
+ });
181
+ }
182
+ body.content = content;
183
+ }
184
+ else if (isMediaKind(kind)) {
185
+ if (!opts.file)
186
+ throw new Error(`${kind} kind requires --file.`);
187
+ if (opts.content != null) {
188
+ throw new Error("--content is only valid for markdown kinds.");
189
+ }
190
+ if (opts.autoAttachments) {
191
+ throw new Error("--auto-attachments is only valid for markdown kinds.");
192
+ }
193
+ const { mediaUrl, mediaKey } = await uploadArtifactMedia(opts.file);
194
+ body.mediaUrl = mediaUrl;
195
+ body.mediaKey = mediaKey;
196
+ }
197
+ const resp = (await apiPost("/artifacts", body));
198
+ const a = unwrapResp(resp);
199
+ if (opts.postId) {
200
+ await attachArtifactToPost(opts.postId, a.artifactId);
201
+ }
202
+ if (isJsonMode(artifact)) {
203
+ jsonOut(a);
204
+ return;
205
+ }
206
+ console.log(`Artifact created!\n` +
207
+ ` ID: ${a.artifactId}\n` +
208
+ ` Kind: ${a.kind}` +
209
+ (a.title ? `\n Title: ${a.title}` : "") +
210
+ (opts.postId ? `\n Attached to post: ${opts.postId}` : ""));
211
+ });
212
+ // ── Revise ──
213
+ artifact
214
+ .command("revise <artifactId>")
215
+ .description("Add a draft revision to an artifact. New body via --file, --content, or stdin (markdown), or --file (media). Use --from to branch off a specific revision.")
216
+ .option("--file <path>", "Local file: markdown body (markdown kinds) or media file (media kinds)")
217
+ .option("--content <md>", "Markdown body inline (markdown kinds; pass \"-\" for stdin)")
218
+ .option("--change-note <note>", "Note describing this revision")
219
+ .option("--from <revisionId>", "Branch the new draft off this revision (defaults to the latest)")
220
+ .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before revising (markdown kinds; uses the artifact's stored metadata.vaultSlug)")
221
+ .action(async (artifactId, opts) => {
222
+ // Fetch the artifact first to learn its kind — that determines how
223
+ // --file is handled (markdown body text vs binary media upload). Also
224
+ // carries metadata.vaultSlug for --auto-attachments. Branching on kind
225
+ // (like `create`) is REQUIRED: resolveBody() reads --file as UTF-8, so
226
+ // calling it for a media kind would corrupt the binary into `content`.
227
+ const getResp = (await apiGet(`/artifacts/${artifactId}`));
228
+ const existing = unwrapResp(getResp);
229
+ const kind = existing.kind;
230
+ const body = {};
231
+ if (opts.changeNote != null)
232
+ body.changeNote = opts.changeNote;
233
+ if (opts.from)
234
+ body.fromRevisionId = opts.from;
235
+ if (isMarkdownKind(kind)) {
236
+ const content = resolveBody(opts);
237
+ if (content == null) {
238
+ throw new Error("markdown/meeting_summary kinds require a new body via --file, --content, or stdin.");
239
+ }
240
+ if (opts.autoAttachments) {
241
+ const vaultSlug = existing.metadata?.vaultSlug;
242
+ if (!vaultSlug) {
243
+ throw new Error("--auto-attachments needs the artifact's metadata.vaultSlug, which is not set.");
244
+ }
245
+ const token = await getValidToken();
246
+ const links = extractWikiLinks(content);
247
+ await uploadAttachments(vaultSlug, links, token, {
248
+ addToSyncfiles: true,
249
+ });
250
+ }
251
+ body.content = content;
252
+ }
253
+ else if (isMediaKind(kind)) {
254
+ if (!opts.file)
255
+ throw new Error(`${kind} kind requires --file.`);
256
+ if (opts.content != null) {
257
+ throw new Error("--content is only valid for markdown kinds.");
258
+ }
259
+ if (opts.autoAttachments) {
260
+ throw new Error("--auto-attachments is only valid for markdown kinds.");
261
+ }
262
+ const { mediaUrl, mediaKey } = await uploadArtifactMedia(opts.file);
263
+ body.mediaUrl = mediaUrl;
264
+ body.mediaKey = mediaKey;
265
+ }
266
+ const resp = (await apiPost(`/artifacts/${artifactId}/revisions`, body));
267
+ const rev = unwrapResp(resp);
268
+ if (isJsonMode(artifact)) {
269
+ jsonOut(rev);
270
+ return;
271
+ }
272
+ console.log(`Draft revision added!\n` +
273
+ ` Revision: ${rev.revisionId}\n` +
274
+ ` Seq: ${rev.seq}\n` +
275
+ ` State: ${rev.state}`);
276
+ });
277
+ // ── Publish ──
278
+ artifact
279
+ .command("publish <artifactId>")
280
+ .description("Publish a revision (becomes the artifact's single published revision).")
281
+ .requiredOption("--revision <revisionId>", "Revision to publish")
282
+ .action(async (artifactId, opts) => {
283
+ const resp = (await apiPost(`/artifacts/${artifactId}/publish`, {
284
+ revisionId: opts.revision,
285
+ }));
286
+ const a = unwrapResp(resp);
287
+ if (isJsonMode(artifact)) {
288
+ jsonOut(a);
289
+ return;
290
+ }
291
+ console.log(`Published!\n Artifact: ${a.artifactId}\n Revision: ${opts.revision}`);
292
+ });
293
+ // ── Revert ──
294
+ artifact
295
+ .command("revert <artifactId>")
296
+ .description("Revert the artifact's published pointer to an earlier revision.")
297
+ .requiredOption("--to <revisionId>", "Revision to revert to")
298
+ .action(async (artifactId, opts) => {
299
+ const resp = (await apiPost(`/artifacts/${artifactId}/revert`, {
300
+ toRevisionId: opts.to,
301
+ }));
302
+ const a = unwrapResp(resp);
303
+ if (isJsonMode(artifact)) {
304
+ jsonOut(a);
305
+ return;
306
+ }
307
+ console.log(`Reverted!\n Artifact: ${a.artifactId}\n Now published: ${opts.to}`);
308
+ });
309
+ // ── History ──
310
+ artifact
311
+ .command("history <artifactId>")
312
+ .description("List the artifact's full revision tree (owner only).")
313
+ .action(async (artifactId) => {
314
+ const resp = (await apiGet(`/artifacts/${artifactId}/revisions`));
315
+ const revisions = unwrapResp(resp) || [];
316
+ if (isJsonMode(artifact)) {
317
+ jsonOut(revisions);
318
+ return;
319
+ }
320
+ if (!revisions.length) {
321
+ console.log("No revisions.");
322
+ return;
323
+ }
324
+ console.log(`Revision history (${revisions.length}):`);
325
+ for (const r of revisions)
326
+ console.log(formatRevisionLine(r));
327
+ });
328
+ // ── Download ──
329
+ artifact
330
+ .command("download <artifactId>")
331
+ .description("Download an artifact's content. markdown → write the body; media → fetch the bytes. Defaults to the published/latest revision; pass --revision to pick one. Writes to --out or stdout (markdown).")
332
+ .option("--revision <revisionId>", "Specific revision (defaults to the artifact's current revision)")
333
+ .option("--out <path>", "Write to this file (markdown defaults to stdout)")
334
+ .action(async (artifactId, opts) => {
335
+ // Resolve the target revision. Without --revision, use the artifact's
336
+ // current (published/latest) revision from GET /artifacts/:id.
337
+ let kind;
338
+ let rev;
339
+ if (opts.revision) {
340
+ const aResp = (await apiGet(`/artifacts/${artifactId}`));
341
+ kind = unwrapResp(aResp).kind;
342
+ const rResp = (await apiGet(`/artifacts/${artifactId}/revisions/${opts.revision}`));
343
+ rev = unwrapResp(rResp);
344
+ }
345
+ else {
346
+ const aResp = (await apiGet(`/artifacts/${artifactId}`));
347
+ const a = unwrapResp(aResp);
348
+ if (!a.revision) {
349
+ throw new Error("Artifact has no current revision to download.");
350
+ }
351
+ kind = a.kind;
352
+ rev = a.revision;
353
+ }
354
+ if (isMarkdownKind(kind)) {
355
+ const content = rev.content ?? "";
356
+ if (opts.out) {
357
+ const { writeFile, mkdir } = await import("fs/promises");
358
+ const { dirname } = await import("path");
359
+ await mkdir(dirname(opts.out), { recursive: true });
360
+ await writeFile(opts.out, content, "utf8");
361
+ if (isJsonMode(artifact)) {
362
+ jsonOut({ artifactId, revisionId: rev.revisionId, filename: opts.out, size: Buffer.byteLength(content) });
363
+ return;
364
+ }
365
+ console.log(`Wrote ${opts.out} (${Buffer.byteLength(content)} bytes)`);
366
+ return;
367
+ }
368
+ if (isJsonMode(artifact)) {
369
+ jsonOut({ artifactId, revisionId: rev.revisionId, content });
370
+ return;
371
+ }
372
+ process.stdout.write(content.endsWith("\n") ? content : content + "\n");
373
+ return;
374
+ }
375
+ // Media kind — fetch the mediaUrl bytes.
376
+ if (!rev.mediaUrl) {
377
+ throw new Error("Revision has no media URL to download.");
378
+ }
379
+ const res = await fetch(rev.mediaUrl);
380
+ if (!res.ok) {
381
+ throw new Error(`Failed to fetch media from ${rev.mediaUrl}: HTTP ${res.status}`);
382
+ }
383
+ const buffer = Buffer.from(await res.arrayBuffer());
384
+ const contentType = res.headers.get("content-type") || "application/octet-stream";
385
+ const out = opts.out || `${artifactId}${extForContentType(contentType)}`;
386
+ const { writeFile, mkdir } = await import("fs/promises");
387
+ const { dirname } = await import("path");
388
+ await mkdir(dirname(out), { recursive: true });
389
+ await writeFile(out, buffer);
390
+ if (isJsonMode(artifact)) {
391
+ jsonOut({ artifactId, revisionId: rev.revisionId, filename: out, contentType, size: buffer.length });
392
+ return;
393
+ }
394
+ console.log(`Saved media to ${out} (${buffer.length} bytes)`);
395
+ });
396
+ // ── Delete ──
397
+ artifact
398
+ .command("delete <artifactId>")
399
+ .description("Delete an artifact (and its revision tree).")
400
+ .action(async (artifactId) => {
401
+ await apiDelete(`/artifacts/${artifactId}`);
402
+ if (isJsonMode(artifact)) {
403
+ jsonOut({ artifactId, ok: true });
404
+ return;
405
+ }
406
+ console.log(`Deleted ${artifactId}.`);
407
+ });
408
+ // ── Get ──
409
+ artifact
410
+ .command("get <artifactId>")
411
+ .description("Get one artifact with its current revision.")
412
+ .action(async (artifactId) => {
413
+ const resp = (await apiGet(`/artifacts/${artifactId}`));
414
+ const a = unwrapResp(resp);
415
+ if (isJsonMode(artifact)) {
416
+ jsonOut(a);
417
+ return;
418
+ }
419
+ printArtifact(a);
420
+ });
421
+ // ── List ──
422
+ artifact
423
+ .command("list")
424
+ .description("List your artifacts (newest first).")
425
+ .option("--kind <kind>", `Filter by kind: ${ALL_KINDS.join(" | ")}`)
426
+ .option("--limit <n>", "Max items to return")
427
+ .action(async (opts) => {
428
+ const params = {};
429
+ if (opts.kind) {
430
+ if (!ALL_KINDS.includes(opts.kind)) {
431
+ throw new Error(`--kind must be one of: ${ALL_KINDS.join(", ")}`);
432
+ }
433
+ params.kind = opts.kind;
434
+ }
435
+ if (opts.limit)
436
+ params.limit = parseInt(opts.limit, 10);
437
+ const resp = (await apiGet("/artifacts", params));
438
+ const items = unwrapResp(resp) || [];
439
+ if (isJsonMode(artifact)) {
440
+ jsonOut(items);
441
+ return;
442
+ }
443
+ if (!items.length) {
444
+ console.log("No artifacts.");
445
+ return;
446
+ }
447
+ console.log(`Artifacts (${items.length}):`);
448
+ for (const a of items) {
449
+ const pub = a.isPublished ? "published" : "draft";
450
+ console.log(`- [${a.kind}] ${a.artifactId.slice(0, 8)} ${a.title ?? "(untitled)"} (${pub}, ${a.updatedAt})`);
451
+ }
452
+ });
453
+ }
454
+ // Pick a file extension for a downloaded media artifact from its content type.
455
+ function extForContentType(contentType) {
456
+ const ct = contentType.toLowerCase();
457
+ if (ct.includes("png"))
458
+ return ".png";
459
+ if (ct.includes("jpeg") || ct.includes("jpg"))
460
+ return ".jpg";
461
+ if (ct.includes("webp"))
462
+ return ".webp";
463
+ if (ct.includes("gif"))
464
+ return ".gif";
465
+ if (ct.includes("mp4"))
466
+ return ".mp4";
467
+ if (ct.includes("quicktime"))
468
+ return ".mov";
469
+ if (ct.includes("webm"))
470
+ return ".webm";
471
+ return "";
472
+ }
@@ -1,8 +1,7 @@
1
1
  import { WEB_BASE_URL } from "../constants.js";
2
2
  import { apiGet, apiPost, apiPatch, apiDelete } from "../client.js";
3
- import { fetchDraftSummary, isJsonMode, jsonOut, readStdin, resolveVaultSlug, unwrapResp, } from "./utils.js";
4
- import { extractWikiLinks, uploadAttachments, uploadPostAttachments, assertPostAttachmentMix, } from "../attachments.js";
5
- import { getValidToken } from "../auth/manager.js";
3
+ import { isJsonMode, jsonOut, readStdin, resolveVaultSlug, unwrapResp, } from "./utils.js";
4
+ import { uploadPostAttachments, assertPostAttachmentMix, } from "../attachments.js";
6
5
  function readContent(value) {
7
6
  if (value === "-")
8
7
  return readStdin();
@@ -188,48 +187,29 @@ export function registerGlobalCommand(program) {
188
187
  // ── Create post ──
189
188
  global
190
189
  .command("create-post")
191
- .description("Create a post in the global feed. --vault-slug attributes it to a vault you own. With no --vault-slug and no --auto-attachments, the post is created without an authorVaultSlug (vault-less personal post).")
190
+ .description("Create a post in the global feed. --vault-slug attributes it to a vault you own. With no --vault-slug, the post is created without an authorVaultSlug (vault-less personal post).")
192
191
  .option("--title <title>", "Title of the post")
193
192
  .option("--content <content>", "Post content (markdown supported, use \"-\" for stdin)")
194
193
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
195
194
  .option("--vault-slug <vaultSlug>", "Attribute the post to this vault (sets authorVaultSlug). Caller must own the vault.")
196
- .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before posting (also sets authorVaultSlug to that vault)")
197
- .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 on draft.metadata so the client can render an 'Open post' button. The draft's vaultSlug seeds --vault-slug when not given explicitly.")
198
195
  .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], [])
199
196
  .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.")
200
197
  .action(async (opts) => {
201
- if (opts.draftId) {
202
- if (opts.title || opts.content || opts.richText) {
203
- throw new Error("--draft-id sources title and content from the draft; --title, --content, and --rich-text are not allowed alongside it.");
204
- }
198
+ if (!opts.content && !opts.richText) {
199
+ throw new Error("Provide either --content or --rich-text.");
205
200
  }
206
- else {
207
- if (!opts.content && !opts.richText) {
208
- throw new Error("Provide either --content or --rich-text (or --draft-id).");
209
- }
210
- if (opts.content && opts.richText) {
211
- throw new Error("--content and --rich-text are mutually exclusive.");
212
- }
201
+ if (opts.content && opts.richText) {
202
+ throw new Error("--content and --rich-text are mutually exclusive.");
213
203
  }
214
- const draft = opts.draftId ? await fetchDraftSummary(opts.draftId) : null;
215
- const effectiveTitle = draft ? draft.title : opts.title;
216
- const effectiveContent = draft ? draft.content : opts.content;
217
- const effectiveVaultSlugOpt = opts.vaultSlug ?? draft?.vaultSlug ?? undefined;
218
204
  let authorVaultSlug;
219
- if (effectiveVaultSlugOpt || opts.autoAttachments) {
220
- authorVaultSlug = resolveVaultSlug({ vaultSlug: effectiveVaultSlugOpt });
205
+ if (opts.vaultSlug) {
206
+ authorVaultSlug = resolveVaultSlug({ vaultSlug: opts.vaultSlug });
221
207
  }
222
208
  const body = {};
223
- if (effectiveTitle != null)
224
- body.title = effectiveTitle;
225
- if (effectiveContent != null) {
226
- const content = draft ? effectiveContent : readContent(effectiveContent);
227
- if (opts.autoAttachments && authorVaultSlug) {
228
- const token = await getValidToken();
229
- const links = extractWikiLinks(content);
230
- await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
231
- }
232
- body.content = content;
209
+ if (opts.title != null)
210
+ body.title = opts.title;
211
+ if (opts.content != null) {
212
+ body.content = readContent(opts.content);
233
213
  }
234
214
  if (opts.richText != null) {
235
215
  let parsed;
@@ -256,19 +236,6 @@ export function registerGlobalCommand(program) {
256
236
  }
257
237
  const resp = (await apiPost(`/posts`, body));
258
238
  const post = unwrapResp(resp);
259
- if (opts.draftId && post.id != null) {
260
- try {
261
- await apiPatch(`/app/drafts/${opts.draftId}/metadata`, {
262
- postId: typeof post.id === "number" ? post.id : Number(post.id),
263
- spaceSlug: null,
264
- });
265
- }
266
- catch (e) {
267
- // Don't fail the create if linking fails — the post is live; just
268
- // surface a warning so the agent can mention it.
269
- console.error(`Warning: failed to link post to draft ${opts.draftId}: ${e.message}`);
270
- }
271
- }
272
239
  const shareUrl = buildPersonalPostUrl(post);
273
240
  if (isJsonMode(global)) {
274
241
  jsonOut({ ...post, shareUrl });
@@ -278,8 +245,7 @@ export function registerGlobalCommand(program) {
278
245
  ` ID: ${post.id}\n` +
279
246
  (post.title ? ` Title: ${post.title}\n` : "") +
280
247
  ` Created: ${post.createdAt}\n` +
281
- ` URL: ${shareUrl}` +
282
- (opts.draftId ? `\n Linked to draft: ${opts.draftId}` : ""));
248
+ ` URL: ${shareUrl}`);
283
249
  });
284
250
  // ── Edit post ──
285
251
  global
@@ -289,10 +255,9 @@ export function registerGlobalCommand(program) {
289
255
  .option("--content <content>", "New content (markdown supported, use \"-\" for stdin)")
290
256
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
291
257
  .option("--vault-slug <vaultSlug>", "Attribute the post to this vault (sets authorVaultSlug).")
292
- .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before editing (uses --vault-slug or .gobi vault)")
293
258
  .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], [])
294
259
  .action(async (postId, opts) => {
295
- const wantsVaultChange = !!(opts.vaultSlug || opts.autoAttachments);
260
+ const wantsVaultChange = !!opts.vaultSlug;
296
261
  const wantsAttachChange = !!(opts.attach && opts.attach.length > 0);
297
262
  if (opts.title == null &&
298
263
  opts.content == null &&
@@ -305,20 +270,14 @@ export function registerGlobalCommand(program) {
305
270
  throw new Error("--content and --rich-text are mutually exclusive.");
306
271
  }
307
272
  let authorVaultSlug;
308
- if (opts.vaultSlug || opts.autoAttachments) {
273
+ if (opts.vaultSlug) {
309
274
  authorVaultSlug = resolveVaultSlug(opts);
310
275
  }
311
276
  const body = {};
312
277
  if (opts.title != null)
313
278
  body.title = opts.title;
314
279
  if (opts.content != null) {
315
- const content = readContent(opts.content);
316
- if (opts.autoAttachments && authorVaultSlug) {
317
- const token = await getValidToken();
318
- const links = extractWikiLinks(content);
319
- await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
320
- }
321
- body.content = content;
280
+ body.content = readContent(opts.content);
322
281
  }
323
282
  if (opts.richText != null) {
324
283
  let parsed;
@@ -362,8 +321,7 @@ export function registerGlobalCommand(program) {
362
321
  .description("Create a reply to a post in the global feed.")
363
322
  .option("--content <content>", "Reply content (markdown supported, use \"-\" for stdin)")
364
323
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
365
- .option("--vault-slug <vaultSlug>", "Attribute the reply to this vault (sets authorVaultSlug). Also used as upload destination for --auto-attachments.")
366
- .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before posting (also attributes the reply to that vault)")
324
+ .option("--vault-slug <vaultSlug>", "Attribute the reply to this vault (sets authorVaultSlug).")
367
325
  .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], [])
368
326
  .action(async (postId, opts) => {
369
327
  if (!opts.content && !opts.richText) {
@@ -373,18 +331,12 @@ export function registerGlobalCommand(program) {
373
331
  throw new Error("--content and --rich-text are mutually exclusive.");
374
332
  }
375
333
  let authorVaultSlug;
376
- if (opts.vaultSlug || opts.autoAttachments) {
334
+ if (opts.vaultSlug) {
377
335
  authorVaultSlug = resolveVaultSlug(opts);
378
336
  }
379
337
  const body = {};
380
338
  if (opts.content != null) {
381
- const content = readContent(opts.content);
382
- if (opts.autoAttachments && authorVaultSlug) {
383
- const token = await getValidToken();
384
- const links = extractWikiLinks(content);
385
- await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
386
- }
387
- body.content = content;
339
+ body.content = readContent(opts.content);
388
340
  }
389
341
  if (opts.richText != null) {
390
342
  let parsed;
@@ -415,10 +367,9 @@ export function registerGlobalCommand(program) {
415
367
  .description("Edit a reply you authored in the global feed.")
416
368
  .option("--content <content>", "New reply content (markdown supported, use \"-\" for stdin)")
417
369
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
418
- .option("--vault-slug <vaultSlug>", "Attribute the reply to this vault (sets authorVaultSlug). Also used as upload destination for --auto-attachments.")
419
- .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before editing (also attributes the reply to that vault)")
370
+ .option("--vault-slug <vaultSlug>", "Attribute the reply to this vault (sets authorVaultSlug).")
420
371
  .action(async (replyId, opts) => {
421
- const wantsVaultChange = !!(opts.vaultSlug || opts.autoAttachments);
372
+ const wantsVaultChange = !!opts.vaultSlug;
422
373
  if (opts.content == null && opts.richText == null && !wantsVaultChange) {
423
374
  throw new Error("Provide at least --content, --rich-text, or --vault-slug to update.");
424
375
  }
@@ -426,18 +377,12 @@ export function registerGlobalCommand(program) {
426
377
  throw new Error("--content and --rich-text are mutually exclusive.");
427
378
  }
428
379
  let authorVaultSlug;
429
- if (opts.vaultSlug || opts.autoAttachments) {
380
+ if (opts.vaultSlug) {
430
381
  authorVaultSlug = resolveVaultSlug(opts);
431
382
  }
432
383
  const body = {};
433
384
  if (opts.content != null) {
434
- const content = readContent(opts.content);
435
- if (opts.autoAttachments && authorVaultSlug) {
436
- const token = await getValidToken();
437
- const links = extractWikiLinks(content);
438
- await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
439
- }
440
- body.content = content;
385
+ body.content = readContent(opts.content);
441
386
  }
442
387
  if (opts.richText != null) {
443
388
  let parsed;