@gobi-ai/cli 2.0.20 → 2.0.21
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.
|
|
7
|
+
"version": "2.0.21",
|
|
8
8
|
"plugins": [
|
|
9
9
|
{
|
|
10
10
|
"name": "gobi",
|
|
11
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.
|
|
12
|
+
"version": "2.0.21",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "gobi-ai"
|
|
15
15
|
},
|
|
@@ -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,6 +6,7 @@ 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
11
|
import { registerSavedCommand } from "./commands/saved.js";
|
|
11
12
|
import { registerSessionsCommand } from "./commands/sessions.js";
|
|
@@ -60,6 +61,7 @@ export async function cli() {
|
|
|
60
61
|
registerAuthCommand(program);
|
|
61
62
|
registerSpaceCommand(program);
|
|
62
63
|
registerGlobalCommand(program);
|
|
64
|
+
registerPersonalCommand(program);
|
|
63
65
|
registerVaultCommand(program);
|
|
64
66
|
registerSavedCommand(program);
|
|
65
67
|
registerSessionsCommand(program);
|