@gobi-ai/cli 1.3.7 → 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.
Files changed (37) hide show
  1. package/.claude-plugin/marketplace.json +6 -7
  2. package/.claude-plugin/plugin.json +4 -5
  3. package/README.md +78 -89
  4. package/commands/space-explore.md +10 -10
  5. package/commands/space-share.md +13 -7
  6. package/dist/commands/draft.js +213 -0
  7. package/dist/commands/global.js +205 -70
  8. package/dist/commands/init.js +5 -5
  9. package/dist/commands/{notes.js → saved.js} +112 -19
  10. package/dist/commands/space.js +92 -97
  11. package/dist/commands/sync.js +2 -56
  12. package/dist/commands/vault.js +113 -0
  13. package/dist/main.js +6 -10
  14. package/package.json +2 -2
  15. package/skills/gobi-core/SKILL.md +5 -7
  16. package/skills/gobi-core/references/space.md +18 -19
  17. package/skills/gobi-draft/SKILL.md +74 -0
  18. package/skills/gobi-draft/references/draft.md +109 -0
  19. package/skills/gobi-homepage/SKILL.md +16 -16
  20. package/skills/gobi-saved/SKILL.md +59 -0
  21. package/skills/gobi-saved/references/saved.md +52 -0
  22. package/skills/gobi-space/SKILL.md +34 -31
  23. package/skills/gobi-space/references/global.md +84 -24
  24. package/skills/gobi-space/references/space.md +45 -57
  25. package/skills/gobi-vault/SKILL.md +92 -0
  26. package/skills/{gobi-core/references/sync.md → gobi-vault/references/vault.md} +41 -2
  27. package/dist/commands/brain.js +0 -141
  28. package/dist/commands/feed.js +0 -148
  29. package/dist/commands/proposal.js +0 -185
  30. package/skills/gobi-brain/SKILL.md +0 -100
  31. package/skills/gobi-brain/references/brain.md +0 -66
  32. package/skills/gobi-feed/SKILL.md +0 -43
  33. package/skills/gobi-feed/references/feed.md +0 -80
  34. package/skills/gobi-notes/SKILL.md +0 -52
  35. package/skills/gobi-notes/references/notes.md +0 -82
  36. package/skills/gobi-proposal/SKILL.md +0 -66
  37. package/skills/gobi-proposal/references/proposal.md +0 -116
@@ -9,10 +9,10 @@ function readContent(value) {
9
9
  return readFileSync("/dev/stdin", "utf8");
10
10
  return value;
11
11
  }
12
- function formatMessageLine(m) {
13
- const isReply = m.parentThreadId != null;
14
- const id = `[${isReply ? "r" : "t"}:${m.id}]`;
15
- const kind = isReply ? "reply " : "thread";
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 (threads, replies). Space and member admin is web-UI only.")
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-threads <topicSlug>")
128
- .description("List threads tagged with a topic in a space (cursor-paginated).")
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}/threads`, params));
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 threads = (data.threads || []);
147
- if (!threads.length) {
148
- console.log(`No threads found for topic "${topic.name || topicSlug}".`);
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 threads) {
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
- `Threads (${threads.length} items):\n` +
159
+ `Posts (${posts.length} items):\n` +
160
160
  lines.join("\n") +
161
161
  footer);
162
162
  });
163
- // ── Messages (unified feed) ──
163
+ // ── Feed (unified) ──
164
164
  space
165
- .command("messages")
166
- .description("List the unified message feed (threads and replies, newest first) in a space.")
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}/messages`, params));
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 messages found.");
187
+ console.log("No items found.");
188
188
  return;
189
189
  }
190
- const lines = items.map(formatMessageLine);
190
+ const lines = items.map(formatFeedLine);
191
191
  const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
192
- console.log(`Messages (${items.length} items, newest first):\n` + lines.join("\n") + footer);
192
+ console.log(`Feed (${items.length} items, newest first):\n` + lines.join("\n") + footer);
193
193
  });
194
- // ── Ancestors ──
194
+ // ── Posts (get, list, create, edit, delete) ──
195
195
  space
196
- .command("ancestors <threadId>")
197
- .description("Show the ancestor lineage of a thread or reply (root → immediate parent).")
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 (threadId, opts) => {
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 resp = (await apiGet(`/spaces/${spaceSlug}/threads/${threadId}`, params));
231
- const data = unwrapResp(resp);
232
- const pagination = (resp.pagination || {});
233
- const mentions = (resp.mentions || {});
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 thread = (data.thread || data);
220
+ const post = (data.thread || data);
239
221
  const replies = (data.items || []);
240
- const author = thread.author?.name ||
241
- `User ${thread.authorId}`;
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) + "\u2026" : text;
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
- `Thread: ${thread.title}`,
252
- `By: ${author} on ${thread.createdAt}`,
243
+ heading,
244
+ `By: ${author} on ${post.createdAt}`,
245
+ ...(ancestorLines.length
246
+ ? ["", `Ancestors (${ancestors.length} items, root first):`, ...ancestorLines]
247
+ : []),
253
248
  "",
254
- thread.content,
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-threads")
264
- .description("List threads in a space (paginated).")
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}/threads`, params));
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 threads found.");
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(`Threads (${items.length} items):\n` + lines.join("\n") + footer);
291
+ console.log(`Posts (${items.length} items):\n` + lines.join("\n") + footer);
297
292
  });
298
293
  space
299
- .command("create-thread")
300
- .description("Create a thread in a space.")
301
- .requiredOption("--title <title>", "Title of the thread")
302
- .requiredOption("--content <content>", "Thread content (markdown supported)")
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}/threads`, {
309
+ const resp = (await apiPost(`/spaces/${spaceSlug}/posts`, {
315
310
  title: opts.title,
316
311
  content,
317
312
  }));
318
- const thread = unwrapResp(resp);
313
+ const post = unwrapResp(resp);
319
314
  if (isJsonMode(space)) {
320
- jsonOut(thread);
315
+ jsonOut(post);
321
316
  return;
322
317
  }
323
- console.log(`Thread created!\n` +
324
- ` ID: ${thread.id}\n` +
325
- ` Title: ${thread.title}\n` +
326
- ` Created: ${thread.createdAt}`);
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-thread <threadId>")
330
- .description("Edit a thread. You must be the author.")
331
- .option("--title <title>", "New title for the thread")
332
- .option("--content <content>", "New content for the thread (markdown supported)")
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 (threadId, opts) => {
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}/threads/${threadId}`, body));
354
- const thread = unwrapResp(resp);
348
+ const resp = (await apiPatch(`/spaces/${spaceSlug}/posts/${postId}`, body));
349
+ const post = unwrapResp(resp);
355
350
  if (isJsonMode(space)) {
356
- jsonOut(thread);
351
+ jsonOut(post);
357
352
  return;
358
353
  }
359
- console.log(`Thread edited!\n` +
360
- ` ID: ${thread.id}\n` +
361
- ` Title: ${thread.title}\n` +
362
- ` Edited: ${thread.editedAt}`);
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-thread <threadId>")
366
- .description("Delete a thread. You must be the author.")
367
- .action(async (threadId) => {
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}/threads/${threadId}`);
364
+ await apiDelete(`/spaces/${spaceSlug}/posts/${postId}`);
370
365
  if (isJsonMode(space)) {
371
- jsonOut({ id: threadId });
366
+ jsonOut({ id: postId });
372
367
  return;
373
368
  }
374
- console.log(`Thread ${threadId} deleted.`);
369
+ console.log(`Post ${postId} deleted.`);
375
370
  });
376
371
  // ── Replies (create, edit, delete) ──
377
372
  space
378
- .command("create-reply <threadId>")
379
- .description("Create a reply to a thread in a space.")
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 (threadId, opts) => {
376
+ .action(async (postId, opts) => {
382
377
  const spaceSlug = resolveSpaceSlug(space);
383
- const resp = (await apiPost(`/spaces/${spaceSlug}/threads/${threadId}/replies`, { content: readContent(opts.content) }));
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)) {
@@ -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, resolve as pathResolve } from "path";
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 { getVaultSlug } from "./init.js";
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,15 +6,13 @@ 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 { registerBrainCommand } from "./commands/brain.js";
10
- import { registerFeedCommand } from "./commands/feed.js";
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
- import { registerProposalCommand } from "./commands/proposal.js";
15
+ import { registerDraftCommand } from "./commands/draft.js";
18
16
  const require = createRequire(import.meta.url);
19
17
  const { version } = require("../package.json");
20
18
  const SKIP_BANNER_COMMANDS = new Set(["auth", "init", "update"]);
@@ -37,15 +35,13 @@ export async function cli() {
37
35
  registerInitCommand(program);
38
36
  registerSpaceCommand(program);
39
37
  registerGlobalCommand(program);
40
- registerBrainCommand(program);
41
- registerFeedCommand(program);
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
- registerProposalCommand(program);
44
+ registerDraftCommand(program);
49
45
  // Propagate helpWidth to all subcommands
50
46
  const helpWidth = process.stdout.columns || 200;
51
47
  for (const cmd of program.commands) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobi-ai/cli",
3
- "version": "1.3.7",
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
- "second-brain",
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 threads, sessions, brain updates, 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`).
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 `BRAIN.md` file if one doesn't exist
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
- | `BRAIN.md` | Brain document with YAML frontmatter, published via `gobi brain publish` |
129
+ | `PUBLISH.md` | Vault profile document with YAML frontmatter, published via `gobi vault publish` |
132
130
 
133
131
  ## Environment Variables
134
132