@gobi-ai/cli 2.0.20 → 2.0.22

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.
@@ -4,12 +4,12 @@
4
4
  "name": "gobi-ai"
5
5
  },
6
6
  "description": "Claude Code plugin for the Gobi collaborative knowledge platform CLI",
7
- "version": "2.0.19",
7
+ "version": "2.0.22",
8
8
  "plugins": [
9
9
  {
10
10
  "name": "gobi",
11
- "description": "Manage the Gobi collaborative knowledge platform from the command line. Publish vault profiles, create posts and replies, manage saved notes and posts, manage sessions, generate images and videos.",
12
- "version": "2.0.19",
11
+ "description": "Manage the Gobi collaborative knowledge platform from the command line. Publish vault profiles, create posts and replies, generate images and videos.",
12
+ "version": "2.0.22",
13
13
  "author": {
14
14
  "name": "gobi-ai"
15
15
  },
@@ -19,7 +19,6 @@
19
19
  "./skills/gobi-core",
20
20
  "./skills/gobi-space",
21
21
  "./skills/gobi-vault",
22
- "./skills/gobi-saved",
23
22
  "./skills/gobi-draft",
24
23
  "./skills/gobi-media",
25
24
  "./skills/gobi-sense",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gobi",
3
3
  "description": "Manage the Gobi collaborative knowledge platform from the command line",
4
- "version": "2.0.19",
4
+ "version": "2.0.22",
5
5
  "author": {
6
6
  "name": "gobi-ai"
7
7
  },
@@ -10,7 +10,6 @@
10
10
  "./skills/gobi-core",
11
11
  "./skills/gobi-space",
12
12
  "./skills/gobi-vault",
13
- "./skills/gobi-saved",
14
13
  "./skills/gobi-draft",
15
14
  "./skills/gobi-media",
16
15
  "./skills/gobi-sense",
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![npm](https://img.shields.io/npm/v/@gobi-ai/cli)](https://www.npmjs.com/package/@gobi-ai/cli)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
6
 
7
- The programmatic interface to [Gobi](https://gobispace.com) — the agent-facing surface of the ecosystem. The same capabilities the desktop and web clients use (auth, vault sync and publishing, personal posts and replies, saved knowledge, drafts, media generation, activity reads) exposed as composable shell commands so AI agents and developer scripts can participate in a user's Brain.
7
+ The programmatic interface to [Gobi](https://gobispace.com) — the agent-facing surface of the ecosystem. The same capabilities the desktop and web clients use (auth, vault sync and publishing, personal posts and replies, drafts, media generation, activity reads) exposed as composable shell commands so AI agents and developer scripts can participate in a user's Brain.
8
8
 
9
9
  ## Why a CLI?
10
10
 
@@ -203,16 +203,6 @@ A *Personal Post* lives on the author's profile (their primary vault) and surfac
203
203
 
204
204
  `--vault-slug` requires that the caller hold `role: 'owner'` on the target vault. When set, it becomes the post's `authorVaultSlug`. When `--auto-attachments` is set, the same vault is used both as the upload destination for `[[wikilinks]]` and as `authorVaultSlug`.
205
205
 
206
- ### Sessions
207
-
208
- | Command | Description |
209
- |---------|-------------|
210
- | `gobi session list` | List your sessions |
211
- | `gobi session get <id>` | Get a session and its messages |
212
- | `gobi session create-reply <id> --content <c>` | Send a message in a session |
213
-
214
- `session create-reply` also accepts `--rich-text <json>` (mutually exclusive with `--content`).
215
-
216
206
  ### Sense
217
207
 
218
208
  Activity and transcription data captured by Gobi Sense (or the mobile app).
@@ -224,31 +214,6 @@ Activity and transcription data captured by Gobi Sense (or the mobile app).
224
214
 
225
215
  Times are ISO 8601 UTC (e.g. `2026-03-20T00:00:00Z`).
226
216
 
227
- ### Saved
228
-
229
- `gobi saved` is the user's personal saved-knowledge collection — notes you author and posts you bookmark.
230
-
231
- #### Saved notes
232
-
233
- | Command | Description |
234
- |---------|-------------|
235
- | `gobi saved list-notes [--date YYYY-MM-DD]` | List your notes (recent via cursor, or all for a day) |
236
- | `gobi saved get-note <id>` | Get a single note |
237
- | `gobi saved create-note --content <c>` | Create a note (use `-` to read content from stdin) |
238
- | `gobi saved edit-note <id> [--content <c>] [--agent-id <id>]` | Edit a note (`--agent-id null` clears the link) |
239
- | `gobi saved delete-note <id>` | Delete a note you authored |
240
-
241
- `saved list-notes` and `saved create-note` accept `--timezone <iana>` (default: system timezone).
242
-
243
- #### Saved posts (bookmarks)
244
-
245
- | Command | Description |
246
- |---------|-------------|
247
- | `gobi saved list-posts [--type all\|article\|space-post]` | List posts you've bookmarked |
248
- | `gobi saved get-post <postId>` | Get a saved post snapshot |
249
- | `gobi saved create-post --source <id>` | Bookmark a post or reply by id (records a snapshot) |
250
- | `gobi saved delete-post <postId>` | Remove a post from your saved collection |
251
-
252
217
  ### Drafts
253
218
 
254
219
  A *draft* is a unit of standing guidance authored by an agent. Each draft carries 0–3 AI-suggested actions the user picks from. The top 5 pending drafts (lowest priority first) are injected into the agent's system prompt every turn — drafts turn agent suggestions into running context.
@@ -313,14 +278,13 @@ Image, video, and avatar generation. See the `gobi-media` skill for full workflo
313
278
 
314
279
  ## Claude Code skills
315
280
 
316
- The CLI ships a `.claude-plugin/` manifest with eight skills that wrap the command groups for Claude Code agents:
281
+ The CLI ships a `.claude-plugin/` manifest with skills that wrap the command groups for Claude Code agents:
317
282
 
318
283
  | Skill | Covers |
319
284
  |-------|--------|
320
- | `gobi-core` | Auth, session, update, space list/warp |
285
+ | `gobi-core` | Auth, update, space list/warp |
321
286
  | `gobi-vault` | `gobi vault init/list/publish/unpublish/sync` |
322
287
  | `gobi-space` | `gobi space …` and `gobi global …` |
323
- | `gobi-saved` | `gobi saved list-notes/create-note/list-posts/create-post/…` |
324
288
  | `gobi-draft` | `gobi draft …` |
325
289
  | `gobi-media` | `gobi media …` |
326
290
  | `gobi-sense` | `gobi sense list-activities/list-transcriptions` |
@@ -0,0 +1,470 @@
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";
5
+ function readContent(value) {
6
+ if (value === "-")
7
+ return readStdin();
8
+ return value;
9
+ }
10
+ function formatFeedLine(m) {
11
+ const isReply = m.parentPostId != null ||
12
+ m.type === "post-reply";
13
+ const id = `[${isReply ? "r" : "p"}:${m.id}]`;
14
+ const kind = isReply ? "reply" : "post ";
15
+ const author = m.author?.name ||
16
+ `User ${m.authorId ?? "?"}`;
17
+ let label;
18
+ if (isReply) {
19
+ const text = m.content || "";
20
+ label = text.length > 80 ? text.slice(0, 80) + "…" : text;
21
+ label = label.replace(/\s+/g, " ").trim();
22
+ }
23
+ else {
24
+ label = m.title || m.content || "";
25
+ }
26
+ return `${id} ${kind} ${author} "${label}" ${m.createdAt}`;
27
+ }
28
+ export function registerPersonalCommand(program) {
29
+ const personal = program
30
+ .command("personal")
31
+ .description("Personal-space commands (private posts and replies visible only to you). " +
32
+ "Mirrors the `global` subcommand shape — posts/replies live in the same data " +
33
+ "model, scoped via personalSpaceUserId so they never surface on the public feed.");
34
+ // ── Feed (unified) ──
35
+ personal
36
+ .command("feed")
37
+ .description("List your personal-space feed (posts and replies, newest first). Only you can see these rows.")
38
+ .option("--limit <number>", "Items per page", "20")
39
+ .option("--cursor <string>", "Pagination cursor from previous response")
40
+ .action(async (opts) => {
41
+ const params = {
42
+ limit: parseInt(opts.limit, 10),
43
+ };
44
+ if (opts.cursor)
45
+ params.cursor = opts.cursor;
46
+ const resp = (await apiGet(`/posts/personal-space`, params));
47
+ if (isJsonMode(personal)) {
48
+ jsonOut({
49
+ items: resp.data || [],
50
+ pagination: resp.pagination || {},
51
+ });
52
+ return;
53
+ }
54
+ const items = (resp.data || []);
55
+ const pagination = (resp.pagination || {});
56
+ if (!items.length) {
57
+ console.log("No items in your personal space yet.");
58
+ return;
59
+ }
60
+ const lines = items.map(formatFeedLine);
61
+ const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
62
+ console.log(`Personal-space feed (${items.length} items, newest first):\n` +
63
+ lines.join("\n") +
64
+ footer);
65
+ });
66
+ // ── List posts ──
67
+ //
68
+ // No server-side roots-only endpoint exists for the personal-space lane;
69
+ // we fetch the unified feed and filter client-side to `type === 'post'`.
70
+ // The `--limit` then applies to the raw feed page, not the post-only
71
+ // count — callers expecting N roots may need to paginate further.
72
+ personal
73
+ .command("list-posts")
74
+ .description("List root posts (no replies) in your personal space. Filters the personal feed client-side; pagination cursor advances through the underlying feed page.")
75
+ .option("--limit <number>", "Items per page (applied to the underlying feed page)", "20")
76
+ .option("--cursor <string>", "Pagination cursor from previous response")
77
+ .action(async (opts) => {
78
+ const params = {
79
+ limit: parseInt(opts.limit, 10),
80
+ };
81
+ if (opts.cursor)
82
+ params.cursor = opts.cursor;
83
+ const resp = (await apiGet(`/posts/personal-space`, params));
84
+ const allItems = (resp.data || []);
85
+ const items = allItems.filter((t) => t.type !== "post-reply" && t.parentPostId == null);
86
+ const pagination = (resp.pagination || {});
87
+ if (isJsonMode(personal)) {
88
+ jsonOut({ items, pagination });
89
+ return;
90
+ }
91
+ if (!items.length) {
92
+ console.log("No posts found in your personal space.");
93
+ return;
94
+ }
95
+ const lines = [];
96
+ for (const t of items) {
97
+ const vaultSlug = t.vault?.vaultSlug ||
98
+ t.authorVault?.vaultSlug ||
99
+ "—";
100
+ lines.push(`- [${t.id}] "${t.title ?? "(no title)"}" (vault: ${vaultSlug}, ${t.replyCount ?? 0} replies, ${t.createdAt})`);
101
+ }
102
+ const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
103
+ console.log(`Personal-space posts (${items.length} of ${allItems.length} feed items):\n` +
104
+ lines.join("\n") +
105
+ footer);
106
+ });
107
+ // ── Get post (with ancestors and replies) ──
108
+ //
109
+ // Same `/posts/:id` and `/posts/:id/ancestors` routes the global command
110
+ // uses — the server gates these by `viewerUserId`, so private rows
111
+ // resolve for the owner and 404 for everyone else. Personal-space posts
112
+ // and global posts share this endpoint without ambiguity.
113
+ personal
114
+ .command("get-post <postId>")
115
+ .description("Get a personal-space post with its ancestors and replies (paginated). Same endpoint as `gobi global get-post`; only the owner can resolve a private id.")
116
+ .option("--limit <number>", "Items per page", "20")
117
+ .option("--cursor <string>", "Pagination cursor from previous response")
118
+ .option("--full", "Show full reply content without truncation")
119
+ .action(async (postId, opts) => {
120
+ const params = {
121
+ limit: parseInt(opts.limit, 10),
122
+ };
123
+ if (opts.cursor)
124
+ params.cursor = opts.cursor;
125
+ const [postResp, ancestorsResp] = await Promise.all([
126
+ apiGet(`/posts/${postId}`, params),
127
+ apiGet(`/posts/${postId}/ancestors`),
128
+ ]);
129
+ const data = unwrapResp(postResp);
130
+ const pagination = (postResp.pagination || {});
131
+ const mentions = (postResp.mentions || {});
132
+ const ancestorsData = unwrapResp(ancestorsResp);
133
+ const ancestors = (ancestorsData.ancestors || []);
134
+ if (isJsonMode(personal)) {
135
+ jsonOut({ ...data, ancestors, pagination, mentions });
136
+ return;
137
+ }
138
+ const post = (data.update || data.post || data);
139
+ const replies = (data.replies || []);
140
+ const author = post.author?.name ||
141
+ `User ${post.authorId}`;
142
+ const vault = post.vault?.vaultSlug ||
143
+ post.authorVault?.vaultSlug ||
144
+ "—";
145
+ const ancestorLines = [];
146
+ if (ancestors.length) {
147
+ ancestors.forEach((a, i) => {
148
+ ancestorLines.push(` ${i + 1}. ${formatFeedLine(a)}`);
149
+ });
150
+ }
151
+ const replyLines = [];
152
+ for (const r of replies) {
153
+ const rAuthor = r.author?.name ||
154
+ `User ${r.authorId}`;
155
+ const text = r.content || "";
156
+ const truncated = opts.full || text.length <= 200 ? text : text.slice(0, 200) + "…";
157
+ replyLines.push(` - ${rAuthor}: ${truncated} (${r.createdAt})`);
158
+ }
159
+ const isReplyPost = post.parentPostId != null;
160
+ const heading = isReplyPost
161
+ ? `Reply [r:${post.id}] (private)`
162
+ : `Post: ${post.title || "(no title)"} (private)`;
163
+ const output = [
164
+ heading,
165
+ `By: ${author} (vault: ${vault}) on ${post.createdAt}`,
166
+ ...(ancestorLines.length
167
+ ? ["", `Ancestors (${ancestors.length} items, root first):`, ...ancestorLines]
168
+ : []),
169
+ "",
170
+ post.content || "",
171
+ "",
172
+ `Replies (${replies.length} items):`,
173
+ ...replyLines,
174
+ ...(pagination.hasMore
175
+ ? [` Next cursor: ${pagination.nextCursor}`]
176
+ : []),
177
+ ].join("\n");
178
+ console.log(output);
179
+ });
180
+ // ── Create post ──
181
+ //
182
+ // Targets `POST /posts/personal-space`, the only endpoint that stamps
183
+ // `personalSpaceUserId` on the row. Body shape is identical to the
184
+ // global `POST /posts` create (same CreatePostDto). The server skips the
185
+ // `@gobi` mention dispatch and the notification fan-out for this lane —
186
+ // private posts have no audience.
187
+ personal
188
+ .command("create-post")
189
+ .description("Create a private post in your personal space. --vault-slug attributes it to a vault you own. Visible only to you.")
190
+ .option("--title <title>", "Title of the post")
191
+ .option("--content <content>", "Post content (markdown supported, use \"-\" for stdin)")
192
+ .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
193
+ .option("--vault-slug <vaultSlug>", "Attribute the post to this vault (sets authorVaultSlug). Defaults to your primary 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
+ .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
+ .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
+ .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
+ }
203
+ }
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
+ }
211
+ }
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
+ let authorVaultSlug;
217
+ if (effectiveVaultSlugOpt || opts.autoAttachments) {
218
+ authorVaultSlug = resolveVaultSlug({ vaultSlug: effectiveVaultSlugOpt });
219
+ }
220
+ 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;
231
+ }
232
+ if (opts.richText != null) {
233
+ let parsed;
234
+ try {
235
+ parsed = JSON.parse(opts.richText);
236
+ }
237
+ catch {
238
+ throw new Error("Invalid --rich-text JSON.");
239
+ }
240
+ body.richText = parsed;
241
+ }
242
+ if (authorVaultSlug)
243
+ body.authorVaultSlug = authorVaultSlug;
244
+ if (opts.attach && opts.attach.length > 0) {
245
+ assertPostAttachmentMix(opts.attach);
246
+ body.attachments = await uploadPostAttachments(opts.attach);
247
+ }
248
+ if (opts.repostPostId != null) {
249
+ const n = Number(opts.repostPostId);
250
+ if (!Number.isFinite(n) || n <= 0 || !Number.isInteger(n)) {
251
+ throw new Error("--repost-post-id must be a positive integer.");
252
+ }
253
+ body.repostPostId = n;
254
+ }
255
+ const resp = (await apiPost(`/posts/personal-space`, body));
256
+ 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
+ if (isJsonMode(personal)) {
269
+ jsonOut(post);
270
+ return;
271
+ }
272
+ console.log(`Personal-space post created!\n` +
273
+ ` ID: ${post.id}\n` +
274
+ (post.title ? ` Title: ${post.title}\n` : "") +
275
+ ` Created: ${post.createdAt}\n` +
276
+ ` Visibility: private (only you can see this)` +
277
+ (opts.draftId ? `\n Linked to draft: ${opts.draftId}` : ""));
278
+ });
279
+ // ── Edit post ──
280
+ //
281
+ // Same `PATCH /posts/:postId` route the global command uses — the server
282
+ // gates on `authorId === userId` and the read-path guard runs first, so
283
+ // a non-owner can't edit (or even discover) a private post.
284
+ personal
285
+ .command("edit-post <postId>")
286
+ .description("Edit a post you authored in your personal space.")
287
+ .option("--title <title>", "New title")
288
+ .option("--content <content>", "New content (markdown supported, use \"-\" for stdin)")
289
+ .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
290
+ .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
+ .action(async (postId, opts) => {
293
+ const wantsVaultChange = !!(opts.vaultSlug || opts.autoAttachments);
294
+ if (opts.title == null &&
295
+ opts.content == null &&
296
+ opts.richText == null &&
297
+ !wantsVaultChange) {
298
+ throw new Error("Provide at least --title, --content, --rich-text, or --vault-slug to update.");
299
+ }
300
+ if (opts.content && opts.richText) {
301
+ throw new Error("--content and --rich-text are mutually exclusive.");
302
+ }
303
+ let authorVaultSlug;
304
+ if (opts.vaultSlug || opts.autoAttachments) {
305
+ authorVaultSlug = resolveVaultSlug(opts);
306
+ }
307
+ const body = {};
308
+ if (opts.title != null)
309
+ body.title = opts.title;
310
+ if (opts.content != null) {
311
+ const content = readContent(opts.content);
312
+ if (opts.autoAttachments && authorVaultSlug) {
313
+ const token = await getValidToken();
314
+ const links = extractWikiLinks(content);
315
+ await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
316
+ }
317
+ body.content = content;
318
+ }
319
+ if (opts.richText != null) {
320
+ let parsed;
321
+ try {
322
+ parsed = JSON.parse(opts.richText);
323
+ }
324
+ catch {
325
+ throw new Error("Invalid --rich-text JSON.");
326
+ }
327
+ body.richText = parsed;
328
+ }
329
+ if (authorVaultSlug !== undefined)
330
+ body.authorVaultSlug = authorVaultSlug;
331
+ const resp = (await apiPatch(`/posts/${postId}`, body));
332
+ const post = unwrapResp(resp);
333
+ if (isJsonMode(personal)) {
334
+ jsonOut(post);
335
+ return;
336
+ }
337
+ console.log(`Post edited!\n ID: ${post.id}\n Edited: ${post.editedAt ?? post.updatedAt}`);
338
+ });
339
+ // ── Delete post ──
340
+ personal
341
+ .command("delete-post <postId>")
342
+ .description("Delete a post you authored in your personal space.")
343
+ .action(async (postId) => {
344
+ await apiDelete(`/posts/${postId}`);
345
+ if (isJsonMode(personal)) {
346
+ jsonOut({ id: postId });
347
+ return;
348
+ }
349
+ console.log(`Post ${postId} deleted.`);
350
+ });
351
+ // ── Reply ──
352
+ //
353
+ // `POST /posts/:postId/replies` inherits scope from the parent on the
354
+ // server — reply to a personal-space parent → reply is scoped to that
355
+ // personal space. The same command works for global and personal; we
356
+ // expose it here for discoverability/symmetry with `gobi global`.
357
+ personal
358
+ .command("create-reply <postId>")
359
+ .description("Reply to a personal-space post. The reply inherits the parent's private scope automatically.")
360
+ .option("--content <content>", "Reply content (markdown supported, use \"-\" for stdin)")
361
+ .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
362
+ .option("--vault-slug <vaultSlug>", "Attribute the reply to this vault (sets authorVaultSlug). Also used as upload destination for --auto-attachments.")
363
+ .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before posting (also attributes the reply to that vault)")
364
+ .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], [])
365
+ .action(async (postId, opts) => {
366
+ if (!opts.content && !opts.richText) {
367
+ throw new Error("Provide either --content or --rich-text.");
368
+ }
369
+ if (opts.content && opts.richText) {
370
+ throw new Error("--content and --rich-text are mutually exclusive.");
371
+ }
372
+ let authorVaultSlug;
373
+ if (opts.vaultSlug || opts.autoAttachments) {
374
+ authorVaultSlug = resolveVaultSlug(opts);
375
+ }
376
+ const body = {};
377
+ if (opts.content != null) {
378
+ const content = readContent(opts.content);
379
+ if (opts.autoAttachments && authorVaultSlug) {
380
+ const token = await getValidToken();
381
+ const links = extractWikiLinks(content);
382
+ await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
383
+ }
384
+ body.content = content;
385
+ }
386
+ if (opts.richText != null) {
387
+ let parsed;
388
+ try {
389
+ parsed = JSON.parse(opts.richText);
390
+ }
391
+ catch {
392
+ throw new Error("Invalid --rich-text JSON.");
393
+ }
394
+ body.richText = parsed;
395
+ }
396
+ if (authorVaultSlug)
397
+ body.authorVaultSlug = authorVaultSlug;
398
+ if (opts.attach && opts.attach.length > 0) {
399
+ assertPostAttachmentMix(opts.attach);
400
+ body.attachments = await uploadPostAttachments(opts.attach);
401
+ }
402
+ const resp = (await apiPost(`/posts/${postId}/replies`, body));
403
+ const reply = unwrapResp(resp);
404
+ if (isJsonMode(personal)) {
405
+ jsonOut(reply);
406
+ return;
407
+ }
408
+ console.log(`Reply created!\n ID: ${reply.id}\n Created: ${reply.createdAt}`);
409
+ });
410
+ personal
411
+ .command("edit-reply <replyId>")
412
+ .description("Edit a reply you authored in your personal space.")
413
+ .option("--content <content>", "New reply content (markdown supported, use \"-\" for stdin)")
414
+ .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
415
+ .option("--vault-slug <vaultSlug>", "Attribute the reply to this vault (sets authorVaultSlug). Also used as upload destination for --auto-attachments.")
416
+ .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before editing (also attributes the reply to that vault)")
417
+ .action(async (replyId, opts) => {
418
+ const wantsVaultChange = !!(opts.vaultSlug || opts.autoAttachments);
419
+ if (opts.content == null && opts.richText == null && !wantsVaultChange) {
420
+ throw new Error("Provide at least --content, --rich-text, or --vault-slug to update.");
421
+ }
422
+ if (opts.content && opts.richText) {
423
+ throw new Error("--content and --rich-text are mutually exclusive.");
424
+ }
425
+ let authorVaultSlug;
426
+ if (opts.vaultSlug || opts.autoAttachments) {
427
+ authorVaultSlug = resolveVaultSlug(opts);
428
+ }
429
+ const body = {};
430
+ if (opts.content != null) {
431
+ const content = readContent(opts.content);
432
+ if (opts.autoAttachments && authorVaultSlug) {
433
+ const token = await getValidToken();
434
+ const links = extractWikiLinks(content);
435
+ await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
436
+ }
437
+ body.content = content;
438
+ }
439
+ if (opts.richText != null) {
440
+ let parsed;
441
+ try {
442
+ parsed = JSON.parse(opts.richText);
443
+ }
444
+ catch {
445
+ throw new Error("Invalid --rich-text JSON.");
446
+ }
447
+ body.richText = parsed;
448
+ }
449
+ if (authorVaultSlug !== undefined)
450
+ body.authorVaultSlug = authorVaultSlug;
451
+ const resp = (await apiPatch(`/posts/replies/${replyId}`, body));
452
+ const reply = unwrapResp(resp);
453
+ if (isJsonMode(personal)) {
454
+ jsonOut(reply);
455
+ return;
456
+ }
457
+ console.log(`Reply edited!\n ID: ${reply.id}\n Edited: ${reply.editedAt ?? reply.updatedAt}`);
458
+ });
459
+ personal
460
+ .command("delete-reply <replyId>")
461
+ .description("Delete a reply you authored in your personal space.")
462
+ .action(async (replyId) => {
463
+ await apiDelete(`/posts/replies/${replyId}`);
464
+ if (isJsonMode(personal)) {
465
+ jsonOut({ id: replyId });
466
+ return;
467
+ }
468
+ console.log(`Reply ${replyId} deleted.`);
469
+ });
470
+ }
package/dist/main.js CHANGED
@@ -6,9 +6,8 @@ import { registerAuthCommand } from "./commands/auth.js";
6
6
  import { commandRequiresSpace, commandRequiresVault, readSettings, } from "./commands/init.js";
7
7
  import { registerSpaceCommand } from "./commands/space.js";
8
8
  import { registerGlobalCommand } from "./commands/global.js";
9
+ import { registerPersonalCommand } from "./commands/personal.js";
9
10
  import { registerVaultCommand } from "./commands/vault.js";
10
- import { registerSavedCommand } from "./commands/saved.js";
11
- import { registerSessionsCommand } from "./commands/sessions.js";
12
11
  import { registerSenseCommand } from "./commands/sense.js";
13
12
  import { registerUpdateCommand } from "./commands/update.js";
14
13
  import { registerMediaCommand } from "./commands/media.js";
@@ -60,9 +59,8 @@ export async function cli() {
60
59
  registerAuthCommand(program);
61
60
  registerSpaceCommand(program);
62
61
  registerGlobalCommand(program);
62
+ registerPersonalCommand(program);
63
63
  registerVaultCommand(program);
64
- registerSavedCommand(program);
65
- registerSessionsCommand(program);
66
64
  registerSenseCommand(program);
67
65
  registerUpdateCommand(program);
68
66
  registerMediaCommand(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobi-ai/cli",
3
- "version": "2.0.20",
3
+ "version": "2.0.22",
4
4
  "description": "CLI client for the Gobi collaborative knowledge platform",
5
5
  "license": "MIT",
6
6
  "type": "module",