@gobi-ai/cli 1.0.0 → 1.2.0

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": "1.0.0",
7
+ "version": "1.2.0",
8
8
  "plugins": [
9
9
  {
10
10
  "name": "gobi",
11
- "description": "Manage the Gobi collaborative knowledge platform from the command line. Search and ask vaults, publish vault documents, create threads, manage sessions, generate images and videos.",
12
- "version": "1.0.0",
11
+ "description": "Manage the Gobi collaborative knowledge platform from the command line. Search and ask brains, publish brain documents, create threads, manage sessions, generate images and videos.",
12
+ "version": "1.2.0",
13
13
  "author": {
14
14
  "name": "gobi-ai"
15
15
  },
@@ -18,7 +18,8 @@
18
18
  "skills": [
19
19
  "./skills/gobi-core",
20
20
  "./skills/gobi-space",
21
- "./skills/gobi-vault",
21
+ "./skills/gobi-brain",
22
+ "./skills/gobi-feed",
22
23
  "./skills/gobi-media",
23
24
  "./skills/gobi-sense",
24
25
  "./skills/gobi-homepage"
@@ -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": "1.0.0",
4
+ "version": "1.2.0",
5
5
  "author": {
6
6
  "name": "gobi-ai"
7
7
  },
@@ -9,7 +9,8 @@
9
9
  "skills": [
10
10
  "./skills/gobi-core",
11
11
  "./skills/gobi-space",
12
- "./skills/gobi-vault",
12
+ "./skills/gobi-brain",
13
+ "./skills/gobi-feed",
13
14
  "./skills/gobi-media",
14
15
  "./skills/gobi-sense",
15
16
  "./skills/gobi-homepage"
package/README.md CHANGED
@@ -40,11 +40,11 @@ gobi init
40
40
  # Select a space
41
41
  gobi space warp
42
42
 
43
- # Search public vaults
44
- gobi vault search --query "machine learning"
43
+ # Search brains across your spaces
44
+ gobi brain search --query "machine learning"
45
45
 
46
- # Ask a vault a question
47
- gobi vault ask --vault-slug my-vault --question "What is RAG?"
46
+ # Ask a brain a question
47
+ gobi brain ask --vault-slug my-vault --question "What is RAG?"
48
48
  ```
49
49
 
50
50
  ## Commands
@@ -65,32 +65,40 @@ gobi vault ask --vault-slug my-vault --question "What is RAG?"
65
65
  | `gobi space list` | List spaces you are a member of |
66
66
  | `gobi space warp [spaceSlug]` | Select the active space (interactive if slug omitted) |
67
67
 
68
- ### Vaults
68
+ ### Brains
69
69
 
70
70
  | Command | Description |
71
71
  |---------|-------------|
72
- | `gobi vault search --query <q>` | Search public vaults by text and semantic similarity |
73
- | `gobi vault ask --vault-slug <slug> --question <q>` | Ask a vault a question (creates a 1:1 session) |
74
- | `gobi vault publish` | Upload `BRAIN.md` to your vault |
75
- | `gobi vault unpublish` | Remove `BRAIN.md` from your vault |
72
+ | `gobi brain search --query <q>` | Search public brains by text and semantic similarity |
73
+ | `gobi brain ask --vault-slug <slug> --question <q>` | Ask a brain a question (creates a 1:1 session) |
74
+ | `gobi brain publish` | Upload `BRAIN.md` to your vault |
75
+ | `gobi brain unpublish` | Remove `BRAIN.md` from your vault |
76
76
 
77
- Public vaults are accessible at `https://gobispace.com/@{vaultSlug}`.
77
+ Public brains are accessible at `https://gobispace.com/@{vaultSlug}`.
78
78
 
79
- `vault ask` also accepts `--rich-text <json>` (mutually exclusive with `--question`) and `--mode <auto|manual>`.
79
+ `brain ask` also accepts `--rich-text <json>` (mutually exclusive with `--question`) and `--mode <auto|manual>`.
80
80
 
81
- ### Spaces
81
+ ### Brain Updates
82
82
 
83
- > Space and member administration (creating spaces, inviting/approving members, joining/leaving) is web-UI only and not available in the CLI.
83
+ | Command | Description |
84
+ |---------|-------------|
85
+ | `gobi brain post-update --title <t> --content <c>` | Post a brain update |
86
+ | `gobi brain edit-update <id> [--title <t>] [--content <c>]` | Edit a brain update (at least one required) |
87
+ | `gobi brain delete-update <id>` | Delete a brain update |
88
+
89
+ `post-update` and `edit-update` accept `--auto-attachments` to upload wiki-linked `[[files]]` before posting.
90
+
91
+ ### Feed
84
92
 
85
93
  | Command | Description |
86
94
  |---------|-------------|
87
- | `gobi space get [spaceSlug]` | Show space details (uses current space if slug omitted) |
88
- | `gobi space messages` | Unified message feed (threads + replies, newest first) |
89
- | `gobi space ancestors <threadId>` | Walk a thread/reply's lineage from root immediate parent |
95
+ | `gobi feed list` | List recent brain updates from the global public feed |
96
+ | `gobi feed get <updateId>` | Get a feed brain update and its replies |
97
+ | `gobi feed reply <updateId> --content <c>` | Reply to a brain update in the feed |
90
98
 
91
- ### Threads
99
+ `feed list` and `feed get` accept `--limit`/`--cursor` for pagination.
92
100
 
93
- > **Migration note:** Brain-update commands have been removed. To post user-level content, use `gobi global create-thread` (platform-wide global) or `gobi space create-thread` (a specific space).
101
+ ### Threads
94
102
 
95
103
  | Command | Description |
96
104
  |---------|-------------|
@@ -108,18 +116,6 @@ Public vaults are accessible at `https://gobispace.com/@{vaultSlug}`.
108
116
  | `gobi space edit-reply <replyId> --content <c>` | Edit a reply |
109
117
  | `gobi space delete-reply <replyId>` | Delete a reply |
110
118
 
111
- ### Global thread space
112
-
113
- The global thread space is a slugless message feed visible across all spaces.
114
-
115
- | Command | Description |
116
- |---------|-------------|
117
- | `gobi global messages` | List the global unified message feed (newest first) |
118
- | `gobi global get-thread <id>` | Get a global thread and its direct replies |
119
- | `gobi global ancestors <id>` | Walk a global thread/reply's lineage |
120
- | `gobi global create-thread [--title <t>] (--content <c> \| --rich-text <json>)` | Create a thread in the global space |
121
- | `gobi global reply <threadId> (--content <c> \| --rich-text <json>)` | Reply to a global thread |
122
-
123
119
  ### Sessions
124
120
 
125
121
  | Command | Description |
@@ -164,7 +160,7 @@ Times are ISO 8601 UTC (e.g. `2026-03-20T00:00:00Z`).
164
160
  |--------|-------|-------------|
165
161
  | `--json` | All commands | Output results as JSON |
166
162
  | `--space-slug <slug>` | `space` commands | Override the default space (from `.gobi/settings.yaml`) |
167
- | `--vault-slug <slug>` | Per-command | Override the default vault; available on `space create-thread`, `space edit-thread`, `space edit-reply` |
163
+ | `--vault-slug <slug>` | Per-command | Override the default vault; available on `brain post-update`, `brain edit-update`, `space create-thread`, `space edit-thread`, `space edit-reply` |
168
164
 
169
165
  ## Configuration
170
166
 
@@ -8,11 +8,13 @@ Always use the globally installed `gobi` binary (not via npx or ts-node).
8
8
 
9
9
  Explore the active Gobi space to surface discussions, topics, and learnings from others:
10
10
 
11
- 1. Run these commands in parallel:
12
- - `gobi --json space messages` — unified feed of recent threads and replies in the space
11
+ 1. Run these three commands in parallel:
12
+ - `gobi --json space list-threads` — recent discussions in the space
13
13
  - `gobi --json space list-topics` — available topics across the platform
14
- 2. Display results in a readable summary, grouped by type (Topics / Discussions).
14
+ - `gobi --json feed list` learnings and brain updates shared by members across the platform
15
+ 2. Display results in a readable summary, grouped by type (Topics / Discussions / Learnings).
15
16
  3. If `$ARGUMENTS` is provided, filter or highlight entries relevant to that topic or keyword. If a matching topic is found, also run `gobi --json space list-topic-threads <topicSlug>` to show threads tagged with that topic.
16
17
  4. Ask the user if they'd like to read anything in full:
17
18
  - For a topic: run `gobi space list-topic-threads <topicSlug>` and show the threads.
18
19
  - For a thread: run `gobi space get-thread <threadId>` and show it with replies.
20
+ - For a feed brain update: run `gobi feed get <updateId>` to show the update with its replies.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: space-share
3
- description: Summarize recent learnings from this session and draft a thread to share to the active Gobi space.
3
+ description: Summarize recent learnings from this session and draft a brain update to share to the active Gobi space.
4
4
  argument-hint: "[context]"
5
5
  ---
6
6
 
@@ -16,7 +16,7 @@ gobi --json auth status
16
16
 
17
17
  Check that `.gobi/settings.yaml` exists and contains both `vaultSlug` and `selectedSpaceSlug`. If not warped, stop and ask the user to run `/gobi:warp` first.
18
18
 
19
- ## Draft a thread
19
+ ## Draft a brain update
20
20
 
21
21
  If `$ARGUMENTS` is provided, treat it as additional context or emphasis to guide the draft (e.g. "Emphasize the auth fix" or "Focus on the API design decision").
22
22
 
@@ -34,12 +34,12 @@ Focus on:
34
34
 
35
35
  ## Present to the user
36
36
 
37
- Format the draft as a short thread (2–5 bullet points max). Show it to the user and ask for confirmation before posting.
37
+ Format the draft as a short brain update (2–5 bullet points max). Show it to the user and ask for confirmation before posting.
38
38
 
39
- Once confirmed, post it as a thread in the active space:
39
+ Once confirmed, post it:
40
40
 
41
41
  ```bash
42
- gobi space create-thread --title "<short title>" --content "<confirmed content>"
42
+ gobi brain post-update --title "<short title>" --content "<confirmed content>"
43
43
  ```
44
44
 
45
45
  Confirm success and show the user the result.
@@ -0,0 +1,301 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { apiGet, apiPost, apiPatch, apiDelete } from "../client.js";
4
+ import { WEBDRIVE_BASE_URL } from "../constants.js";
5
+ import { getValidToken } from "../auth/manager.js";
6
+ import { getVaultSlug } from "./init.js";
7
+ import { isJsonMode, jsonOut, resolveVaultSlug, unwrapResp } from "./utils.js";
8
+ import { extractWikiLinks, uploadAttachments } from "../attachments.js";
9
+ export function registerBrainCommand(program) {
10
+ const brain = program
11
+ .command("brain")
12
+ .description("Brain commands (search, ask, publish, unpublish, updates).");
13
+ // ── Search ──
14
+ brain
15
+ .command("search")
16
+ .description("Search public brains by text and semantic similarity.")
17
+ .requiredOption("--query <query>", "Search query")
18
+ .action(async (opts) => {
19
+ const resp = (await apiGet(`/vault/public/search`, {
20
+ query: opts.query,
21
+ }));
22
+ const results = (Array.isArray(resp) ? resp : resp.data || resp);
23
+ if (isJsonMode(brain)) {
24
+ jsonOut(results || []);
25
+ return;
26
+ }
27
+ if (!results || results.length === 0) {
28
+ console.log(`No brains found matching "${opts.query}".`);
29
+ return;
30
+ }
31
+ const lines = [];
32
+ for (const entry of results) {
33
+ const vault = (entry.vault || entry);
34
+ const owner = (entry.owner || {});
35
+ const ownerName = owner.name ? ` by ${owner.name}` : "";
36
+ const sim = entry.similarity != null
37
+ ? ` [similarity: ${entry.similarity.toFixed(3)}]`
38
+ : "";
39
+ const spaceSlug = (entry.spaceSlug || vault.spaceSlug || "");
40
+ const vaultSlug = (vault.slug || vault.vaultSlug || vault.id || "N/A");
41
+ lines.push(`- ${vault.name || vault.title || "N/A"} (vault: ${vaultSlug}, space: ${spaceSlug || "N/A"})${ownerName}${sim}`);
42
+ }
43
+ console.log(`Brains matching "${opts.query}":\n` + lines.join("\n"));
44
+ });
45
+ // ── Ask ──
46
+ brain
47
+ .command("ask")
48
+ .description("Ask a brain a question. Creates a targeted session (1:1 conversation).")
49
+ .requiredOption("--vault-slug <vaultSlug>", "Slug of the brain/vault to ask")
50
+ .option("--question <question>", "The question to ask (markdown supported)")
51
+ .option("--rich-text <richText>", "Rich-text JSON array (e.g. [{\"type\":\"text\",\"text\":\"hello\"}])")
52
+ .option("--mode <mode>", 'Session mode: "auto" or "manual"')
53
+ .action(async (opts) => {
54
+ if (!opts.question && !opts.richText) {
55
+ throw new Error("Provide either --question or --rich-text.");
56
+ }
57
+ if (opts.question && opts.richText) {
58
+ throw new Error("--question and --rich-text are mutually exclusive.");
59
+ }
60
+ const body = {
61
+ vaultSlug: opts.vaultSlug,
62
+ };
63
+ if (opts.question != null)
64
+ body.question = opts.question;
65
+ if (opts.richText != null) {
66
+ let parsed;
67
+ try {
68
+ parsed = JSON.parse(opts.richText);
69
+ }
70
+ catch {
71
+ throw new Error("Invalid --rich-text JSON.");
72
+ }
73
+ body.richText = parsed;
74
+ }
75
+ if (opts.mode != null)
76
+ body.mode = opts.mode;
77
+ const resp = (await apiPost(`/chat/targeted`, body));
78
+ const data = unwrapResp(resp);
79
+ if (isJsonMode(brain)) {
80
+ jsonOut(data);
81
+ return;
82
+ }
83
+ const session = (data.session || {});
84
+ const members = (data.members || []);
85
+ console.log(`Session created!\n` +
86
+ ` Session ID: ${session.id}\n` +
87
+ ` Mode: ${session.mode}\n` +
88
+ ` Members: ${members.length}\n` +
89
+ ` Question sent.`);
90
+ });
91
+ // ── Publish ──
92
+ brain
93
+ .command("publish")
94
+ .description("Upload BRAIN.md to the vault root on webdrive. Triggers post-processing (brain sync, metadata update, Discord notification).")
95
+ .action(async () => {
96
+ const vaultId = getVaultSlug();
97
+ const filePath = join(process.cwd(), "BRAIN.md");
98
+ if (!existsSync(filePath)) {
99
+ throw new Error(`BRAIN.md not found in ${process.cwd()}`);
100
+ }
101
+ const content = readFileSync(filePath, "utf-8");
102
+ const token = await getValidToken();
103
+ const url = `${WEBDRIVE_BASE_URL}/api/v1/vaults/${vaultId}/file/BRAIN.md`;
104
+ const res = await fetch(url, {
105
+ method: "PUT",
106
+ headers: {
107
+ Authorization: `Bearer ${token}`,
108
+ "Content-Type": "text/markdown",
109
+ },
110
+ body: content,
111
+ });
112
+ if (!res.ok) {
113
+ throw new Error(`Upload failed: HTTP ${res.status}: ${(await res.text()) || "(no body)"}`);
114
+ }
115
+ if (isJsonMode(brain)) {
116
+ jsonOut({ vaultId });
117
+ return;
118
+ }
119
+ console.log(`Published BRAIN.md to vault "${vaultId}"`);
120
+ });
121
+ // ── Unpublish ──
122
+ brain
123
+ .command("unpublish")
124
+ .description("Delete BRAIN.md from the vault on webdrive.")
125
+ .action(async () => {
126
+ const vaultId = getVaultSlug();
127
+ const token = await getValidToken();
128
+ const url = `${WEBDRIVE_BASE_URL}/api/v1/vaults/${vaultId}/file/BRAIN.md`;
129
+ const res = await fetch(url, {
130
+ method: "DELETE",
131
+ headers: { Authorization: `Bearer ${token}` },
132
+ });
133
+ if (!res.ok) {
134
+ throw new Error(`Delete failed: HTTP ${res.status}: ${(await res.text()) || "(no body)"}`);
135
+ }
136
+ if (isJsonMode(brain)) {
137
+ jsonOut({ vaultId });
138
+ return;
139
+ }
140
+ console.log(`Deleted BRAIN.md from vault "${vaultId}"`);
141
+ });
142
+ // ── Updates (post, edit, delete) ──
143
+ brain
144
+ .command("post-update")
145
+ .description("Post a brain update for a vault.")
146
+ .option("--vault-slug <vaultSlug>", "Vault slug (overrides .gobi/settings.yaml)")
147
+ .requiredOption("--title <title>", "Title of the update")
148
+ .requiredOption("--content <content>", "Update content (markdown supported)")
149
+ .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before posting")
150
+ .action(async (opts) => {
151
+ const vaultSlug = resolveVaultSlug(opts);
152
+ if (opts.autoAttachments) {
153
+ const token = await getValidToken();
154
+ const links = extractWikiLinks(opts.content);
155
+ await uploadAttachments(vaultSlug, links, token, { addToSyncfiles: true });
156
+ }
157
+ const resp = (await apiPost(`/brain-updates/vault/${vaultSlug}`, {
158
+ title: opts.title,
159
+ content: opts.content,
160
+ }));
161
+ const u = unwrapResp(resp);
162
+ if (isJsonMode(brain)) {
163
+ jsonOut(u);
164
+ return;
165
+ }
166
+ console.log(`Brain update posted!\n` +
167
+ ` ID: ${u.id}\n` +
168
+ ` Title: ${u.title}\n` +
169
+ ` Vault: ${u.vaultSlug || vaultSlug}\n` +
170
+ ` Created: ${u.createdAt}`);
171
+ });
172
+ brain
173
+ .command("edit-update <updateId>")
174
+ .description("Edit a published brain update. You must be the author.")
175
+ .option("--title <title>", "New title for the update")
176
+ .option("--content <content>", "New content for the update (markdown supported)")
177
+ .option("--vault-slug <vaultSlug>", "Vault slug for attachment uploads (overrides .gobi/settings.yaml)")
178
+ .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before editing")
179
+ .action(async (updateId, opts) => {
180
+ if (!opts.title && !opts.content) {
181
+ throw new Error("Provide at least --title or --content to update.");
182
+ }
183
+ if (opts.autoAttachments && opts.content) {
184
+ const vaultSlug = resolveVaultSlug(opts);
185
+ const token = await getValidToken();
186
+ const links = extractWikiLinks(opts.content);
187
+ await uploadAttachments(vaultSlug, links, token, { addToSyncfiles: true });
188
+ }
189
+ const body = {};
190
+ if (opts.title != null)
191
+ body.title = opts.title;
192
+ if (opts.content != null)
193
+ body.content = opts.content;
194
+ const resp = (await apiPatch(`/brain-updates/${updateId}`, body));
195
+ const u = unwrapResp(resp);
196
+ if (isJsonMode(brain)) {
197
+ jsonOut(u);
198
+ return;
199
+ }
200
+ console.log(`Brain update edited!\n` +
201
+ ` ID: ${u.id}\n` +
202
+ ` Title: ${u.title}\n` +
203
+ ` Updated: ${u.updatedAt}`);
204
+ });
205
+ brain
206
+ .command("delete-update <updateId>")
207
+ .description("Delete a published brain update. You must be the author.")
208
+ .action(async (updateId) => {
209
+ await apiDelete(`/brain-updates/${updateId}`);
210
+ if (isJsonMode(brain)) {
211
+ jsonOut({ id: updateId });
212
+ return;
213
+ }
214
+ console.log(`Brain update ${updateId} deleted.`);
215
+ });
216
+ // ── Update Replies (get-update, reply-to-update, edit-update-reply, delete-update-reply) ──
217
+ brain
218
+ .command("get-update <updateId>")
219
+ .description("Get a brain update and its replies (paginated).")
220
+ .option("--limit <number>", "Replies per page", "20")
221
+ .option("--cursor <string>", "Pagination cursor from previous response")
222
+ .option("--full", "Show full reply content without truncation")
223
+ .action(async (updateId, opts) => {
224
+ const params = {
225
+ limit: parseInt(opts.limit, 10),
226
+ };
227
+ if (opts.cursor)
228
+ params.cursor = opts.cursor;
229
+ const resp = (await apiGet(`/brain-updates/${updateId}`, params));
230
+ const data = unwrapResp(resp);
231
+ const pagination = (resp.pagination || {});
232
+ if (isJsonMode(brain)) {
233
+ jsonOut({ ...data, pagination });
234
+ return;
235
+ }
236
+ const update = (data.update || data);
237
+ const replies = (data.replies || []);
238
+ const author = update.author?.name ||
239
+ `User ${update.authorId}`;
240
+ const vault = update.vault?.vaultSlug || "?";
241
+ const replyLines = [];
242
+ for (const r of replies) {
243
+ const rAuthor = r.author?.name ||
244
+ `User ${r.authorId}`;
245
+ const text = r.content;
246
+ const truncated = opts.full || text.length <= 200 ? text : text.slice(0, 200) + "\u2026";
247
+ replyLines.push(` - ${rAuthor}: ${truncated} (${r.createdAt})`);
248
+ }
249
+ const output = [
250
+ `Brain Update: ${update.title || "(no title)"}`,
251
+ `By: ${author} (vault: ${vault}) on ${update.createdAt}`,
252
+ "",
253
+ update.content,
254
+ "",
255
+ `Replies (${replies.length} items):`,
256
+ ...replyLines,
257
+ ...(pagination.hasMore
258
+ ? [` Next cursor: ${pagination.nextCursor}`]
259
+ : []),
260
+ ].join("\n");
261
+ console.log(output);
262
+ });
263
+ brain
264
+ .command("reply-to-update <updateId>")
265
+ .description("Reply to a brain update.")
266
+ .requiredOption("--content <content>", 'Reply content (markdown supported, use "-" for stdin)')
267
+ .action(async (updateId, opts) => {
268
+ const content = opts.content === "-" ? readFileSync("/dev/stdin", "utf8") : opts.content;
269
+ const resp = (await apiPost(`/brain-updates/${updateId}/replies`, { content }));
270
+ const reply = unwrapResp(resp);
271
+ if (isJsonMode(brain)) {
272
+ jsonOut(reply);
273
+ return;
274
+ }
275
+ console.log(`Reply created!\n ID: ${reply.id}\n Created: ${reply.createdAt}`);
276
+ });
277
+ brain
278
+ .command("edit-update-reply <replyId>")
279
+ .description("Edit a brain update reply. You must be the author.")
280
+ .requiredOption("--content <content>", "New content for the reply (markdown supported)")
281
+ .action(async (replyId, opts) => {
282
+ const resp = (await apiPatch(`/brain-updates/replies/${replyId}`, { content: opts.content }));
283
+ const reply = unwrapResp(resp);
284
+ if (isJsonMode(brain)) {
285
+ jsonOut(reply);
286
+ return;
287
+ }
288
+ console.log(`Reply edited!\n ID: ${reply.id}\n Edited: ${reply.editedAt}`);
289
+ });
290
+ brain
291
+ .command("delete-update-reply <replyId>")
292
+ .description("Delete a brain update reply. You must be the author.")
293
+ .action(async (replyId) => {
294
+ await apiDelete(`/brain-updates/replies/${replyId}`);
295
+ if (isJsonMode(brain)) {
296
+ jsonOut({ replyId });
297
+ return;
298
+ }
299
+ console.log(`Brain update reply ${replyId} deleted.`);
300
+ });
301
+ }
@@ -0,0 +1,116 @@
1
+ import { readFileSync } from "fs";
2
+ import { apiGet, apiPost } from "../client.js";
3
+ import { isJsonMode, jsonOut, unwrapResp } from "./utils.js";
4
+ export function registerFeedCommand(program) {
5
+ const feed = program
6
+ .command("feed")
7
+ .description("Feed of brain updates from people across the platform.");
8
+ // ── List ──
9
+ feed
10
+ .command("list")
11
+ .description("List recent brain updates from the global public feed.")
12
+ .option("--limit <number>", "Items per page", "20")
13
+ .option("--cursor <string>", "Pagination cursor from previous response")
14
+ .action(async (opts) => {
15
+ const params = {
16
+ limit: parseInt(opts.limit, 10),
17
+ };
18
+ if (opts.cursor)
19
+ params.cursor = opts.cursor;
20
+ const resp = (await apiGet(`/feed`, params));
21
+ if (isJsonMode(feed)) {
22
+ jsonOut({
23
+ items: resp.data || [],
24
+ pagination: resp.pagination || {},
25
+ });
26
+ return;
27
+ }
28
+ const items = (resp.data || []);
29
+ const pagination = (resp.pagination || {});
30
+ if (!items.length) {
31
+ console.log("No feed items found.");
32
+ return;
33
+ }
34
+ const lines = [];
35
+ for (const u of items) {
36
+ const author = u.author?.name ||
37
+ `User ${u.authorId}`;
38
+ const vaultSlug = u.vault?.vaultSlug ||
39
+ u.authorVault?.vaultSlug ||
40
+ "?";
41
+ const replyCount = u.replyCount ?? 0;
42
+ const replies = replyCount ? `, ${replyCount} ${replyCount === 1 ? "reply" : "replies"}` : "";
43
+ lines.push(`- [${u.id}] "${u.title}" by ${author} (vault: ${vaultSlug}, ${u.createdAt}${replies})`);
44
+ }
45
+ const footer = pagination.hasMore
46
+ ? `\n Next cursor: ${pagination.nextCursor}`
47
+ : "";
48
+ console.log(`Feed (${items.length} items):\n` + lines.join("\n") + footer);
49
+ });
50
+ // ── Get ──
51
+ feed
52
+ .command("get <updateId>")
53
+ .description("Get a feed brain update and its replies (paginated).")
54
+ .option("--limit <number>", "Replies per page", "20")
55
+ .option("--cursor <string>", "Pagination cursor from previous response")
56
+ .option("--full", "Show full reply content without truncation")
57
+ .action(async (updateId, opts) => {
58
+ const params = {
59
+ limit: parseInt(opts.limit, 10),
60
+ };
61
+ if (opts.cursor)
62
+ params.cursor = opts.cursor;
63
+ const resp = (await apiGet(`/feed/${updateId}`, params));
64
+ const data = unwrapResp(resp);
65
+ const pagination = (resp.pagination || {});
66
+ if (isJsonMode(feed)) {
67
+ jsonOut({ ...data, pagination });
68
+ return;
69
+ }
70
+ const update = (data.update || data);
71
+ const replies = (data.replies || []);
72
+ const author = update.author?.name ||
73
+ `User ${update.authorId}`;
74
+ const vault = update.vault?.vaultSlug ||
75
+ update.authorVault?.vaultSlug ||
76
+ "?";
77
+ const replyLines = [];
78
+ for (const r of replies) {
79
+ const rAuthor = r.author?.name ||
80
+ `User ${r.authorId}`;
81
+ const text = r.content || "";
82
+ const truncated = opts.full || text.length <= 200 ? text : text.slice(0, 200) + "…";
83
+ replyLines.push(` - ${rAuthor}: ${truncated} (${r.createdAt})`);
84
+ }
85
+ const output = [
86
+ `Feed Update: ${update.title || "(no title)"}`,
87
+ `By: ${author} (vault: ${vault}) on ${update.createdAt}`,
88
+ "",
89
+ update.content || "",
90
+ "",
91
+ `Replies (${replies.length} items):`,
92
+ ...replyLines,
93
+ ...(pagination.hasMore
94
+ ? [` Next cursor: ${pagination.nextCursor}`]
95
+ : []),
96
+ ].join("\n");
97
+ console.log(output);
98
+ });
99
+ // ── Reply ──
100
+ feed
101
+ .command("reply <updateId>")
102
+ .description("Reply to a brain update in the feed.")
103
+ .requiredOption("--content <content>", 'Reply content (markdown supported, use "-" for stdin)')
104
+ .action(async (updateId, opts) => {
105
+ const content = opts.content === "-" ? readFileSync("/dev/stdin", "utf8") : opts.content;
106
+ const resp = (await apiPost(`/feed/${updateId}/replies`, {
107
+ content,
108
+ }));
109
+ const reply = unwrapResp(resp);
110
+ if (isJsonMode(feed)) {
111
+ jsonOut(reply);
112
+ return;
113
+ }
114
+ console.log(`Reply created!\n ID: ${reply.id}\n Created: ${reply.createdAt}`);
115
+ });
116
+ }