@gobi-ai/cli 1.3.8 → 2.0.1

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.
Files changed (38) hide show
  1. package/.claude-plugin/marketplace.json +5 -6
  2. package/.claude-plugin/plugin.json +3 -4
  3. package/README.md +174 -82
  4. package/commands/space-explore.md +10 -10
  5. package/commands/space-share.md +13 -7
  6. package/dist/attachments.js +2 -1
  7. package/dist/commands/draft.js +2 -3
  8. package/dist/commands/global.js +212 -72
  9. package/dist/commands/init.js +5 -5
  10. package/dist/commands/{notes.js → saved.js} +115 -23
  11. package/dist/commands/space.js +121 -111
  12. package/dist/commands/sync.js +2 -56
  13. package/dist/commands/update.js +14 -8
  14. package/dist/commands/utils.js +6 -0
  15. package/dist/commands/vault.js +113 -0
  16. package/dist/main.js +4 -8
  17. package/package.json +2 -2
  18. package/skills/gobi-core/SKILL.md +19 -15
  19. package/skills/gobi-core/references/space.md +18 -19
  20. package/skills/gobi-draft/SKILL.md +3 -3
  21. package/skills/gobi-homepage/SKILL.md +21 -19
  22. package/skills/gobi-media/SKILL.md +2 -2
  23. package/skills/gobi-saved/SKILL.md +59 -0
  24. package/skills/gobi-saved/references/saved.md +52 -0
  25. package/skills/gobi-sense/SKILL.md +8 -4
  26. package/skills/gobi-space/SKILL.md +55 -38
  27. package/skills/gobi-space/references/global.md +87 -26
  28. package/skills/gobi-space/references/space.md +49 -61
  29. package/skills/gobi-vault/SKILL.md +92 -0
  30. package/skills/{gobi-core/references/sync.md → gobi-vault/references/vault.md} +41 -2
  31. package/dist/commands/brain.js +0 -141
  32. package/dist/commands/feed.js +0 -148
  33. package/skills/gobi-brain/SKILL.md +0 -100
  34. package/skills/gobi-brain/references/brain.md +0 -66
  35. package/skills/gobi-feed/SKILL.md +0 -43
  36. package/skills/gobi-feed/references/feed.md +0 -80
  37. package/skills/gobi-notes/SKILL.md +0 -52
  38. package/skills/gobi-notes/references/notes.md +0 -82
@@ -1,15 +1,17 @@
1
- import { readFileSync } from "fs";
2
- import { apiGet, apiPost } from "../client.js";
3
- import { isJsonMode, jsonOut, unwrapResp } from "./utils.js";
1
+ import { apiGet, apiPost, apiPatch, apiDelete } from "../client.js";
2
+ import { isJsonMode, jsonOut, readStdin, resolveVaultSlug, unwrapResp, } from "./utils.js";
3
+ import { extractWikiLinks, uploadAttachments } from "../attachments.js";
4
+ import { getValidToken } from "../auth/manager.js";
4
5
  function readContent(value) {
5
6
  if (value === "-")
6
- return readFileSync("/dev/stdin", "utf8");
7
+ return readStdin();
7
8
  return value;
8
9
  }
9
- function formatMessageLine(m) {
10
- const isReply = m.parentThreadId != null;
11
- const id = `[${isReply ? "r" : "t"}:${m.id}]`;
12
- const kind = isReply ? "reply " : "thread";
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 ";
13
15
  const author = m.author?.name ||
14
16
  `User ${m.authorId ?? "?"}`;
15
17
  let label;
@@ -26,20 +28,23 @@ function formatMessageLine(m) {
26
28
  export function registerGlobalCommand(program) {
27
29
  const global = program
28
30
  .command("global")
29
- .description("Global thread space commands (no slug; visible across all spaces).");
30
- // ── Messages (unified feed) ──
31
+ .description("Global commands (posts and replies in the public feed across all vaults).");
32
+ // ── Feed (unified) ──
31
33
  global
32
- .command("messages")
33
- .description("List the global unified message feed (threads and replies, newest first).")
34
+ .command("feed")
35
+ .description("List the global public feed (posts and replies, newest first).")
34
36
  .option("--limit <number>", "Items per page", "20")
35
37
  .option("--cursor <string>", "Pagination cursor from previous response")
38
+ .option("--following", "Only include posts from authors you follow")
36
39
  .action(async (opts) => {
37
40
  const params = {
38
41
  limit: parseInt(opts.limit, 10),
39
42
  };
40
43
  if (opts.cursor)
41
44
  params.cursor = opts.cursor;
42
- const resp = (await apiGet(`/global/messages`, params));
45
+ if (opts.following)
46
+ params.following = "true";
47
+ const resp = (await apiGet(`/feed`, params));
43
48
  if (isJsonMode(global)) {
44
49
  jsonOut({
45
50
  items: resp.data || [],
@@ -50,92 +55,189 @@ export function registerGlobalCommand(program) {
50
55
  const items = (resp.data || []);
51
56
  const pagination = (resp.pagination || {});
52
57
  if (!items.length) {
53
- console.log("No messages found.");
58
+ console.log("No items found.");
54
59
  return;
55
60
  }
56
- const lines = items.map(formatMessageLine);
61
+ const lines = items.map(formatFeedLine);
57
62
  const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
58
- console.log(`Global messages (${items.length} items, newest first):\n` + lines.join("\n") + footer);
63
+ console.log(`Global feed (${items.length} items, newest first):\n` + lines.join("\n") + footer);
59
64
  });
60
- // ── Get thread ──
65
+ // ── List posts ──
61
66
  global
62
- .command("get-thread <threadId>")
63
- .description("Get a global thread and its direct replies (paginated).")
64
- .option("--limit <number>", "Replies per page", "20")
67
+ .command("list-posts")
68
+ .description("List posts in the global feed (paginated). Pass --mine to limit to your own posts.")
69
+ .option("--limit <number>", "Items per page", "20")
65
70
  .option("--cursor <string>", "Pagination cursor from previous response")
66
- .action(async (threadId, opts) => {
71
+ .option("--mine", "Only include posts authored by you")
72
+ .option("--vault-slug <vaultSlug>", "Filter by author vault slug")
73
+ .action(async (opts) => {
67
74
  const params = {
68
75
  limit: parseInt(opts.limit, 10),
69
76
  };
70
77
  if (opts.cursor)
71
78
  params.cursor = opts.cursor;
72
- const resp = (await apiGet(`/global/threads/${threadId}`, params));
73
- const data = unwrapResp(resp);
79
+ if (opts.mine)
80
+ params.mine = "true";
81
+ if (opts.vaultSlug)
82
+ params.vaultSlug = opts.vaultSlug;
83
+ const resp = (await apiGet(`/posts`, params));
84
+ if (isJsonMode(global)) {
85
+ jsonOut({
86
+ items: resp.data || [],
87
+ pagination: resp.pagination || {},
88
+ });
89
+ return;
90
+ }
91
+ const items = (resp.data || []);
74
92
  const pagination = (resp.pagination || {});
93
+ if (!items.length) {
94
+ console.log("No posts found.");
95
+ return;
96
+ }
97
+ const lines = [];
98
+ for (const t of items) {
99
+ const author = t.author?.name ||
100
+ `User ${t.authorId}`;
101
+ const vaultSlug = t.vault?.vaultSlug ||
102
+ t.authorVault?.vaultSlug ||
103
+ "?";
104
+ lines.push(`- [${t.id}] "${t.title}" by ${author} (vault: ${vaultSlug}, ${t.replyCount ?? 0} replies, ${t.createdAt})`);
105
+ }
106
+ const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
107
+ console.log(`Posts (${items.length} items):\n` + lines.join("\n") + footer);
108
+ });
109
+ // ── Get post (with ancestors and replies) ──
110
+ global
111
+ .command("get-post <postId>")
112
+ .description("Get a global post with its ancestors and replies (paginated).")
113
+ .option("--limit <number>", "Replies per page", "20")
114
+ .option("--cursor <string>", "Pagination cursor from previous response")
115
+ .option("--full", "Show full reply content without truncation")
116
+ .action(async (postId, opts) => {
117
+ const params = {
118
+ limit: parseInt(opts.limit, 10),
119
+ };
120
+ if (opts.cursor)
121
+ params.cursor = opts.cursor;
122
+ const [postResp, ancestorsResp] = await Promise.all([
123
+ apiGet(`/feed/${postId}`, params),
124
+ apiGet(`/feed/${postId}/ancestors`),
125
+ ]);
126
+ const data = unwrapResp(postResp);
127
+ const pagination = (postResp.pagination || {});
128
+ const mentions = (postResp.mentions || {});
129
+ const ancestorsData = unwrapResp(ancestorsResp);
130
+ const ancestors = (ancestorsData.ancestors || []);
75
131
  if (isJsonMode(global)) {
76
- jsonOut({ ...data, pagination });
132
+ jsonOut({ ...data, ancestors, pagination, mentions });
77
133
  return;
78
134
  }
79
- const thread = (data.thread || data);
80
- const replies = (data.items || []);
81
- const author = thread.author?.name ||
82
- `User ${thread.authorId}`;
135
+ const post = (data.update || data.post || data);
136
+ const replies = (data.replies || []);
137
+ const author = post.author?.name ||
138
+ `User ${post.authorId}`;
139
+ const vault = post.vault?.vaultSlug ||
140
+ post.authorVault?.vaultSlug ||
141
+ "?";
142
+ const ancestorLines = [];
143
+ if (ancestors.length) {
144
+ ancestors.forEach((a, i) => {
145
+ ancestorLines.push(` ${i + 1}. ${formatFeedLine(a)}`);
146
+ });
147
+ }
83
148
  const replyLines = [];
84
149
  for (const r of replies) {
85
150
  const rAuthor = r.author?.name ||
86
151
  `User ${r.authorId}`;
87
152
  const text = r.content || "";
88
- const truncated = text.length > 200 ? text.slice(0, 200) + "…" : text;
153
+ const truncated = opts.full || text.length <= 200 ? text : text.slice(0, 200) + "…";
89
154
  replyLines.push(` - ${rAuthor}: ${truncated} (${r.createdAt})`);
90
155
  }
91
- const isReply = thread.parentThreadId != null;
92
- const heading = isReply
93
- ? `Reply [r:${thread.id}]`
94
- : `Thread: ${thread.title || "(no title)"}`;
156
+ const isReplyPost = post.parentPostId != null;
157
+ const heading = isReplyPost
158
+ ? `Reply [r:${post.id}]`
159
+ : `Post: ${post.title || "(no title)"}`;
95
160
  const output = [
96
161
  heading,
97
- `By: ${author} on ${thread.createdAt}`,
162
+ `By: ${author} (vault: ${vault}) on ${post.createdAt}`,
163
+ ...(ancestorLines.length
164
+ ? ["", `Ancestors (${ancestors.length} items, root first):`, ...ancestorLines]
165
+ : []),
98
166
  "",
99
- thread.content,
167
+ post.content || "",
100
168
  "",
101
169
  `Replies (${replies.length} items):`,
102
170
  ...replyLines,
103
- ...(pagination.hasMore ? [` Next cursor: ${pagination.nextCursor}`] : []),
171
+ ...(pagination.hasMore
172
+ ? [` Next cursor: ${pagination.nextCursor}`]
173
+ : []),
104
174
  ].join("\n");
105
175
  console.log(output);
106
176
  });
107
- // ── Ancestors ──
177
+ // ── Create post ──
108
178
  global
109
- .command("ancestors <threadId>")
110
- .description("Show the ancestor lineage of a global thread or reply (root immediate parent).")
111
- .action(async (threadId) => {
112
- const resp = (await apiGet(`/global/threads/${threadId}/ancestors`));
113
- const data = unwrapResp(resp);
114
- const ancestors = (data.ancestors || []);
115
- if (isJsonMode(global)) {
116
- jsonOut({ ancestors });
117
- return;
179
+ .command("create-post")
180
+ .description("Create a post in the global feed (publishes from your vault).")
181
+ .option("--title <title>", "Title of the post")
182
+ .option("--content <content>", "Post content (markdown supported, use \"-\" for stdin)")
183
+ .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
184
+ .option("--vault-slug <vaultSlug>", "Author vault slug (overrides .gobi/settings.yaml)")
185
+ .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before posting")
186
+ .action(async (opts) => {
187
+ if (!opts.content && !opts.richText) {
188
+ throw new Error("Provide either --content or --rich-text.");
189
+ }
190
+ if (opts.content && opts.richText) {
191
+ throw new Error("--content and --rich-text are mutually exclusive.");
118
192
  }
119
- if (!ancestors.length) {
120
- console.log("No ancestors (this is a root thread).");
193
+ const vaultSlug = resolveVaultSlug(opts);
194
+ const body = {};
195
+ if (opts.title != null)
196
+ body.title = opts.title;
197
+ if (opts.content != null) {
198
+ const content = readContent(opts.content);
199
+ if (opts.autoAttachments) {
200
+ const token = await getValidToken();
201
+ const links = extractWikiLinks(content);
202
+ await uploadAttachments(vaultSlug, links, token, { addToSyncfiles: true });
203
+ }
204
+ body.content = content;
205
+ }
206
+ if (opts.richText != null) {
207
+ let parsed;
208
+ try {
209
+ parsed = JSON.parse(opts.richText);
210
+ }
211
+ catch {
212
+ throw new Error("Invalid --rich-text JSON.");
213
+ }
214
+ body.richText = parsed;
215
+ }
216
+ const resp = (await apiPost(`/posts/vault/${vaultSlug}`, body));
217
+ const post = unwrapResp(resp);
218
+ if (isJsonMode(global)) {
219
+ jsonOut(post);
121
220
  return;
122
221
  }
123
- const lines = [];
124
- ancestors.forEach((a, i) => {
125
- lines.push(`${i + 1}. ${formatMessageLine(a)}`);
126
- });
127
- console.log(`Ancestors (${ancestors.length} items, root first):\n` + lines.join("\n"));
222
+ console.log(`Post created!\n` +
223
+ ` ID: ${post.id}\n` +
224
+ (post.title ? ` Title: ${post.title}\n` : "") +
225
+ ` Created: ${post.createdAt}`);
128
226
  });
129
- // ── Create thread ──
227
+ // ── Edit post ──
130
228
  global
131
- .command("create-thread")
132
- .description("Create a thread in the global space.")
133
- .option("--title <title>", "Title of the thread")
134
- .option("--content <content>", "Thread content (markdown supported, use \"-\" for stdin)")
229
+ .command("edit-post <postId>")
230
+ .description("Edit a post you authored in the global feed.")
231
+ .option("--title <title>", "New title")
232
+ .option("--content <content>", "New content (markdown supported, use \"-\" for stdin)")
135
233
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
136
- .action(async (opts) => {
137
- if (!opts.content && !opts.richText) {
138
- throw new Error("Provide either --content or --rich-text.");
234
+ .option("--vault-slug <vaultSlug>", "Attribute the post to this vault (sets authorVaultId). Pass an empty string to detach.")
235
+ .action(async (postId, opts) => {
236
+ if (opts.title == null &&
237
+ opts.content == null &&
238
+ opts.richText == null &&
239
+ opts.vaultSlug === undefined) {
240
+ throw new Error("Provide at least --title, --content, --rich-text, or --vault-slug to update.");
139
241
  }
140
242
  if (opts.content && opts.richText) {
141
243
  throw new Error("--content and --rich-text are mutually exclusive.");
@@ -155,24 +257,35 @@ export function registerGlobalCommand(program) {
155
257
  }
156
258
  body.richText = parsed;
157
259
  }
158
- const resp = (await apiPost(`/global/threads`, body));
159
- const thread = unwrapResp(resp);
260
+ if (opts.vaultSlug !== undefined)
261
+ body.authorVaultSlug = opts.vaultSlug;
262
+ const resp = (await apiPatch(`/posts/${postId}`, body));
263
+ const post = unwrapResp(resp);
160
264
  if (isJsonMode(global)) {
161
- jsonOut(thread);
265
+ jsonOut(post);
162
266
  return;
163
267
  }
164
- console.log(`Global thread created!\n` +
165
- ` ID: ${thread.id}\n` +
166
- (thread.title ? ` Title: ${thread.title}\n` : "") +
167
- ` Created: ${thread.createdAt}`);
268
+ console.log(`Post edited!\n ID: ${post.id}\n Edited: ${post.editedAt ?? post.updatedAt}`);
269
+ });
270
+ // ── Delete post ──
271
+ global
272
+ .command("delete-post <postId>")
273
+ .description("Delete a post you authored in the global feed.")
274
+ .action(async (postId) => {
275
+ await apiDelete(`/posts/${postId}`);
276
+ if (isJsonMode(global)) {
277
+ jsonOut({ id: postId });
278
+ return;
279
+ }
280
+ console.log(`Post ${postId} deleted.`);
168
281
  });
169
282
  // ── Reply ──
170
283
  global
171
- .command("reply <threadId>")
172
- .description("Reply to a thread in the global space.")
284
+ .command("create-reply <postId>")
285
+ .description("Reply to a post in the global feed.")
173
286
  .option("--content <content>", "Reply content (markdown supported, use \"-\" for stdin)")
174
287
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
175
- .action(async (threadId, opts) => {
288
+ .action(async (postId, opts) => {
176
289
  if (!opts.content && !opts.richText) {
177
290
  throw new Error("Provide either --content or --rich-text.");
178
291
  }
@@ -192,7 +305,7 @@ export function registerGlobalCommand(program) {
192
305
  }
193
306
  body.richText = parsed;
194
307
  }
195
- const resp = (await apiPost(`/global/threads/${threadId}/replies`, body));
308
+ const resp = (await apiPost(`/posts/${postId}/replies`, body));
196
309
  const reply = unwrapResp(resp);
197
310
  if (isJsonMode(global)) {
198
311
  jsonOut(reply);
@@ -200,4 +313,31 @@ export function registerGlobalCommand(program) {
200
313
  }
201
314
  console.log(`Reply created!\n ID: ${reply.id}\n Created: ${reply.createdAt}`);
202
315
  });
316
+ global
317
+ .command("edit-reply <replyId>")
318
+ .description("Edit a reply you authored in the global feed.")
319
+ .requiredOption("--content <content>", "New reply content (markdown supported, use \"-\" for stdin)")
320
+ .action(async (replyId, opts) => {
321
+ const content = readContent(opts.content);
322
+ const resp = (await apiPatch(`/posts/replies/${replyId}`, {
323
+ content,
324
+ }));
325
+ const reply = unwrapResp(resp);
326
+ if (isJsonMode(global)) {
327
+ jsonOut(reply);
328
+ return;
329
+ }
330
+ console.log(`Reply edited!\n ID: ${reply.id}\n Edited: ${reply.editedAt ?? reply.updatedAt}`);
331
+ });
332
+ global
333
+ .command("delete-reply <replyId>")
334
+ .description("Delete a reply you authored in the global feed.")
335
+ .action(async (replyId) => {
336
+ await apiDelete(`/posts/replies/${replyId}`);
337
+ if (isJsonMode(global)) {
338
+ jsonOut({ replyId });
339
+ return;
340
+ }
341
+ console.log(`Reply ${replyId} deleted.`);
342
+ });
203
343
  }
@@ -205,11 +205,11 @@ export async function runInitFlow() {
205
205
  writeVaultSetting(vaultId);
206
206
  console.log(`Vault set to "${vaultName}" (${vaultId})`);
207
207
  console.log(`Updated ${SETTINGS_DIR}/${SETTINGS_FILE}`);
208
- // Create default BRAIN.md if it doesn't exist
209
- const brainPath = join(process.cwd(), "BRAIN.md");
210
- if (!existsSync(brainPath)) {
211
- writeFileSync(brainPath, `---\ntitle: ${vaultName}\ntags: []\ndescription:\nthumbnail:\nprompt:\n---\n`, "utf-8");
212
- console.log("Created BRAIN.md");
208
+ // Create default PUBLISH.md if it doesn't exist
209
+ const publishPath = join(process.cwd(), "PUBLISH.md");
210
+ if (!existsSync(publishPath)) {
211
+ writeFileSync(publishPath, `---\ntitle: ${vaultName}\ntags: []\ndescription:\nthumbnail:\nprompt:\n---\n`, "utf-8");
212
+ console.log("Created PUBLISH.md");
213
213
  }
214
214
  }
215
215
  export function registerInitCommand(program) {
@@ -1,6 +1,5 @@
1
- import { readFileSync } from "fs";
2
1
  import { apiGet, apiPost, apiPatch, apiDelete } from "../client.js";
3
- import { isJsonMode, jsonOut, unwrapResp } from "./utils.js";
2
+ import { isJsonMode, jsonOut, readStdin, unwrapResp } from "./utils.js";
4
3
  function defaultTimezone() {
5
4
  return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
6
5
  }
@@ -18,12 +17,18 @@ function formatNoteLine(note) {
18
17
  const agent = note.agentId != null ? `, agent: ${note.agentId}` : "";
19
18
  return `- [${note.id}] "${snippet.replace(/\n/g, " ")}" (${note.eventDate}${agent}${attachStr}, updated ${note.updatedAt})`;
20
19
  }
21
- export function registerNotesCommand(program) {
22
- const notes = program
23
- .command("notes")
24
- .description("Personal notes (create, list, get, edit, delete).");
25
- // ── List ──
26
- notes
20
+ function formatSavedPostLine(item) {
21
+ const author = item.author?.name;
22
+ const title = item.title || item.content || "(no title)";
23
+ const snippet = title.length > 80 ? title.slice(0, 80) + "…" : title;
24
+ const space = item.spaceSlug ? `, space: ${item.spaceSlug}` : "";
25
+ return `- [${item.postId}] "${snippet.replace(/\n/g, " ")}" by ${author ?? "?"}${space} (saved ${item.savedAt})`;
26
+ }
27
+ function registerNoteCommands(saved) {
28
+ const note = saved
29
+ .command("note")
30
+ .description("Personal saved notes (create, list, get, edit, delete).");
31
+ note
27
32
  .command("list")
28
33
  .description("List your notes. Without --date, returns recent notes via cursor pagination. With --date, returns all notes for that day.")
29
34
  .option("--date <date>", "Filter to a single day (YYYY-MM-DD)")
@@ -42,7 +47,7 @@ export function registerNotesCommand(program) {
42
47
  const resp = (await apiGet(`/app/notes`, params));
43
48
  const items = (resp.data || []);
44
49
  const pagination = (resp.pagination || {});
45
- if (isJsonMode(notes)) {
50
+ if (isJsonMode(saved)) {
46
51
  jsonOut({ items, pagination });
47
52
  return;
48
53
  }
@@ -56,14 +61,13 @@ export function registerNotesCommand(program) {
56
61
  : "";
57
62
  console.log(`Notes (${items.length} items):\n` + lines.join("\n") + footer);
58
63
  });
59
- // ── Get ──
60
- notes
64
+ note
61
65
  .command("get <noteId>")
62
66
  .description("Get a single note by id.")
63
67
  .action(async (noteId) => {
64
68
  const resp = (await apiGet(`/app/notes/${noteId}`));
65
69
  const note = unwrapResp(resp);
66
- if (isJsonMode(notes)) {
70
+ if (isJsonMode(saved)) {
67
71
  jsonOut(note);
68
72
  return;
69
73
  }
@@ -86,8 +90,7 @@ export function registerNotesCommand(program) {
86
90
  .trimEnd();
87
91
  console.log(output);
88
92
  });
89
- // ── Create ──
90
- notes
93
+ note
91
94
  .command("create")
92
95
  .description("Create a note. Provide --content (use '-' for stdin) and/or attachments.")
93
96
  .option("--content <content>", 'Note content (markdown supported, use "-" for stdin)')
@@ -97,7 +100,7 @@ export function registerNotesCommand(program) {
97
100
  if (!opts.content) {
98
101
  throw new Error("--content is required (use '-' to read from stdin)");
99
102
  }
100
- const content = opts.content === "-" ? readFileSync("/dev/stdin", "utf8") : opts.content;
103
+ const content = opts.content === "-" ? readStdin() : opts.content;
101
104
  const body = {
102
105
  content,
103
106
  timezone: opts.timezone || defaultTimezone(),
@@ -106,14 +109,13 @@ export function registerNotesCommand(program) {
106
109
  body.agentId = parseInt(opts.agentId, 10);
107
110
  const resp = (await apiPost(`/app/notes`, body));
108
111
  const note = unwrapResp(resp);
109
- if (isJsonMode(notes)) {
112
+ if (isJsonMode(saved)) {
110
113
  jsonOut(note);
111
114
  return;
112
115
  }
113
116
  console.log(`Note created!\n ID: ${note.id}\n Date: ${note.eventDate}\n Created: ${note.createdAt}`);
114
117
  });
115
- // ── Edit ──
116
- notes
118
+ note
117
119
  .command("edit <noteId>")
118
120
  .description("Edit a note. Provide --content and/or --agent-id.")
119
121
  .option("--content <content>", 'New note content (markdown supported, use "-" for stdin)')
@@ -125,29 +127,119 @@ export function registerNotesCommand(program) {
125
127
  const body = {};
126
128
  if (opts.content != null) {
127
129
  body.content =
128
- opts.content === "-" ? readFileSync("/dev/stdin", "utf8") : opts.content;
130
+ opts.content === "-" ? readStdin() : opts.content;
129
131
  }
130
132
  if (opts.agentId != null) {
131
133
  body.agentId = opts.agentId === "null" ? null : parseInt(opts.agentId, 10);
132
134
  }
133
135
  const resp = (await apiPatch(`/app/notes/${noteId}`, body));
134
136
  const note = unwrapResp(resp);
135
- if (isJsonMode(notes)) {
137
+ if (isJsonMode(saved)) {
136
138
  jsonOut(note);
137
139
  return;
138
140
  }
139
141
  console.log(`Note edited!\n ID: ${note.id}\n Updated: ${note.updatedAt}`);
140
142
  });
141
- // ── Delete ──
142
- notes
143
+ note
143
144
  .command("delete <noteId>")
144
145
  .description("Delete a note you authored.")
145
146
  .action(async (noteId) => {
146
147
  await apiDelete(`/app/notes/${noteId}`);
147
- if (isJsonMode(notes)) {
148
+ if (isJsonMode(saved)) {
148
149
  jsonOut({ id: noteId });
149
150
  return;
150
151
  }
151
152
  console.log(`Note ${noteId} deleted.`);
152
153
  });
153
154
  }
155
+ function registerPostCommands(saved) {
156
+ const post = saved
157
+ .command("post")
158
+ .description("Saved posts (snapshots of posts and replies you bookmark).");
159
+ post
160
+ .command("list")
161
+ .description("List posts you have saved.")
162
+ .option("--type <type>", "Filter by type: all|article|space-post", "all")
163
+ .option("--limit <number>", "Items per page (1-50)", "20")
164
+ .option("--cursor <string>", "Pagination cursor from previous response")
165
+ .action(async (opts) => {
166
+ const params = {
167
+ type: opts.type,
168
+ limit: parseInt(opts.limit, 10),
169
+ };
170
+ if (opts.cursor)
171
+ params.cursor = opts.cursor;
172
+ const resp = (await apiGet(`/reactions/me/saved`, params));
173
+ const items = (resp.data || []);
174
+ const pagination = (resp.pagination || {});
175
+ if (isJsonMode(saved)) {
176
+ jsonOut({ items, pagination });
177
+ return;
178
+ }
179
+ if (!items.length) {
180
+ console.log("No saved posts found.");
181
+ return;
182
+ }
183
+ const lines = items.map(formatSavedPostLine);
184
+ const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
185
+ console.log(`Saved posts (${items.length} items):\n` + lines.join("\n") + footer);
186
+ });
187
+ post
188
+ .command("get <postId>")
189
+ .description("Get a saved post snapshot by post id.")
190
+ .action(async (postId) => {
191
+ const resp = (await apiGet(`/feed/${postId}`));
192
+ const data = unwrapResp(resp);
193
+ if (isJsonMode(saved)) {
194
+ jsonOut(data);
195
+ return;
196
+ }
197
+ const post = (data.update || data.post || data);
198
+ const author = post.author?.name ||
199
+ `User ${post.authorId}`;
200
+ const title = post.title || "(no title)";
201
+ console.log([
202
+ `Saved post [${post.id}]: ${title}`,
203
+ `By: ${author} on ${post.createdAt}`,
204
+ "",
205
+ post.content || "",
206
+ ].join("\n"));
207
+ });
208
+ post
209
+ .command("create")
210
+ .description("Save a post or reply. Records a snapshot in your saved-posts collection.")
211
+ .requiredOption("--source <id>", "Source post or reply id to save (numeric)")
212
+ .action(async (opts) => {
213
+ const sourceId = parseInt(opts.source, 10);
214
+ if (!Number.isFinite(sourceId)) {
215
+ throw new Error("--source must be a numeric post or reply id.");
216
+ }
217
+ const resp = (await apiPost(`/reactions/posts/${sourceId}/save`, {
218
+ vaultIds: [],
219
+ }));
220
+ const data = unwrapResp(resp);
221
+ if (isJsonMode(saved)) {
222
+ jsonOut({ postId: sourceId, ...data });
223
+ return;
224
+ }
225
+ console.log(`Saved post ${sourceId}.`);
226
+ });
227
+ post
228
+ .command("delete <postId>")
229
+ .description("Remove a post from your saved-posts collection.")
230
+ .action(async (postId) => {
231
+ await apiDelete(`/reactions/posts/${postId}/save`);
232
+ if (isJsonMode(saved)) {
233
+ jsonOut({ postId });
234
+ return;
235
+ }
236
+ console.log(`Removed post ${postId} from saved.`);
237
+ });
238
+ }
239
+ export function registerSavedCommand(program) {
240
+ const saved = program
241
+ .command("saved")
242
+ .description("Saved-knowledge commands (notes and posts).");
243
+ registerNoteCommands(saved);
244
+ registerPostCommands(saved);
245
+ }