@gobi-ai/cli 1.3.8 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +3 -4
- package/.claude-plugin/plugin.json +2 -3
- package/README.md +71 -80
- package/commands/space-explore.md +10 -10
- package/commands/space-share.md +13 -7
- package/dist/commands/global.js +205 -70
- package/dist/commands/init.js +5 -5
- package/dist/commands/{notes.js → saved.js} +112 -19
- package/dist/commands/space.js +92 -97
- package/dist/commands/sync.js +2 -56
- package/dist/commands/vault.js +113 -0
- package/dist/main.js +4 -8
- package/package.json +2 -2
- package/skills/gobi-core/SKILL.md +5 -7
- package/skills/gobi-core/references/space.md +18 -19
- package/skills/gobi-draft/SKILL.md +1 -1
- package/skills/gobi-homepage/SKILL.md +16 -16
- package/skills/gobi-saved/SKILL.md +59 -0
- package/skills/gobi-saved/references/saved.md +52 -0
- package/skills/gobi-space/SKILL.md +34 -31
- package/skills/gobi-space/references/global.md +84 -24
- package/skills/gobi-space/references/space.md +45 -57
- 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
|
@@ -9,10 +9,10 @@ function readContent(value) {
|
|
|
9
9
|
return readFileSync("/dev/stdin", "utf8");
|
|
10
10
|
return value;
|
|
11
11
|
}
|
|
12
|
-
function
|
|
13
|
-
const isReply = m.
|
|
14
|
-
const id = `[${isReply ? "r" : "
|
|
15
|
-
const kind = isReply ? "reply
|
|
12
|
+
function formatFeedLine(m) {
|
|
13
|
+
const isReply = m.parentPostId != null;
|
|
14
|
+
const id = `[${isReply ? "r" : "p"}:${m.id}]`;
|
|
15
|
+
const kind = isReply ? "reply" : "post ";
|
|
16
16
|
const author = m.author?.name ||
|
|
17
17
|
`User ${m.authorId ?? "?"}`;
|
|
18
18
|
let label;
|
|
@@ -29,7 +29,7 @@ function formatMessageLine(m) {
|
|
|
29
29
|
export function registerSpaceCommand(program) {
|
|
30
30
|
const space = program
|
|
31
31
|
.command("space")
|
|
32
|
-
.description("Space commands (
|
|
32
|
+
.description("Space commands (posts, replies). Space and member admin is web-UI only.")
|
|
33
33
|
.option("--space-slug <slug>", "Space slug (overrides .gobi/settings.yaml)");
|
|
34
34
|
// ── List spaces ──
|
|
35
35
|
space
|
|
@@ -124,8 +124,8 @@ export function registerSpaceCommand(program) {
|
|
|
124
124
|
console.log(`Topics (${items.length}):\n` + lines.join("\n"));
|
|
125
125
|
});
|
|
126
126
|
space
|
|
127
|
-
.command("list-topic-
|
|
128
|
-
.description("List
|
|
127
|
+
.command("list-topic-posts <topicSlug>")
|
|
128
|
+
.description("List posts tagged with a topic in a space (cursor-paginated).")
|
|
129
129
|
.option("--limit <number>", "Items per page", "20")
|
|
130
130
|
.option("--cursor <string>", "Pagination cursor from previous response")
|
|
131
131
|
.action(async (topicSlug, opts) => {
|
|
@@ -135,7 +135,7 @@ export function registerSpaceCommand(program) {
|
|
|
135
135
|
};
|
|
136
136
|
if (opts.cursor)
|
|
137
137
|
params.cursor = opts.cursor;
|
|
138
|
-
const resp = (await apiGet(`/spaces/${spaceSlug}/topics/${topicSlug}/
|
|
138
|
+
const resp = (await apiGet(`/spaces/${spaceSlug}/topics/${topicSlug}/posts`, params));
|
|
139
139
|
const data = unwrapResp(resp);
|
|
140
140
|
const pagination = (resp.pagination || {});
|
|
141
141
|
if (isJsonMode(space)) {
|
|
@@ -143,27 +143,27 @@ export function registerSpaceCommand(program) {
|
|
|
143
143
|
return;
|
|
144
144
|
}
|
|
145
145
|
const topic = (data.topic || {});
|
|
146
|
-
const
|
|
147
|
-
if (!
|
|
148
|
-
console.log(`No
|
|
146
|
+
const posts = (data.posts || []);
|
|
147
|
+
if (!posts.length) {
|
|
148
|
+
console.log(`No posts found for topic "${topic.name || topicSlug}".`);
|
|
149
149
|
return;
|
|
150
150
|
}
|
|
151
151
|
const lines = [];
|
|
152
|
-
for (const t of
|
|
152
|
+
for (const t of posts) {
|
|
153
153
|
const author = t.author?.name || "Unknown";
|
|
154
154
|
const spaceName = t.space?.name || "";
|
|
155
155
|
lines.push(`- [${t.id}] "${t.title}" by ${author} in ${spaceName} (${t.replyCount} replies, ${t.createdAt})`);
|
|
156
156
|
}
|
|
157
157
|
const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
|
|
158
158
|
console.log(`Topic: ${topic.name || topicSlug}\n` +
|
|
159
|
-
`
|
|
159
|
+
`Posts (${posts.length} items):\n` +
|
|
160
160
|
lines.join("\n") +
|
|
161
161
|
footer);
|
|
162
162
|
});
|
|
163
|
-
// ──
|
|
163
|
+
// ── Feed (unified) ──
|
|
164
164
|
space
|
|
165
|
-
.command("
|
|
166
|
-
.description("List the unified
|
|
165
|
+
.command("feed")
|
|
166
|
+
.description("List the unified feed (posts and replies, newest first) in a space.")
|
|
167
167
|
.option("--limit <number>", "Items per page", "20")
|
|
168
168
|
.option("--cursor <string>", "Pagination cursor from previous response")
|
|
169
169
|
.action(async (opts) => {
|
|
@@ -173,7 +173,7 @@ export function registerSpaceCommand(program) {
|
|
|
173
173
|
};
|
|
174
174
|
if (opts.cursor)
|
|
175
175
|
params.cursor = opts.cursor;
|
|
176
|
-
const resp = (await apiGet(`/spaces/${spaceSlug}/
|
|
176
|
+
const resp = (await apiGet(`/spaces/${spaceSlug}/feed`, params));
|
|
177
177
|
if (isJsonMode(space)) {
|
|
178
178
|
jsonOut({
|
|
179
179
|
items: resp.data || [],
|
|
@@ -184,74 +184,69 @@ export function registerSpaceCommand(program) {
|
|
|
184
184
|
const items = (resp.data || []);
|
|
185
185
|
const pagination = (resp.pagination || {});
|
|
186
186
|
if (!items.length) {
|
|
187
|
-
console.log("No
|
|
187
|
+
console.log("No items found.");
|
|
188
188
|
return;
|
|
189
189
|
}
|
|
190
|
-
const lines = items.map(
|
|
190
|
+
const lines = items.map(formatFeedLine);
|
|
191
191
|
const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
|
|
192
|
-
console.log(`
|
|
192
|
+
console.log(`Feed (${items.length} items, newest first):\n` + lines.join("\n") + footer);
|
|
193
193
|
});
|
|
194
|
-
// ──
|
|
194
|
+
// ── Posts (get, list, create, edit, delete) ──
|
|
195
195
|
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).")
|
|
196
|
+
.command("get-post <postId>")
|
|
197
|
+
.description("Get a post with its ancestors and replies (paginated).")
|
|
221
198
|
.option("--limit <number>", "Replies per page", "20")
|
|
222
199
|
.option("--cursor <string>", "Pagination cursor from previous response")
|
|
223
|
-
.action(async (
|
|
200
|
+
.action(async (postId, opts) => {
|
|
224
201
|
const spaceSlug = resolveSpaceSlug(space);
|
|
225
202
|
const params = {
|
|
226
203
|
limit: parseInt(opts.limit, 10),
|
|
227
204
|
};
|
|
228
205
|
if (opts.cursor)
|
|
229
206
|
params.cursor = opts.cursor;
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
207
|
+
const [postResp, ancestorsResp] = await Promise.all([
|
|
208
|
+
apiGet(`/spaces/${spaceSlug}/posts/${postId}`, params),
|
|
209
|
+
apiGet(`/spaces/${spaceSlug}/posts/${postId}/ancestors`),
|
|
210
|
+
]);
|
|
211
|
+
const data = unwrapResp(postResp);
|
|
212
|
+
const pagination = (postResp.pagination || {});
|
|
213
|
+
const mentions = (postResp.mentions || {});
|
|
214
|
+
const ancestorsData = unwrapResp(ancestorsResp);
|
|
215
|
+
const ancestors = (ancestorsData.ancestors || []);
|
|
234
216
|
if (isJsonMode(space)) {
|
|
235
|
-
jsonOut({ ...data, pagination, mentions });
|
|
217
|
+
jsonOut({ ...data, ancestors, pagination, mentions });
|
|
236
218
|
return;
|
|
237
219
|
}
|
|
238
|
-
const
|
|
220
|
+
const post = (data.thread || data);
|
|
239
221
|
const replies = (data.items || []);
|
|
240
|
-
const author =
|
|
241
|
-
`User ${
|
|
222
|
+
const author = post.author?.name ||
|
|
223
|
+
`User ${post.authorId}`;
|
|
224
|
+
const ancestorLines = [];
|
|
225
|
+
if (ancestors.length) {
|
|
226
|
+
ancestors.forEach((a, i) => {
|
|
227
|
+
ancestorLines.push(` ${i + 1}. ${formatFeedLine(a)}`);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
242
230
|
const replyLines = [];
|
|
243
231
|
for (const r of replies) {
|
|
244
232
|
const rAuthor = r.author?.name ||
|
|
245
233
|
`User ${r.authorId}`;
|
|
246
234
|
const text = r.content;
|
|
247
|
-
const truncated = text.length > 200 ? text.slice(0, 200) + "
|
|
235
|
+
const truncated = text && text.length > 200 ? text.slice(0, 200) + "…" : text;
|
|
248
236
|
replyLines.push(` - ${rAuthor}: ${truncated} (${r.createdAt})`);
|
|
249
237
|
}
|
|
238
|
+
const isReplyPost = post.parentPostId != null;
|
|
239
|
+
const heading = isReplyPost
|
|
240
|
+
? `Reply [r:${post.id}]`
|
|
241
|
+
: `Post: ${post.title || "(no title)"}`;
|
|
250
242
|
const output = [
|
|
251
|
-
|
|
252
|
-
`By: ${author} on ${
|
|
243
|
+
heading,
|
|
244
|
+
`By: ${author} on ${post.createdAt}`,
|
|
245
|
+
...(ancestorLines.length
|
|
246
|
+
? ["", `Ancestors (${ancestors.length} items, root first):`, ...ancestorLines]
|
|
247
|
+
: []),
|
|
253
248
|
"",
|
|
254
|
-
|
|
249
|
+
post.content || "",
|
|
255
250
|
"",
|
|
256
251
|
`Replies (${replies.length} items):`,
|
|
257
252
|
...replyLines,
|
|
@@ -260,8 +255,8 @@ export function registerSpaceCommand(program) {
|
|
|
260
255
|
console.log(output);
|
|
261
256
|
});
|
|
262
257
|
space
|
|
263
|
-
.command("list-
|
|
264
|
-
.description("List
|
|
258
|
+
.command("list-posts")
|
|
259
|
+
.description("List posts in a space (paginated).")
|
|
265
260
|
.option("--limit <number>", "Items per page", "20")
|
|
266
261
|
.option("--cursor <string>", "Pagination cursor from previous response")
|
|
267
262
|
.action(async (opts) => {
|
|
@@ -271,7 +266,7 @@ export function registerSpaceCommand(program) {
|
|
|
271
266
|
};
|
|
272
267
|
if (opts.cursor)
|
|
273
268
|
params.cursor = opts.cursor;
|
|
274
|
-
const resp = (await apiGet(`/spaces/${spaceSlug}/
|
|
269
|
+
const resp = (await apiGet(`/spaces/${spaceSlug}/posts`, params));
|
|
275
270
|
if (isJsonMode(space)) {
|
|
276
271
|
jsonOut({
|
|
277
272
|
items: resp.data || [],
|
|
@@ -283,7 +278,7 @@ export function registerSpaceCommand(program) {
|
|
|
283
278
|
const items = (resp.data || []);
|
|
284
279
|
const pagination = (resp.pagination || {});
|
|
285
280
|
if (!items.length) {
|
|
286
|
-
console.log("No
|
|
281
|
+
console.log("No posts found.");
|
|
287
282
|
return;
|
|
288
283
|
}
|
|
289
284
|
const lines = [];
|
|
@@ -293,13 +288,13 @@ export function registerSpaceCommand(program) {
|
|
|
293
288
|
lines.push(`- [${t.id}] "${t.title}" by ${author} (${t.replyCount} replies, ${t.createdAt})`);
|
|
294
289
|
}
|
|
295
290
|
const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
|
|
296
|
-
console.log(`
|
|
291
|
+
console.log(`Posts (${items.length} items):\n` + lines.join("\n") + footer);
|
|
297
292
|
});
|
|
298
293
|
space
|
|
299
|
-
.command("create-
|
|
300
|
-
.description("Create a
|
|
301
|
-
.requiredOption("--title <title>", "Title of the
|
|
302
|
-
.requiredOption("--content <content>", "
|
|
294
|
+
.command("create-post")
|
|
295
|
+
.description("Create a post in a space.")
|
|
296
|
+
.requiredOption("--title <title>", "Title of the post")
|
|
297
|
+
.requiredOption("--content <content>", "Post content (markdown supported)")
|
|
303
298
|
.option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before posting")
|
|
304
299
|
.option("--vault-slug <vaultSlug>", "Vault slug for attachment uploads (overrides .gobi/settings.yaml)")
|
|
305
300
|
.action(async (opts) => {
|
|
@@ -311,28 +306,28 @@ export function registerSpaceCommand(program) {
|
|
|
311
306
|
await uploadAttachments(vaultSlug, links, token, { addToSyncfiles: true });
|
|
312
307
|
}
|
|
313
308
|
const spaceSlug = resolveSpaceSlug(space);
|
|
314
|
-
const resp = (await apiPost(`/spaces/${spaceSlug}/
|
|
309
|
+
const resp = (await apiPost(`/spaces/${spaceSlug}/posts`, {
|
|
315
310
|
title: opts.title,
|
|
316
311
|
content,
|
|
317
312
|
}));
|
|
318
|
-
const
|
|
313
|
+
const post = unwrapResp(resp);
|
|
319
314
|
if (isJsonMode(space)) {
|
|
320
|
-
jsonOut(
|
|
315
|
+
jsonOut(post);
|
|
321
316
|
return;
|
|
322
317
|
}
|
|
323
|
-
console.log(`
|
|
324
|
-
` ID: ${
|
|
325
|
-
` Title: ${
|
|
326
|
-
` Created: ${
|
|
318
|
+
console.log(`Post created!\n` +
|
|
319
|
+
` ID: ${post.id}\n` +
|
|
320
|
+
` Title: ${post.title}\n` +
|
|
321
|
+
` Created: ${post.createdAt}`);
|
|
327
322
|
});
|
|
328
323
|
space
|
|
329
|
-
.command("edit-
|
|
330
|
-
.description("Edit a
|
|
331
|
-
.option("--title <title>", "New title for the
|
|
332
|
-
.option("--content <content>", "New content for the
|
|
324
|
+
.command("edit-post <postId>")
|
|
325
|
+
.description("Edit a post. You must be the author.")
|
|
326
|
+
.option("--title <title>", "New title for the post")
|
|
327
|
+
.option("--content <content>", "New content for the post (markdown supported)")
|
|
333
328
|
.option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before editing")
|
|
334
329
|
.option("--vault-slug <vaultSlug>", "Vault slug for attachment uploads (overrides .gobi/settings.yaml)")
|
|
335
|
-
.action(async (
|
|
330
|
+
.action(async (postId, opts) => {
|
|
336
331
|
if (!opts.title && !opts.content) {
|
|
337
332
|
throw new Error("Provide at least --title or --content to update.");
|
|
338
333
|
}
|
|
@@ -350,37 +345,37 @@ export function registerSpaceCommand(program) {
|
|
|
350
345
|
}
|
|
351
346
|
body.content = content;
|
|
352
347
|
}
|
|
353
|
-
const resp = (await apiPatch(`/spaces/${spaceSlug}/
|
|
354
|
-
const
|
|
348
|
+
const resp = (await apiPatch(`/spaces/${spaceSlug}/posts/${postId}`, body));
|
|
349
|
+
const post = unwrapResp(resp);
|
|
355
350
|
if (isJsonMode(space)) {
|
|
356
|
-
jsonOut(
|
|
351
|
+
jsonOut(post);
|
|
357
352
|
return;
|
|
358
353
|
}
|
|
359
|
-
console.log(`
|
|
360
|
-
` ID: ${
|
|
361
|
-
` Title: ${
|
|
362
|
-
` Edited: ${
|
|
354
|
+
console.log(`Post edited!\n` +
|
|
355
|
+
` ID: ${post.id}\n` +
|
|
356
|
+
` Title: ${post.title}\n` +
|
|
357
|
+
` Edited: ${post.editedAt}`);
|
|
363
358
|
});
|
|
364
359
|
space
|
|
365
|
-
.command("delete-
|
|
366
|
-
.description("Delete a
|
|
367
|
-
.action(async (
|
|
360
|
+
.command("delete-post <postId>")
|
|
361
|
+
.description("Delete a post. You must be the author.")
|
|
362
|
+
.action(async (postId) => {
|
|
368
363
|
const spaceSlug = resolveSpaceSlug(space);
|
|
369
|
-
await apiDelete(`/spaces/${spaceSlug}/
|
|
364
|
+
await apiDelete(`/spaces/${spaceSlug}/posts/${postId}`);
|
|
370
365
|
if (isJsonMode(space)) {
|
|
371
|
-
jsonOut({ id:
|
|
366
|
+
jsonOut({ id: postId });
|
|
372
367
|
return;
|
|
373
368
|
}
|
|
374
|
-
console.log(`
|
|
369
|
+
console.log(`Post ${postId} deleted.`);
|
|
375
370
|
});
|
|
376
371
|
// ── Replies (create, edit, delete) ──
|
|
377
372
|
space
|
|
378
|
-
.command("create-reply <
|
|
379
|
-
.description("Create a reply to a
|
|
373
|
+
.command("create-reply <postId>")
|
|
374
|
+
.description("Create a reply to a post in a space.")
|
|
380
375
|
.requiredOption("--content <content>", "Reply content (markdown supported)")
|
|
381
|
-
.action(async (
|
|
376
|
+
.action(async (postId, opts) => {
|
|
382
377
|
const spaceSlug = resolveSpaceSlug(space);
|
|
383
|
-
const resp = (await apiPost(`/spaces/${spaceSlug}/
|
|
378
|
+
const resp = (await apiPost(`/spaces/${spaceSlug}/posts/${postId}/replies`, { content: readContent(opts.content) }));
|
|
384
379
|
const msg = unwrapResp(resp);
|
|
385
380
|
const mentions = (resp.mentions || {});
|
|
386
381
|
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
|
-
}
|
|
@@ -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
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -6,12 +6,10 @@ import { registerAuthCommand } from "./commands/auth.js";
|
|
|
6
6
|
import { registerInitCommand, printContext } from "./commands/init.js";
|
|
7
7
|
import { registerSpaceCommand } from "./commands/space.js";
|
|
8
8
|
import { registerGlobalCommand } from "./commands/global.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { registerNotesCommand } from "./commands/notes.js";
|
|
9
|
+
import { registerVaultCommand } from "./commands/vault.js";
|
|
10
|
+
import { registerSavedCommand } from "./commands/saved.js";
|
|
12
11
|
import { registerSessionsCommand } from "./commands/sessions.js";
|
|
13
12
|
import { registerSenseCommand } from "./commands/sense.js";
|
|
14
|
-
import { registerSyncCommand } from "./commands/sync.js";
|
|
15
13
|
import { registerUpdateCommand } from "./commands/update.js";
|
|
16
14
|
import { registerMediaCommand } from "./commands/media.js";
|
|
17
15
|
import { registerDraftCommand } from "./commands/draft.js";
|
|
@@ -37,12 +35,10 @@ export async function cli() {
|
|
|
37
35
|
registerInitCommand(program);
|
|
38
36
|
registerSpaceCommand(program);
|
|
39
37
|
registerGlobalCommand(program);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
registerNotesCommand(program);
|
|
38
|
+
registerVaultCommand(program);
|
|
39
|
+
registerSavedCommand(program);
|
|
43
40
|
registerSessionsCommand(program);
|
|
44
41
|
registerSenseCommand(program);
|
|
45
|
-
registerSyncCommand(program);
|
|
46
42
|
registerUpdateCommand(program);
|
|
47
43
|
registerMediaCommand(program);
|
|
48
44
|
registerDraftCommand(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gobi-ai/cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "CLI client for the Gobi collaborative knowledge platform",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"keywords": [
|
|
16
16
|
"gobi",
|
|
17
17
|
"cli",
|
|
18
|
-
"
|
|
18
|
+
"vault",
|
|
19
19
|
"knowledge"
|
|
20
20
|
],
|
|
21
21
|
"publishConfig": {
|
|
@@ -38,9 +38,8 @@ brew tap gobi-ai/tap && brew install gobi
|
|
|
38
38
|
|
|
39
39
|
## Key Concepts
|
|
40
40
|
|
|
41
|
-
- **Space**: A shared space for a group or community. A logged-in user can be a member of one or more spaces. A space contains
|
|
42
|
-
- **Vault**: A filetree storage of information and knowledge. A local directory becomes a vault when it contains `.gobi/settings.yaml` with a vault slug and a space slug. Each vault is identified by a slug (e.g. `brave-path-zr962w`).
|
|
43
|
-
- **Brain**: Another name for a vault when referring to its AI-searchable knowledge. You can search brains, ask them questions, and publish a `BRAIN.md` document to configure your vault's brain.
|
|
41
|
+
- **Space**: A shared space for a group or community. A logged-in user can be a member of one or more spaces. A space contains posts, replies, sessions, and connected vaults.
|
|
42
|
+
- **Vault**: A filetree storage of information and knowledge. A local directory becomes a vault when it contains `.gobi/settings.yaml` with a vault slug and a space slug. Each vault is identified by a slug (e.g. `brave-path-zr962w`). Configure a vault's public profile with a `PUBLISH.md` document and `gobi vault publish`.
|
|
44
43
|
|
|
45
44
|
## First-Time Setup
|
|
46
45
|
|
|
@@ -56,7 +55,7 @@ This is an **interactive** command that:
|
|
|
56
55
|
1. Logs in automatically if not already authenticated (opens a browser URL for Google OAuth)
|
|
57
56
|
2. Prompts the user to select an existing vault or create a new one
|
|
58
57
|
3. Writes `.gobi/settings.yaml` in the current directory with the chosen vault slug
|
|
59
|
-
4. Creates a `
|
|
58
|
+
4. Creates a `PUBLISH.md` file if one doesn't exist
|
|
60
59
|
|
|
61
60
|
### Step 2: Select a Space
|
|
62
61
|
|
|
@@ -111,7 +110,7 @@ JSON responses have the shape `{ "success": true, "data": ... }` on success or `
|
|
|
111
110
|
- `gobi session get` — Get a session and its messages (paginated).
|
|
112
111
|
- `gobi session list` — List all sessions you are part of, sorted by most recent activity.
|
|
113
112
|
- `gobi session reply` — Send a human reply to a session you are a member of.
|
|
114
|
-
- `gobi sync` — Sync local vault files with Gobi Webdrive.
|
|
113
|
+
- `gobi vault sync` — Sync local vault files with Gobi Webdrive.
|
|
115
114
|
- `gobi update` — Update gobi-cli to the latest version.
|
|
116
115
|
|
|
117
116
|
## Reference Documentation
|
|
@@ -119,7 +118,6 @@ JSON responses have the shape `{ "success": true, "data": ... }` on success or `
|
|
|
119
118
|
- [gobi auth](references/auth.md)
|
|
120
119
|
- [gobi init](references/init.md)
|
|
121
120
|
- [gobi session](references/session.md)
|
|
122
|
-
- [gobi sync](references/sync.md)
|
|
123
121
|
- [gobi update](references/update.md)
|
|
124
122
|
|
|
125
123
|
## Configuration Files
|
|
@@ -128,7 +126,7 @@ JSON responses have the shape `{ "success": true, "data": ... }` on success or `
|
|
|
128
126
|
|------|-------------|
|
|
129
127
|
| `~/.gobi/credentials.json` | Stored authentication tokens (auto-managed) |
|
|
130
128
|
| `.gobi/settings.yaml` | Per-project vault and space configuration |
|
|
131
|
-
| `
|
|
129
|
+
| `PUBLISH.md` | Vault profile document with YAML frontmatter, published via `gobi vault publish` |
|
|
132
130
|
|
|
133
131
|
## Environment Variables
|
|
134
132
|
|