@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.
- package/.claude-plugin/marketplace.json +5 -6
- package/.claude-plugin/plugin.json +3 -4
- package/README.md +174 -82
- package/commands/space-explore.md +10 -10
- package/commands/space-share.md +13 -7
- package/dist/attachments.js +2 -1
- package/dist/commands/draft.js +2 -3
- package/dist/commands/global.js +212 -72
- package/dist/commands/init.js +5 -5
- package/dist/commands/{notes.js → saved.js} +115 -23
- package/dist/commands/space.js +121 -111
- package/dist/commands/sync.js +2 -56
- package/dist/commands/update.js +14 -8
- package/dist/commands/utils.js +6 -0
- package/dist/commands/vault.js +113 -0
- package/dist/main.js +4 -8
- package/package.json +2 -2
- package/skills/gobi-core/SKILL.md +19 -15
- package/skills/gobi-core/references/space.md +18 -19
- package/skills/gobi-draft/SKILL.md +3 -3
- package/skills/gobi-homepage/SKILL.md +21 -19
- package/skills/gobi-media/SKILL.md +2 -2
- package/skills/gobi-saved/SKILL.md +59 -0
- package/skills/gobi-saved/references/saved.md +52 -0
- package/skills/gobi-sense/SKILL.md +8 -4
- package/skills/gobi-space/SKILL.md +55 -38
- package/skills/gobi-space/references/global.md +87 -26
- package/skills/gobi-space/references/space.md +49 -61
- package/skills/gobi-vault/SKILL.md +92 -0
- package/skills/{gobi-core/references/sync.md → gobi-vault/references/vault.md} +41 -2
- package/dist/commands/brain.js +0 -141
- package/dist/commands/feed.js +0 -148
- package/skills/gobi-brain/SKILL.md +0 -100
- package/skills/gobi-brain/references/brain.md +0 -66
- package/skills/gobi-feed/SKILL.md +0 -43
- package/skills/gobi-feed/references/feed.md +0 -80
- package/skills/gobi-notes/SKILL.md +0 -52
- package/skills/gobi-notes/references/notes.md +0 -82
package/dist/commands/global.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
|
7
|
+
return readStdin();
|
|
7
8
|
return value;
|
|
8
9
|
}
|
|
9
|
-
function
|
|
10
|
-
const isReply = m.
|
|
11
|
-
|
|
12
|
-
const
|
|
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
|
|
30
|
-
// ──
|
|
31
|
+
.description("Global commands (posts and replies in the public feed across all vaults).");
|
|
32
|
+
// ── Feed (unified) ──
|
|
31
33
|
global
|
|
32
|
-
.command("
|
|
33
|
-
.description("List the global
|
|
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
|
-
|
|
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
|
|
58
|
+
console.log("No items found.");
|
|
54
59
|
return;
|
|
55
60
|
}
|
|
56
|
-
const lines = items.map(
|
|
61
|
+
const lines = items.map(formatFeedLine);
|
|
57
62
|
const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
|
|
58
|
-
console.log(`Global
|
|
63
|
+
console.log(`Global feed (${items.length} items, newest first):\n` + lines.join("\n") + footer);
|
|
59
64
|
});
|
|
60
|
-
// ──
|
|
65
|
+
// ── List posts ──
|
|
61
66
|
global
|
|
62
|
-
.command("
|
|
63
|
-
.description("
|
|
64
|
-
.option("--limit <number>", "
|
|
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
|
-
.
|
|
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
|
-
|
|
73
|
-
|
|
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
|
|
80
|
-
const replies = (data.
|
|
81
|
-
const author =
|
|
82
|
-
`User ${
|
|
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
|
|
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
|
|
92
|
-
const heading =
|
|
93
|
-
? `Reply [r:${
|
|
94
|
-
: `
|
|
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 ${
|
|
162
|
+
`By: ${author} (vault: ${vault}) on ${post.createdAt}`,
|
|
163
|
+
...(ancestorLines.length
|
|
164
|
+
? ["", `Ancestors (${ancestors.length} items, root first):`, ...ancestorLines]
|
|
165
|
+
: []),
|
|
98
166
|
"",
|
|
99
|
-
|
|
167
|
+
post.content || "",
|
|
100
168
|
"",
|
|
101
169
|
`Replies (${replies.length} items):`,
|
|
102
170
|
...replyLines,
|
|
103
|
-
...(pagination.hasMore
|
|
171
|
+
...(pagination.hasMore
|
|
172
|
+
? [` Next cursor: ${pagination.nextCursor}`]
|
|
173
|
+
: []),
|
|
104
174
|
].join("\n");
|
|
105
175
|
console.log(output);
|
|
106
176
|
});
|
|
107
|
-
// ──
|
|
177
|
+
// ── Create post ──
|
|
108
178
|
global
|
|
109
|
-
.command("
|
|
110
|
-
.description("
|
|
111
|
-
.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
// ──
|
|
227
|
+
// ── Edit post ──
|
|
130
228
|
global
|
|
131
|
-
.command("
|
|
132
|
-
.description("
|
|
133
|
-
.option("--title <title>", "
|
|
134
|
-
.option("--content <content>", "
|
|
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
|
-
.
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
159
|
-
|
|
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(
|
|
265
|
+
jsonOut(post);
|
|
162
266
|
return;
|
|
163
267
|
}
|
|
164
|
-
console.log(`
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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 <
|
|
172
|
-
.description("Reply to a
|
|
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 (
|
|
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(`/
|
|
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
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
|
209
|
-
const
|
|
210
|
-
if (!existsSync(
|
|
211
|
-
writeFileSync(
|
|
212
|
-
console.log("Created
|
|
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
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 === "-" ?
|
|
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(
|
|
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
|
-
|
|
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 === "-" ?
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
+
}
|