@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/space.js
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
import { readFileSync } from "fs";
|
|
2
1
|
import { apiGet, apiPost, apiPatch, apiDelete } from "../client.js";
|
|
3
2
|
import { selectSpace, writeSpaceSetting } from "./init.js";
|
|
4
|
-
import { isJsonMode, jsonOut, resolveSpaceSlug, resolveVaultSlug, unwrapResp } from "./utils.js";
|
|
3
|
+
import { isJsonMode, jsonOut, readStdin, resolveSpaceSlug, resolveVaultSlug, unwrapResp, } from "./utils.js";
|
|
5
4
|
import { extractWikiLinks, uploadAttachments } from "../attachments.js";
|
|
6
5
|
import { getValidToken } from "../auth/manager.js";
|
|
7
6
|
function readContent(value) {
|
|
8
7
|
if (value === "-")
|
|
9
|
-
return
|
|
8
|
+
return readStdin();
|
|
10
9
|
return value;
|
|
11
10
|
}
|
|
12
|
-
function
|
|
13
|
-
const isReply = m.
|
|
14
|
-
const id = `[${isReply ? "r" : "
|
|
15
|
-
const kind = isReply ? "reply
|
|
11
|
+
function formatFeedLine(m) {
|
|
12
|
+
const isReply = m.parentPostId != null;
|
|
13
|
+
const id = `[${isReply ? "r" : "p"}:${m.id}]`;
|
|
14
|
+
const kind = isReply ? "reply" : "post ";
|
|
16
15
|
const author = m.author?.name ||
|
|
17
16
|
`User ${m.authorId ?? "?"}`;
|
|
18
17
|
let label;
|
|
@@ -29,7 +28,7 @@ function formatMessageLine(m) {
|
|
|
29
28
|
export function registerSpaceCommand(program) {
|
|
30
29
|
const space = program
|
|
31
30
|
.command("space")
|
|
32
|
-
.description("Space commands (
|
|
31
|
+
.description("Space commands (posts, replies). Space and member admin is web-UI only.")
|
|
33
32
|
.option("--space-slug <slug>", "Space slug (overrides .gobi/settings.yaml)");
|
|
34
33
|
// ── List spaces ──
|
|
35
34
|
space
|
|
@@ -124,8 +123,8 @@ export function registerSpaceCommand(program) {
|
|
|
124
123
|
console.log(`Topics (${items.length}):\n` + lines.join("\n"));
|
|
125
124
|
});
|
|
126
125
|
space
|
|
127
|
-
.command("list-topic-
|
|
128
|
-
.description("List
|
|
126
|
+
.command("list-topic-posts <topicSlug>")
|
|
127
|
+
.description("List posts tagged with a topic in a space (cursor-paginated).")
|
|
129
128
|
.option("--limit <number>", "Items per page", "20")
|
|
130
129
|
.option("--cursor <string>", "Pagination cursor from previous response")
|
|
131
130
|
.action(async (topicSlug, opts) => {
|
|
@@ -135,7 +134,7 @@ export function registerSpaceCommand(program) {
|
|
|
135
134
|
};
|
|
136
135
|
if (opts.cursor)
|
|
137
136
|
params.cursor = opts.cursor;
|
|
138
|
-
const resp = (await apiGet(`/spaces/${spaceSlug}/topics/${topicSlug}/
|
|
137
|
+
const resp = (await apiGet(`/spaces/${spaceSlug}/topics/${topicSlug}/posts`, params));
|
|
139
138
|
const data = unwrapResp(resp);
|
|
140
139
|
const pagination = (resp.pagination || {});
|
|
141
140
|
if (isJsonMode(space)) {
|
|
@@ -143,27 +142,27 @@ export function registerSpaceCommand(program) {
|
|
|
143
142
|
return;
|
|
144
143
|
}
|
|
145
144
|
const topic = (data.topic || {});
|
|
146
|
-
const
|
|
147
|
-
if (!
|
|
148
|
-
console.log(`No
|
|
145
|
+
const posts = (data.posts || []);
|
|
146
|
+
if (!posts.length) {
|
|
147
|
+
console.log(`No posts found for topic "${topic.name || topicSlug}".`);
|
|
149
148
|
return;
|
|
150
149
|
}
|
|
151
150
|
const lines = [];
|
|
152
|
-
for (const t of
|
|
151
|
+
for (const t of posts) {
|
|
153
152
|
const author = t.author?.name || "Unknown";
|
|
154
153
|
const spaceName = t.space?.name || "";
|
|
155
154
|
lines.push(`- [${t.id}] "${t.title}" by ${author} in ${spaceName} (${t.replyCount} replies, ${t.createdAt})`);
|
|
156
155
|
}
|
|
157
156
|
const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
|
|
158
157
|
console.log(`Topic: ${topic.name || topicSlug}\n` +
|
|
159
|
-
`
|
|
158
|
+
`Posts (${posts.length} items):\n` +
|
|
160
159
|
lines.join("\n") +
|
|
161
160
|
footer);
|
|
162
161
|
});
|
|
163
|
-
// ──
|
|
162
|
+
// ── Feed (unified) ──
|
|
164
163
|
space
|
|
165
|
-
.command("
|
|
166
|
-
.description("List the unified
|
|
164
|
+
.command("feed")
|
|
165
|
+
.description("List the unified feed (posts and replies, newest first) in a space.")
|
|
167
166
|
.option("--limit <number>", "Items per page", "20")
|
|
168
167
|
.option("--cursor <string>", "Pagination cursor from previous response")
|
|
169
168
|
.action(async (opts) => {
|
|
@@ -173,7 +172,7 @@ export function registerSpaceCommand(program) {
|
|
|
173
172
|
};
|
|
174
173
|
if (opts.cursor)
|
|
175
174
|
params.cursor = opts.cursor;
|
|
176
|
-
const resp = (await apiGet(`/spaces/${spaceSlug}/
|
|
175
|
+
const resp = (await apiGet(`/spaces/${spaceSlug}/feed`, params));
|
|
177
176
|
if (isJsonMode(space)) {
|
|
178
177
|
jsonOut({
|
|
179
178
|
items: resp.data || [],
|
|
@@ -184,74 +183,69 @@ export function registerSpaceCommand(program) {
|
|
|
184
183
|
const items = (resp.data || []);
|
|
185
184
|
const pagination = (resp.pagination || {});
|
|
186
185
|
if (!items.length) {
|
|
187
|
-
console.log("No
|
|
186
|
+
console.log("No items found.");
|
|
188
187
|
return;
|
|
189
188
|
}
|
|
190
|
-
const lines = items.map(
|
|
189
|
+
const lines = items.map(formatFeedLine);
|
|
191
190
|
const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
|
|
192
|
-
console.log(`
|
|
191
|
+
console.log(`Feed (${items.length} items, newest first):\n` + lines.join("\n") + footer);
|
|
193
192
|
});
|
|
194
|
-
// ──
|
|
193
|
+
// ── Posts (get, list, create, edit, delete) ──
|
|
195
194
|
space
|
|
196
|
-
.command("
|
|
197
|
-
.description("
|
|
198
|
-
.action(async (threadId) => {
|
|
199
|
-
const spaceSlug = resolveSpaceSlug(space);
|
|
200
|
-
const resp = (await apiGet(`/spaces/${spaceSlug}/threads/${threadId}/ancestors`));
|
|
201
|
-
const data = unwrapResp(resp);
|
|
202
|
-
const ancestors = (data.ancestors || []);
|
|
203
|
-
if (isJsonMode(space)) {
|
|
204
|
-
jsonOut({ ancestors });
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
if (!ancestors.length) {
|
|
208
|
-
console.log("No ancestors (this is a root thread).");
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
const lines = [];
|
|
212
|
-
ancestors.forEach((a, i) => {
|
|
213
|
-
lines.push(`${i + 1}. ${formatMessageLine(a)}`);
|
|
214
|
-
});
|
|
215
|
-
console.log(`Ancestors (${ancestors.length} items, root first):\n` + lines.join("\n"));
|
|
216
|
-
});
|
|
217
|
-
// ── Threads (get, list, create, edit, delete) ──
|
|
218
|
-
space
|
|
219
|
-
.command("get-thread <threadId>")
|
|
220
|
-
.description("Get a thread and its replies (paginated).")
|
|
195
|
+
.command("get-post <postId>")
|
|
196
|
+
.description("Get a post with its ancestors and replies (paginated).")
|
|
221
197
|
.option("--limit <number>", "Replies per page", "20")
|
|
222
198
|
.option("--cursor <string>", "Pagination cursor from previous response")
|
|
223
|
-
.action(async (
|
|
199
|
+
.action(async (postId, opts) => {
|
|
224
200
|
const spaceSlug = resolveSpaceSlug(space);
|
|
225
201
|
const params = {
|
|
226
202
|
limit: parseInt(opts.limit, 10),
|
|
227
203
|
};
|
|
228
204
|
if (opts.cursor)
|
|
229
205
|
params.cursor = opts.cursor;
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
206
|
+
const [postResp, ancestorsResp] = await Promise.all([
|
|
207
|
+
apiGet(`/spaces/${spaceSlug}/posts/${postId}`, params),
|
|
208
|
+
apiGet(`/spaces/${spaceSlug}/posts/${postId}/ancestors`),
|
|
209
|
+
]);
|
|
210
|
+
const data = unwrapResp(postResp);
|
|
211
|
+
const pagination = (postResp.pagination || {});
|
|
212
|
+
const mentions = (postResp.mentions || {});
|
|
213
|
+
const ancestorsData = unwrapResp(ancestorsResp);
|
|
214
|
+
const ancestors = (ancestorsData.ancestors || []);
|
|
234
215
|
if (isJsonMode(space)) {
|
|
235
|
-
jsonOut({ ...data, pagination, mentions });
|
|
216
|
+
jsonOut({ ...data, ancestors, pagination, mentions });
|
|
236
217
|
return;
|
|
237
218
|
}
|
|
238
|
-
const
|
|
219
|
+
const post = (data.thread || data);
|
|
239
220
|
const replies = (data.items || []);
|
|
240
|
-
const author =
|
|
241
|
-
`User ${
|
|
221
|
+
const author = post.author?.name ||
|
|
222
|
+
`User ${post.authorId}`;
|
|
223
|
+
const ancestorLines = [];
|
|
224
|
+
if (ancestors.length) {
|
|
225
|
+
ancestors.forEach((a, i) => {
|
|
226
|
+
ancestorLines.push(` ${i + 1}. ${formatFeedLine(a)}`);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
242
229
|
const replyLines = [];
|
|
243
230
|
for (const r of replies) {
|
|
244
231
|
const rAuthor = r.author?.name ||
|
|
245
232
|
`User ${r.authorId}`;
|
|
246
233
|
const text = r.content;
|
|
247
|
-
const truncated = text.length > 200 ? text.slice(0, 200) + "
|
|
234
|
+
const truncated = text && text.length > 200 ? text.slice(0, 200) + "…" : text;
|
|
248
235
|
replyLines.push(` - ${rAuthor}: ${truncated} (${r.createdAt})`);
|
|
249
236
|
}
|
|
237
|
+
const isReplyPost = post.parentPostId != null;
|
|
238
|
+
const heading = isReplyPost
|
|
239
|
+
? `Reply [r:${post.id}]`
|
|
240
|
+
: `Post: ${post.title || "(no title)"}`;
|
|
250
241
|
const output = [
|
|
251
|
-
|
|
252
|
-
`By: ${author} on ${
|
|
242
|
+
heading,
|
|
243
|
+
`By: ${author} on ${post.createdAt}`,
|
|
244
|
+
...(ancestorLines.length
|
|
245
|
+
? ["", `Ancestors (${ancestors.length} items, root first):`, ...ancestorLines]
|
|
246
|
+
: []),
|
|
253
247
|
"",
|
|
254
|
-
|
|
248
|
+
post.content || "",
|
|
255
249
|
"",
|
|
256
250
|
`Replies (${replies.length} items):`,
|
|
257
251
|
...replyLines,
|
|
@@ -260,8 +254,8 @@ export function registerSpaceCommand(program) {
|
|
|
260
254
|
console.log(output);
|
|
261
255
|
});
|
|
262
256
|
space
|
|
263
|
-
.command("list-
|
|
264
|
-
.description("List
|
|
257
|
+
.command("list-posts")
|
|
258
|
+
.description("List posts in a space (paginated).")
|
|
265
259
|
.option("--limit <number>", "Items per page", "20")
|
|
266
260
|
.option("--cursor <string>", "Pagination cursor from previous response")
|
|
267
261
|
.action(async (opts) => {
|
|
@@ -271,7 +265,7 @@ export function registerSpaceCommand(program) {
|
|
|
271
265
|
};
|
|
272
266
|
if (opts.cursor)
|
|
273
267
|
params.cursor = opts.cursor;
|
|
274
|
-
const resp = (await apiGet(`/spaces/${spaceSlug}/
|
|
268
|
+
const resp = (await apiGet(`/spaces/${spaceSlug}/posts`, params));
|
|
275
269
|
if (isJsonMode(space)) {
|
|
276
270
|
jsonOut({
|
|
277
271
|
items: resp.data || [],
|
|
@@ -283,7 +277,7 @@ export function registerSpaceCommand(program) {
|
|
|
283
277
|
const items = (resp.data || []);
|
|
284
278
|
const pagination = (resp.pagination || {});
|
|
285
279
|
if (!items.length) {
|
|
286
|
-
console.log("No
|
|
280
|
+
console.log("No posts found.");
|
|
287
281
|
return;
|
|
288
282
|
}
|
|
289
283
|
const lines = [];
|
|
@@ -293,94 +287,110 @@ export function registerSpaceCommand(program) {
|
|
|
293
287
|
lines.push(`- [${t.id}] "${t.title}" by ${author} (${t.replyCount} replies, ${t.createdAt})`);
|
|
294
288
|
}
|
|
295
289
|
const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
|
|
296
|
-
console.log(`
|
|
290
|
+
console.log(`Posts (${items.length} items):\n` + lines.join("\n") + footer);
|
|
297
291
|
});
|
|
298
292
|
space
|
|
299
|
-
.command("create-
|
|
300
|
-
.description("Create a
|
|
301
|
-
.requiredOption("--title <title>", "Title of the
|
|
302
|
-
.requiredOption("--content <content>", "
|
|
303
|
-
.option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before posting")
|
|
304
|
-
.option("--vault-slug <vaultSlug>", "
|
|
293
|
+
.command("create-post")
|
|
294
|
+
.description("Create a post in a space.")
|
|
295
|
+
.requiredOption("--title <title>", "Title of the post")
|
|
296
|
+
.requiredOption("--content <content>", "Post content (markdown supported)")
|
|
297
|
+
.option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before posting (also attributes the post to that vault)")
|
|
298
|
+
.option("--vault-slug <vaultSlug>", "Attribute the post to this vault (sets authorVaultId). Also used as upload destination for --auto-attachments.")
|
|
305
299
|
.action(async (opts) => {
|
|
306
300
|
const content = readContent(opts.content);
|
|
301
|
+
let authorVaultSlug;
|
|
302
|
+
if (opts.vaultSlug || opts.autoAttachments) {
|
|
303
|
+
authorVaultSlug = resolveVaultSlug(opts);
|
|
304
|
+
}
|
|
307
305
|
if (opts.autoAttachments) {
|
|
308
|
-
const vaultSlug = resolveVaultSlug(opts);
|
|
309
306
|
const token = await getValidToken();
|
|
310
307
|
const links = extractWikiLinks(content);
|
|
311
|
-
await uploadAttachments(
|
|
308
|
+
await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
|
|
312
309
|
}
|
|
313
310
|
const spaceSlug = resolveSpaceSlug(space);
|
|
314
|
-
const
|
|
311
|
+
const body = {
|
|
315
312
|
title: opts.title,
|
|
316
313
|
content,
|
|
317
|
-
}
|
|
318
|
-
|
|
314
|
+
};
|
|
315
|
+
if (authorVaultSlug)
|
|
316
|
+
body.authorVaultSlug = authorVaultSlug;
|
|
317
|
+
const resp = (await apiPost(`/spaces/${spaceSlug}/posts`, body));
|
|
318
|
+
const post = unwrapResp(resp);
|
|
319
319
|
if (isJsonMode(space)) {
|
|
320
|
-
jsonOut(
|
|
320
|
+
jsonOut(post);
|
|
321
321
|
return;
|
|
322
322
|
}
|
|
323
|
-
console.log(`
|
|
324
|
-
` ID: ${
|
|
325
|
-
` Title: ${
|
|
326
|
-
` Created: ${
|
|
323
|
+
console.log(`Post created!\n` +
|
|
324
|
+
` ID: ${post.id}\n` +
|
|
325
|
+
` Title: ${post.title}\n` +
|
|
326
|
+
` Created: ${post.createdAt}`);
|
|
327
327
|
});
|
|
328
328
|
space
|
|
329
|
-
.command("edit-
|
|
330
|
-
.description("Edit a
|
|
331
|
-
.option("--title <title>", "New title for the
|
|
332
|
-
.option("--content <content>", "New content for the
|
|
333
|
-
.option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before editing")
|
|
334
|
-
.option("--vault-slug <vaultSlug>", "
|
|
335
|
-
.action(async (
|
|
336
|
-
|
|
337
|
-
|
|
329
|
+
.command("edit-post <postId>")
|
|
330
|
+
.description("Edit a post. You must be the author.")
|
|
331
|
+
.option("--title <title>", "New title for the post")
|
|
332
|
+
.option("--content <content>", "New content for the post (markdown supported)")
|
|
333
|
+
.option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before editing (also attributes the post to that vault)")
|
|
334
|
+
.option("--vault-slug <vaultSlug>", "Attribute the post to this vault (sets authorVaultId). Also used as upload destination for --auto-attachments. Pass an empty string to detach.")
|
|
335
|
+
.action(async (postId, opts) => {
|
|
336
|
+
const wantsVaultChange = opts.vaultSlug !== undefined || opts.autoAttachments;
|
|
337
|
+
if (!opts.title && !opts.content && !wantsVaultChange) {
|
|
338
|
+
throw new Error("Provide at least --title, --content, or --vault-slug to update.");
|
|
338
339
|
}
|
|
339
340
|
const spaceSlug = resolveSpaceSlug(space);
|
|
341
|
+
let authorVaultSlug;
|
|
342
|
+
if (opts.vaultSlug !== undefined) {
|
|
343
|
+
// Empty string detaches; non-empty resolves through settings fallback.
|
|
344
|
+
authorVaultSlug = opts.vaultSlug === "" ? "" : resolveVaultSlug(opts);
|
|
345
|
+
}
|
|
346
|
+
else if (opts.autoAttachments) {
|
|
347
|
+
authorVaultSlug = resolveVaultSlug(opts);
|
|
348
|
+
}
|
|
340
349
|
const body = {};
|
|
341
350
|
if (opts.title != null)
|
|
342
351
|
body.title = opts.title;
|
|
343
352
|
if (opts.content != null) {
|
|
344
353
|
const content = readContent(opts.content);
|
|
345
354
|
if (opts.autoAttachments) {
|
|
346
|
-
const vaultSlug = resolveVaultSlug(opts);
|
|
347
355
|
const token = await getValidToken();
|
|
348
356
|
const links = extractWikiLinks(content);
|
|
349
|
-
await uploadAttachments(
|
|
357
|
+
await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
|
|
350
358
|
}
|
|
351
359
|
body.content = content;
|
|
352
360
|
}
|
|
353
|
-
|
|
354
|
-
|
|
361
|
+
if (authorVaultSlug !== undefined)
|
|
362
|
+
body.authorVaultSlug = authorVaultSlug;
|
|
363
|
+
const resp = (await apiPatch(`/spaces/${spaceSlug}/posts/${postId}`, body));
|
|
364
|
+
const post = unwrapResp(resp);
|
|
355
365
|
if (isJsonMode(space)) {
|
|
356
|
-
jsonOut(
|
|
366
|
+
jsonOut(post);
|
|
357
367
|
return;
|
|
358
368
|
}
|
|
359
|
-
console.log(`
|
|
360
|
-
` ID: ${
|
|
361
|
-
` Title: ${
|
|
362
|
-
` Edited: ${
|
|
369
|
+
console.log(`Post edited!\n` +
|
|
370
|
+
` ID: ${post.id}\n` +
|
|
371
|
+
` Title: ${post.title}\n` +
|
|
372
|
+
` Edited: ${post.editedAt}`);
|
|
363
373
|
});
|
|
364
374
|
space
|
|
365
|
-
.command("delete-
|
|
366
|
-
.description("Delete a
|
|
367
|
-
.action(async (
|
|
375
|
+
.command("delete-post <postId>")
|
|
376
|
+
.description("Delete a post. You must be the author.")
|
|
377
|
+
.action(async (postId) => {
|
|
368
378
|
const spaceSlug = resolveSpaceSlug(space);
|
|
369
|
-
await apiDelete(`/spaces/${spaceSlug}/
|
|
379
|
+
await apiDelete(`/spaces/${spaceSlug}/posts/${postId}`);
|
|
370
380
|
if (isJsonMode(space)) {
|
|
371
|
-
jsonOut({ id:
|
|
381
|
+
jsonOut({ id: postId });
|
|
372
382
|
return;
|
|
373
383
|
}
|
|
374
|
-
console.log(`
|
|
384
|
+
console.log(`Post ${postId} deleted.`);
|
|
375
385
|
});
|
|
376
386
|
// ── Replies (create, edit, delete) ──
|
|
377
387
|
space
|
|
378
|
-
.command("create-reply <
|
|
379
|
-
.description("Create a reply to a
|
|
388
|
+
.command("create-reply <postId>")
|
|
389
|
+
.description("Create a reply to a post in a space.")
|
|
380
390
|
.requiredOption("--content <content>", "Reply content (markdown supported)")
|
|
381
|
-
.action(async (
|
|
391
|
+
.action(async (postId, opts) => {
|
|
382
392
|
const spaceSlug = resolveSpaceSlug(space);
|
|
383
|
-
const resp = (await apiPost(`/spaces/${spaceSlug}/
|
|
393
|
+
const resp = (await apiPost(`/spaces/${spaceSlug}/posts/${postId}/replies`, { content: readContent(opts.content) }));
|
|
384
394
|
const msg = unwrapResp(resp);
|
|
385
395
|
const mentions = (resp.mentions || {});
|
|
386
396
|
if (isJsonMode(space)) {
|
package/dist/commands/sync.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "crypto";
|
|
2
2
|
import { existsSync, readFileSync, rmSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
3
3
|
import { writeFile } from "fs/promises";
|
|
4
|
-
import { join, dirname, extname
|
|
4
|
+
import { join, dirname, extname } from "path";
|
|
5
5
|
import Database from "better-sqlite3";
|
|
6
6
|
import inquirer from "inquirer";
|
|
7
7
|
import ignore from "ignore";
|
|
@@ -9,8 +9,7 @@ import trash from "trash";
|
|
|
9
9
|
import { WEBDRIVE_BASE_URL } from "../constants.js";
|
|
10
10
|
import { getValidToken } from "../auth/manager.js";
|
|
11
11
|
import { GobiError } from "../errors.js";
|
|
12
|
-
import {
|
|
13
|
-
import { isJsonMode, jsonOut } from "./utils.js";
|
|
12
|
+
import { jsonOut } from "./utils.js";
|
|
14
13
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
15
14
|
const SYNC_IGNORE_NAMES = new Set([
|
|
16
15
|
".gobi",
|
|
@@ -880,56 +879,3 @@ async function executeSyncPlan(opts, baseUrl, token, gobiDir) {
|
|
|
880
879
|
}
|
|
881
880
|
return result;
|
|
882
881
|
}
|
|
883
|
-
// ─── Commander Registration ───────────────────────────────────────────────────
|
|
884
|
-
export function registerSyncCommand(program) {
|
|
885
|
-
program
|
|
886
|
-
.command("sync")
|
|
887
|
-
.description("Sync local vault files with Gobi Webdrive.")
|
|
888
|
-
.option("--upload-only", "Only upload local changes to server")
|
|
889
|
-
.option("--download-only", "Only download server changes to local")
|
|
890
|
-
.option("--conflict <strategy>", "Conflict resolution strategy: ask|server|client|skip", "ask")
|
|
891
|
-
.option("--dir <path>", "Local vault directory (default: current directory)")
|
|
892
|
-
.option("--dry-run", "Preview changes without making them")
|
|
893
|
-
.option("--full", "Full sync: ignore cursor and hash cache, re-check every file")
|
|
894
|
-
.option("--path <path>", "Restrict sync to a specific file or folder (repeatable)", (v, prev) => prev.concat(v), [])
|
|
895
|
-
.option("--plan-file <path>", "Write dry-run plan to file (use with --dry-run) or read plan to execute (use with --execute)")
|
|
896
|
-
.option("--execute", "Execute a previously written plan file (requires --plan-file)")
|
|
897
|
-
.option("--conflict-choices <json>", "Per-file conflict resolutions as JSON object, e.g. '{\"file.md\":\"server\"}' (use with --execute)")
|
|
898
|
-
.action(async function (opts) {
|
|
899
|
-
if (opts.uploadOnly && opts.downloadOnly) {
|
|
900
|
-
throw new GobiError("--upload-only and --download-only are mutually exclusive.", "INVALID_OPTION");
|
|
901
|
-
}
|
|
902
|
-
if (opts.execute && !opts.planFile) {
|
|
903
|
-
throw new GobiError("--execute requires --plan-file", "INVALID_OPTION");
|
|
904
|
-
}
|
|
905
|
-
const validStrategies = ["ask", "server", "client", "skip"];
|
|
906
|
-
if (!validStrategies.includes(opts.conflict)) {
|
|
907
|
-
throw new GobiError(`Invalid --conflict value "${opts.conflict}". Use: ask|server|client|skip`, "INVALID_OPTION");
|
|
908
|
-
}
|
|
909
|
-
let conflictChoices;
|
|
910
|
-
if (opts.conflictChoices) {
|
|
911
|
-
try {
|
|
912
|
-
conflictChoices = JSON.parse(opts.conflictChoices);
|
|
913
|
-
}
|
|
914
|
-
catch {
|
|
915
|
-
throw new GobiError("--conflict-choices must be valid JSON", "INVALID_OPTION");
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
const vaultSlug = getVaultSlug();
|
|
919
|
-
const dir = opts.dir ? pathResolve(opts.dir) : process.cwd();
|
|
920
|
-
await runSync({
|
|
921
|
-
vaultSlug,
|
|
922
|
-
dir,
|
|
923
|
-
uploadOnly: !!opts.uploadOnly,
|
|
924
|
-
downloadOnly: !!opts.downloadOnly,
|
|
925
|
-
conflict: opts.conflict,
|
|
926
|
-
dryRun: !!opts.dryRun,
|
|
927
|
-
full: !!opts.full,
|
|
928
|
-
paths: opts.path ?? [],
|
|
929
|
-
planFile: opts.planFile,
|
|
930
|
-
execute: !!opts.execute,
|
|
931
|
-
conflictChoices,
|
|
932
|
-
jsonMode: isJsonMode(this),
|
|
933
|
-
});
|
|
934
|
-
});
|
|
935
|
-
}
|
package/dist/commands/update.js
CHANGED
|
@@ -10,20 +10,26 @@ async function fetchLatestVersion() {
|
|
|
10
10
|
const data = (await res.json());
|
|
11
11
|
return data.version;
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
// `which` is Unix-only; Windows uses `where`. `where` may print multiple
|
|
14
|
+
// matches on separate lines — take the first one.
|
|
15
|
+
function locateGobi() {
|
|
16
|
+
const cmd = process.platform === "win32" ? "where gobi" : "which gobi";
|
|
14
17
|
try {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
return "brew";
|
|
18
|
-
}
|
|
18
|
+
const out = execSync(cmd, { encoding: "utf-8" }).trim();
|
|
19
|
+
return out.split(/\r?\n/)[0] || null;
|
|
19
20
|
}
|
|
20
21
|
catch {
|
|
21
|
-
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function detectInstallMethod() {
|
|
26
|
+
const gobiBin = locateGobi();
|
|
27
|
+
if (gobiBin && (gobiBin.includes("/Cellar/") || gobiBin.includes("/homebrew/"))) {
|
|
28
|
+
return "brew";
|
|
22
29
|
}
|
|
23
30
|
try {
|
|
24
31
|
const npmGlobalDir = execSync("npm root -g", { encoding: "utf-8" }).trim();
|
|
25
|
-
|
|
26
|
-
if (gobiBin.includes(npmGlobalDir.replace("/lib/node_modules", ""))) {
|
|
32
|
+
if (gobiBin && gobiBin.includes(npmGlobalDir.replace("/lib/node_modules", ""))) {
|
|
27
33
|
return "npm";
|
|
28
34
|
}
|
|
29
35
|
}
|
package/dist/commands/utils.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
1
2
|
import { getSpaceSlug, getVaultSlug } from "./init.js";
|
|
3
|
+
// Reads all of stdin synchronously. Uses fd 0 (cross-platform) instead of
|
|
4
|
+
// "/dev/stdin", which doesn't exist on Windows.
|
|
5
|
+
export function readStdin() {
|
|
6
|
+
return readFileSync(0, "utf8");
|
|
7
|
+
}
|
|
2
8
|
export function isJsonMode(cmd) {
|
|
3
9
|
return !!cmd.parent?.opts().json;
|
|
4
10
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { join, resolve as pathResolve } from "path";
|
|
3
|
+
import { WEBDRIVE_BASE_URL } from "../constants.js";
|
|
4
|
+
import { getValidToken } from "../auth/manager.js";
|
|
5
|
+
import { GobiError } from "../errors.js";
|
|
6
|
+
import { getVaultSlug } from "./init.js";
|
|
7
|
+
import { isJsonMode, jsonOut } from "./utils.js";
|
|
8
|
+
import { runSync } from "./sync.js";
|
|
9
|
+
export const PUBLISH_FILENAME = "PUBLISH.md";
|
|
10
|
+
export function registerVaultCommand(program) {
|
|
11
|
+
const vault = program
|
|
12
|
+
.command("vault")
|
|
13
|
+
.description("Vault commands (publish/unpublish profile, sync files).");
|
|
14
|
+
vault
|
|
15
|
+
.command("publish")
|
|
16
|
+
.description(`Upload ${PUBLISH_FILENAME} to the vault root on webdrive. Triggers post-processing (vault sync, metadata update, Discord notification).`)
|
|
17
|
+
.action(async () => {
|
|
18
|
+
const vaultId = getVaultSlug();
|
|
19
|
+
const filePath = join(process.cwd(), PUBLISH_FILENAME);
|
|
20
|
+
if (!existsSync(filePath)) {
|
|
21
|
+
throw new Error(`${PUBLISH_FILENAME} not found in ${process.cwd()}`);
|
|
22
|
+
}
|
|
23
|
+
const content = readFileSync(filePath, "utf-8");
|
|
24
|
+
const token = await getValidToken();
|
|
25
|
+
const url = `${WEBDRIVE_BASE_URL}/api/v1/vaults/${vaultId}/file/${PUBLISH_FILENAME}`;
|
|
26
|
+
const res = await fetch(url, {
|
|
27
|
+
method: "PUT",
|
|
28
|
+
headers: {
|
|
29
|
+
Authorization: `Bearer ${token}`,
|
|
30
|
+
"Content-Type": "text/markdown",
|
|
31
|
+
},
|
|
32
|
+
body: content,
|
|
33
|
+
});
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
throw new Error(`Upload failed: HTTP ${res.status}: ${(await res.text()) || "(no body)"}`);
|
|
36
|
+
}
|
|
37
|
+
if (isJsonMode(vault)) {
|
|
38
|
+
jsonOut({ vaultId });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
console.log(`Published ${PUBLISH_FILENAME} to vault "${vaultId}"`);
|
|
42
|
+
});
|
|
43
|
+
vault
|
|
44
|
+
.command("unpublish")
|
|
45
|
+
.description(`Delete ${PUBLISH_FILENAME} from the vault on webdrive.`)
|
|
46
|
+
.action(async () => {
|
|
47
|
+
const vaultId = getVaultSlug();
|
|
48
|
+
const token = await getValidToken();
|
|
49
|
+
const url = `${WEBDRIVE_BASE_URL}/api/v1/vaults/${vaultId}/file/${PUBLISH_FILENAME}`;
|
|
50
|
+
const res = await fetch(url, {
|
|
51
|
+
method: "DELETE",
|
|
52
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
53
|
+
});
|
|
54
|
+
if (!res.ok) {
|
|
55
|
+
throw new Error(`Delete failed: HTTP ${res.status}: ${(await res.text()) || "(no body)"}`);
|
|
56
|
+
}
|
|
57
|
+
if (isJsonMode(vault)) {
|
|
58
|
+
jsonOut({ vaultId });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
console.log(`Deleted ${PUBLISH_FILENAME} from vault "${vaultId}"`);
|
|
62
|
+
});
|
|
63
|
+
vault
|
|
64
|
+
.command("sync")
|
|
65
|
+
.description("Sync local vault files with Gobi Webdrive.")
|
|
66
|
+
.option("--upload-only", "Only upload local changes to server")
|
|
67
|
+
.option("--download-only", "Only download server changes to local")
|
|
68
|
+
.option("--conflict <strategy>", "Conflict resolution strategy: ask|server|client|skip", "ask")
|
|
69
|
+
.option("--dir <path>", "Local vault directory (default: current directory)")
|
|
70
|
+
.option("--dry-run", "Preview changes without making them")
|
|
71
|
+
.option("--full", "Full sync: ignore cursor and hash cache, re-check every file")
|
|
72
|
+
.option("--path <path>", "Restrict sync to a specific file or folder (repeatable)", (v, prev) => prev.concat(v), [])
|
|
73
|
+
.option("--plan-file <path>", "Write dry-run plan to file (use with --dry-run) or read plan to execute (use with --execute)")
|
|
74
|
+
.option("--execute", "Execute a previously written plan file (requires --plan-file)")
|
|
75
|
+
.option("--conflict-choices <json>", "Per-file conflict resolutions as JSON object, e.g. '{\"file.md\":\"server\"}' (use with --execute)")
|
|
76
|
+
.action(async function (opts) {
|
|
77
|
+
if (opts.uploadOnly && opts.downloadOnly) {
|
|
78
|
+
throw new GobiError("--upload-only and --download-only are mutually exclusive.", "INVALID_OPTION");
|
|
79
|
+
}
|
|
80
|
+
if (opts.execute && !opts.planFile) {
|
|
81
|
+
throw new GobiError("--execute requires --plan-file", "INVALID_OPTION");
|
|
82
|
+
}
|
|
83
|
+
const validStrategies = ["ask", "server", "client", "skip"];
|
|
84
|
+
if (!validStrategies.includes(opts.conflict)) {
|
|
85
|
+
throw new GobiError(`Invalid --conflict value "${opts.conflict}". Use: ask|server|client|skip`, "INVALID_OPTION");
|
|
86
|
+
}
|
|
87
|
+
let conflictChoices;
|
|
88
|
+
if (opts.conflictChoices) {
|
|
89
|
+
try {
|
|
90
|
+
conflictChoices = JSON.parse(opts.conflictChoices);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
throw new GobiError("--conflict-choices must be valid JSON", "INVALID_OPTION");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const vaultSlug = getVaultSlug();
|
|
97
|
+
const dir = opts.dir ? pathResolve(opts.dir) : process.cwd();
|
|
98
|
+
await runSync({
|
|
99
|
+
vaultSlug,
|
|
100
|
+
dir,
|
|
101
|
+
uploadOnly: !!opts.uploadOnly,
|
|
102
|
+
downloadOnly: !!opts.downloadOnly,
|
|
103
|
+
conflict: opts.conflict,
|
|
104
|
+
dryRun: !!opts.dryRun,
|
|
105
|
+
full: !!opts.full,
|
|
106
|
+
paths: opts.path ?? [],
|
|
107
|
+
planFile: opts.planFile,
|
|
108
|
+
execute: !!opts.execute,
|
|
109
|
+
conflictChoices,
|
|
110
|
+
jsonMode: isJsonMode(this),
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|