@gobi-ai/cli 2.0.4 → 2.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,12 +4,12 @@
4
4
  "name": "gobi-ai"
5
5
  },
6
6
  "description": "Claude Code plugin for the Gobi collaborative knowledge platform CLI",
7
- "version": "2.0.4",
7
+ "version": "2.0.6",
8
8
  "plugins": [
9
9
  {
10
10
  "name": "gobi",
11
11
  "description": "Manage the Gobi collaborative knowledge platform from the command line. Publish vault profiles, create posts and replies, manage saved notes and posts, manage sessions, generate images and videos.",
12
- "version": "2.0.4",
12
+ "version": "2.0.6",
13
13
  "author": {
14
14
  "name": "gobi-ai"
15
15
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gobi",
3
3
  "description": "Manage the Gobi collaborative knowledge platform from the command line",
4
- "version": "2.0.4",
4
+ "version": "2.0.6",
5
5
  "author": {
6
6
  "name": "gobi-ai"
7
7
  },
package/README.md CHANGED
@@ -38,10 +38,13 @@ npm link
38
38
  ## Quick start
39
39
 
40
40
  ```sh
41
- # Initialize log in and set up your vault (creates PUBLISH.md if missing)
42
- gobi init
41
+ # Sign in (device-code flow opens a URL, you authorize, the CLI polls)
42
+ gobi auth login
43
43
 
44
- # Select a community space
44
+ # Set up the vault for the current directory (creates PUBLISH.md if missing)
45
+ gobi vault init
46
+
47
+ # Select a community space for the current directory
45
48
  gobi space warp
46
49
 
47
50
  # Publish your vault profile (after editing PUBLISH.md frontmatter)
@@ -52,9 +55,17 @@ gobi vault sync
52
55
 
53
56
  # Browse the global feed and create a personal post
54
57
  gobi global feed
55
- gobi global create-post --title "Hello" --content "Trying gobi" --vault-slug my-vault
58
+ gobi global create-post --title "Hello" --content "Trying gobi"
56
59
  ```
57
60
 
61
+ Each setup step unlocks a different family of commands — run only the ones the workflow needs:
62
+
63
+ | Step | Unlocks |
64
+ |------|---------|
65
+ | `gobi auth login` | All authenticated commands |
66
+ | `gobi vault init` | Every `gobi vault …` command (`publish`, `unpublish`, `sync`) and lets `global create-post` default to that vault |
67
+ | `gobi space warp` | Every `gobi space …` command without needing `--space-slug` |
68
+
58
69
  ---
59
70
 
60
71
  ## Using gobi from an agent
@@ -82,7 +93,7 @@ The CLI looks up two pieces of state:
82
93
  | Path | What | Who manages |
83
94
  |------|------|-------------|
84
95
  | `~/.gobi/credentials.json` | Auth tokens (`accessToken`, `refreshToken`) | `gobi auth login` writes; `gobi auth logout` clears |
85
- | `.gobi/settings.yaml` | Per-project `vaultSlug` and `selectedSpaceSlug` | `gobi init` and `gobi space warp` write |
96
+ | `.gobi/settings.yaml` | Per-project `vaultSlug` and `selectedSpaceSlug` | `gobi vault init` and `gobi space warp` write |
86
97
 
87
98
  An agent should check these before calling commands that need a vault or space:
88
99
 
@@ -94,9 +105,9 @@ gobi --json auth status
94
105
  cat .gobi/settings.yaml 2>/dev/null
95
106
  ```
96
107
 
97
- If `.gobi/settings.yaml` is missing, `gobi init` and `gobi space warp` are the interactive entry points — they require user input, so an agent should hand off to the user rather than trying to drive them silently.
108
+ If `.gobi/settings.yaml` is missing, `gobi vault init` and `gobi space warp` are the interactive entry points — they require user input, so an agent should hand off to the user rather than trying to drive them silently.
98
109
 
99
- Every command that depends on a vault or space accepts an explicit override (`--vault-slug`, `--space-slug`) so an agent can act without ambient state.
110
+ `gobi space …` commands accept `--space-slug <slug>` (on the parent group or any subcommand) to override the default space. Per-command `--vault-slug` overrides are documented inline.
100
111
 
101
112
  ### Headless auth
102
113
 
@@ -122,7 +133,8 @@ When the runtime exports `GOBI_SESSION_ID`, `gobi draft add` picks it up automat
122
133
 
123
134
  | Command | Description |
124
135
  |---------|-------------|
125
- | `gobi init` | Log in (if needed) and select or create a vault. Creates `PUBLISH.md` if missing. |
136
+ | `gobi vault init` | Select or create the vault for this directory. Writes `vaultSlug` to `.gobi/settings.yaml` and seeds `PUBLISH.md`. |
137
+ | `gobi vault list` | List vaults you own |
126
138
  | `gobi space list` | List spaces you are a member of |
127
139
  | `gobi space warp [spaceSlug]` | Select the active space (interactive if slug omitted) |
128
140
 
@@ -164,13 +176,13 @@ A *Space* is a community knowledge area. A *Space Post* lives in one space. The
164
176
  | `gobi space list-topics` | List topics in the space, ordered by most recent linkage |
165
177
  | `gobi space list-topic-posts <topicSlug>` | List posts tagged with a topic |
166
178
  | `gobi space list-posts` | List posts in the space |
167
- | `gobi space get-post <postId>` | Get a post with its ancestors and replies |
179
+ | `gobi space get-post <postId> [--full]` | Get a post with its ancestors and replies. `--full` shows reply content without truncation. |
168
180
  | `gobi space create-post --title <t> --content <c> [--vault-slug <slug>] [--auto-attachments]` | Create a space post. `--vault-slug` attributes it to a vault you own; `--auto-attachments` uploads `[[wikilinks]]` to that vault and uses it as `authorVaultSlug`. |
169
181
  | `gobi space edit-post <postId> [--title <t>] [--content <c>] [--vault-slug <slug>] [--auto-attachments]` | Edit a space post. `--vault-slug ""` detaches the vault. |
170
182
  | `gobi space delete-post <postId>` | Delete a space post |
171
- | `gobi space create-reply <postId> --content <c>` | Reply to a space post |
172
- | `gobi space edit-reply <replyId> --content <c>` | Edit a reply |
173
- | `gobi space delete-reply <replyId>` | Delete a reply |
183
+ | `gobi space create-reply <postId> (--content <c> \| --rich-text <json>) [--vault-slug <slug>] [--auto-attachments]` | Create a reply to a space post |
184
+ | `gobi space edit-reply <replyId> [--content <c>] [--rich-text <json>] [--vault-slug <slug>] [--auto-attachments]` | Edit a reply you authored. `--vault-slug ""` detaches attribution. |
185
+ | `gobi space delete-reply <replyId>` | Delete a reply you authored |
174
186
 
175
187
  ### Global feed (personal posts)
176
188
 
@@ -178,14 +190,14 @@ A *Personal Post* lives on the author's profile (their primary vault) and surfac
178
190
 
179
191
  | Command | Description |
180
192
  |---------|-------------|
181
- | `gobi global feed` | List the global public feed (posts + replies, newest first) |
193
+ | `gobi global feed [--following]` | List the global public feed (posts + replies, newest first). `--following` limits to authors you follow. |
182
194
  | `gobi global list-posts [--mine] [--vault-slug <slug>]` | List personal posts; filter to your own or by author vault |
183
- | `gobi global get-post <postId>` | Get a personal post with its ancestors and replies |
195
+ | `gobi global get-post <postId> [--full]` | Get a personal post with its ancestors and replies. `--full` shows reply content without truncation. |
184
196
  | `gobi global create-post [--title <t>] (--content <c> \| --rich-text <json>) [--vault-slug <slug>] [--auto-attachments]` | Create a personal post |
185
197
  | `gobi global edit-post <postId> [--title <t>] [--content <c>] [--vault-slug <slug>]` | Edit a personal post you authored. `--vault-slug ""` detaches the vault. |
186
198
  | `gobi global delete-post <postId>` | Delete a personal post you authored |
187
- | `gobi global create-reply <postId> (--content <c> \| --rich-text <json>)` | Reply to a personal post |
188
- | `gobi global edit-reply <replyId> --content <c>` | Edit a reply you authored |
199
+ | `gobi global create-reply <postId> (--content <c> \| --rich-text <json>) [--vault-slug <slug>] [--auto-attachments]` | Create a reply to a personal post |
200
+ | `gobi global edit-reply <replyId> [--content <c>] [--rich-text <json>] [--vault-slug <slug>] [--auto-attachments]` | Edit a reply you authored. `--vault-slug ""` detaches attribution. |
189
201
  | `gobi global delete-reply <replyId>` | Delete a reply you authored |
190
202
 
191
203
  `--vault-slug` requires that the caller hold `role: 'owner'` on the target vault. When set, it becomes the post's `authorVaultSlug`. When `--auto-attachments` is set, the same vault is used both as the upload destination for `[[wikilinks]]` and as `authorVaultSlug`.
@@ -196,9 +208,9 @@ A *Personal Post* lives on the author's profile (their primary vault) and surfac
196
208
  |---------|-------------|
197
209
  | `gobi session list` | List your sessions |
198
210
  | `gobi session get <id>` | Get a session and its messages |
199
- | `gobi session reply <id> --content <c>` | Send a message in a session |
211
+ | `gobi session create-reply <id> --content <c>` | Send a message in a session |
200
212
 
201
- `session reply` also accepts `--rich-text <json>` (mutually exclusive with `--content`).
213
+ `session create-reply` also accepts `--rich-text <json>` (mutually exclusive with `--content`).
202
214
 
203
215
  ### Sense
204
216
 
@@ -206,8 +218,8 @@ Activity and transcription data captured by Gobi Sense (or the mobile app).
206
218
 
207
219
  | Command | Description |
208
220
  |---------|-------------|
209
- | `gobi sense activities --start-time <iso> --end-time <iso>` | Fetch activity records in a time range |
210
- | `gobi sense transcriptions --start-time <iso> --end-time <iso>` | Fetch transcription records in a time range |
221
+ | `gobi sense list-activities --start-time <iso> --end-time <iso>` | List activity records in a time range |
222
+ | `gobi sense list-transcriptions --start-time <iso> --end-time <iso>` | List transcription records in a time range |
211
223
 
212
224
  Times are ISO 8601 UTC (e.g. `2026-03-20T00:00:00Z`).
213
225
 
@@ -219,22 +231,22 @@ Times are ISO 8601 UTC (e.g. `2026-03-20T00:00:00Z`).
219
231
 
220
232
  | Command | Description |
221
233
  |---------|-------------|
222
- | `gobi saved note list [--date YYYY-MM-DD]` | List your notes (recent via cursor, or all for a day) |
223
- | `gobi saved note get <id>` | Get a single note |
224
- | `gobi saved note create --content <c>` | Create a note (use `-` to read content from stdin) |
225
- | `gobi saved note edit <id> [--content <c>] [--agent-id <id>]` | Edit a note (`--agent-id null` clears the link) |
226
- | `gobi saved note delete <id>` | Delete a note you authored |
234
+ | `gobi saved list-notes [--date YYYY-MM-DD]` | List your notes (recent via cursor, or all for a day) |
235
+ | `gobi saved get-note <id>` | Get a single note |
236
+ | `gobi saved create-note --content <c>` | Create a note (use `-` to read content from stdin) |
237
+ | `gobi saved edit-note <id> [--content <c>] [--agent-id <id>]` | Edit a note (`--agent-id null` clears the link) |
238
+ | `gobi saved delete-note <id>` | Delete a note you authored |
227
239
 
228
- `saved note list` and `saved note create` accept `--timezone <iana>` (default: system timezone).
240
+ `saved list-notes` and `saved create-note` accept `--timezone <iana>` (default: system timezone).
229
241
 
230
- #### Saved posts
242
+ #### Saved posts (bookmarks)
231
243
 
232
244
  | Command | Description |
233
245
  |---------|-------------|
234
- | `gobi saved post list [--type all\|article\|space-post]` | List posts you've saved |
235
- | `gobi saved post get <postId>` | Get a saved post snapshot |
236
- | `gobi saved post create --source <id>` | Save a post or reply by id (records a snapshot) |
237
- | `gobi saved post delete <postId>` | Remove a post from your saved collection |
246
+ | `gobi saved list-posts [--type all\|article\|space-post]` | List posts you've bookmarked |
247
+ | `gobi saved get-post <postId>` | Get a saved post snapshot |
248
+ | `gobi saved create-post --source <id>` | Bookmark a post or reply by id (records a snapshot) |
249
+ | `gobi saved delete-post <postId>` | Remove a post from your saved collection |
238
250
 
239
251
  ### Drafts
240
252
 
@@ -247,7 +259,7 @@ Each action is `{ label, message? }`: `label` is the short button text (1–80 c
247
259
  | Command | Description |
248
260
  |---------|-------------|
249
261
  | `gobi draft list [--limit N]` | List drafts (priority ASC, then newest first) |
250
- | `gobi draft get <id>` | Show one draft with its history and suggested actions |
262
+ | `gobi draft get <id>` | Get one draft with its history and suggested actions |
251
263
  | `gobi draft add <title> <content> [--session <id>] [--priority N] [--action <label[::message]>]…` | Add a draft. Pass `--action` up to 3 times; each action is `Label` or `Label::Message`. `--session` falls back to `$GOBI_SESSION_ID`. Use `-` for content to read from stdin. |
252
264
  | `gobi draft delete <id>` | Delete a draft |
253
265
  | `gobi draft prioritize <id> <priority>` | Set priority (lower = higher) |
@@ -260,12 +272,14 @@ Image, video, and avatar generation. See the `gobi-media` skill for full workflo
260
272
 
261
273
  | Command | Description |
262
274
  |---------|-------------|
263
- | `gobi media image-generate --prompt <p> [--aspect-ratio <r>] [-o <file>]` | Generate an image (use `-o` to wait + download) |
264
- | `gobi media image-edit --image <f> --prompt <p>` | Edit/inpaint an image |
265
- | `gobi media video-create --avatar-id <a> --voice-id <v> --script <s>` | Avatar video with voice narration |
266
- | `gobi media cinematic-create --prompt <p>` | Cinematic video from a text prompt |
267
- | `gobi media avatar-design / avatar-from-selfie` | Custom avatars from prompts or selfies |
268
- | `gobi media avatars` / `gobi media voices` | List available avatars and voices |
275
+ | `gobi media generate-image --prompt <p> [--aspect-ratio <r>] [-o <file>]` | Generate an image (use `-o` to wait + download) |
276
+ | `gobi media edit-image --image <f> --prompt <p>` | Edit an image with a prompt |
277
+ | `gobi media inpaint-image --image <f> --mask <m> --prompt <p>` | Inpaint a masked region |
278
+ | `gobi media create-video --avatar-id <a> --voice-id <v> --script <s>` | Avatar video with voice narration |
279
+ | `gobi media create-cinematic --prompt <p>` | Cinematic video from a text prompt |
280
+ | `gobi media design-avatar / design-avatar-from-selfie` | Custom avatars from prompts or selfies |
281
+ | `gobi media list-avatars` / `gobi media list-voices` | List available avatars and voices |
282
+ | `gobi media list-videos` / `gobi media get-video <id>` | List or get videos |
269
283
  | `gobi media upload <file>` | Upload a local file and get a media id |
270
284
 
271
285
  ### Global options
@@ -302,13 +316,13 @@ The CLI ships a `.claude-plugin/` manifest with eight skills that wrap the comma
302
316
 
303
317
  | Skill | Covers |
304
318
  |-------|--------|
305
- | `gobi-core` | Auth, init, session, update, space list/warp |
306
- | `gobi-vault` | `gobi vault publish/unpublish/sync` |
319
+ | `gobi-core` | Auth, session, update, space list/warp |
320
+ | `gobi-vault` | `gobi vault init/list/publish/unpublish/sync` |
307
321
  | `gobi-space` | `gobi space …` and `gobi global …` |
308
- | `gobi-saved` | `gobi saved note …` and `gobi saved post …` |
322
+ | `gobi-saved` | `gobi saved list-notes/create-note/list-posts/create-post/…` |
309
323
  | `gobi-draft` | `gobi draft …` |
310
324
  | `gobi-media` | `gobi media …` |
311
- | `gobi-sense` | `gobi sense activities/transcriptions` |
325
+ | `gobi-sense` | `gobi sense list-activities/list-transcriptions` |
312
326
  | `gobi-homepage` | Building custom HTML homepages with `window.gobi` |
313
327
 
314
328
  Each skill's `SKILL.md` is hand-written orientation; `references/` is regenerated from `--help` output by `npm run generate-skill-docs`.
@@ -14,7 +14,7 @@ First, verify the user is set up:
14
14
  gobi --json auth status
15
15
  ```
16
16
 
17
- Check that `.gobi/settings.yaml` exists and contains both `vaultSlug` and `selectedSpaceSlug`. If not, stop and ask the user to run `gobi init` and `gobi space warp` first.
17
+ Check that `.gobi/settings.yaml` exists. If you plan to publish to a Space (`gobi space create-post`), it must contain `selectedSpaceSlug` otherwise stop and ask the user to run `gobi space warp` first. `vaultSlug` is optional for `gobi global create-post`; if missing the post is created without an `authorVaultSlug` (vault-less). Run `gobi vault init` if the user wants the post attributed to a vault they own.
18
18
 
19
19
  ## Draft a personal post
20
20
 
@@ -1,7 +1,8 @@
1
1
  import { BASE_URL, POLL_MAX_DURATION_MS } from "../constants.js";
2
2
  import { DeviceCodeError } from "../errors.js";
3
3
  import { storeTokens, logout, isAuthenticated, getCurrentUser, } from "../auth/manager.js";
4
- import { printContext } from "./init.js";
4
+ import { readSettings } from "./init.js";
5
+ import { isJsonMode, jsonOut } from "./utils.js";
5
6
  function sleep(ms) {
6
7
  return new Promise((resolve) => setTimeout(resolve, ms));
7
8
  }
@@ -69,22 +70,50 @@ export function registerAuthCommand(program) {
69
70
  .command("status")
70
71
  .description("Check whether you are currently authenticated with Gobi.")
71
72
  .action(() => {
72
- if (isAuthenticated()) {
73
- const user = getCurrentUser();
74
- const name = user?.name || "Unknown";
75
- const email = user?.email || "Unknown";
76
- console.log(`Authenticated as ${name} (${email})`);
77
- printContext();
78
- }
79
- else {
73
+ const settings = readSettings();
74
+ const vaultSlug = settings?.vaultSlug ?? null;
75
+ const spaceSlug = settings?.selectedSpaceSlug ?? null;
76
+ if (!isAuthenticated()) {
77
+ if (isJsonMode(auth)) {
78
+ jsonOut({
79
+ authenticated: false,
80
+ user: null,
81
+ vaultSlug,
82
+ spaceSlug,
83
+ });
84
+ return;
85
+ }
80
86
  console.log("You are not authenticated. Use 'gobi auth login' to log in.");
87
+ return;
88
+ }
89
+ const user = getCurrentUser();
90
+ if (isJsonMode(auth)) {
91
+ jsonOut({
92
+ authenticated: true,
93
+ user: {
94
+ name: user?.name ?? null,
95
+ email: user?.email ?? null,
96
+ },
97
+ vaultSlug,
98
+ spaceSlug,
99
+ });
100
+ return;
81
101
  }
102
+ const name = user?.name || "Unknown";
103
+ const email = user?.email || "Unknown";
104
+ console.log(`Authenticated as ${name} (${email})`);
105
+ console.log(` Vault: ${vaultSlug ?? "(not set)"}`);
106
+ console.log(` Space: ${spaceSlug ?? "(not set)"}`);
82
107
  });
83
108
  auth
84
109
  .command("logout")
85
110
  .description("Log out of Gobi and remove stored credentials.")
86
111
  .action(async () => {
87
112
  await logout();
113
+ if (isJsonMode(auth)) {
114
+ jsonOut({ loggedOut: true });
115
+ return;
116
+ }
88
117
  console.log("Logged out. Credentials removed.");
89
118
  });
90
119
  }
@@ -43,7 +43,7 @@ export function registerDraftCommand(program) {
43
43
  draft
44
44
  .command("list")
45
45
  .description("List drafts (priority ASC, then newest first).")
46
- .option("--limit <number>", "Max drafts to return (1-200)", "50")
46
+ .option("--limit <number>", "Items per page", "20")
47
47
  .action(async (opts) => {
48
48
  const params = { limit: parseInt(opts.limit, 10) };
49
49
  const resp = (await apiGet("/app/drafts", params));
@@ -63,7 +63,7 @@ export function registerDraftCommand(program) {
63
63
  // ── Get ──
64
64
  draft
65
65
  .command("get <draftId>")
66
- .description("Show one draft with its history and suggested actions.")
66
+ .description("Get one draft with its history and suggested actions.")
67
67
  .action(async (draftId) => {
68
68
  const resp = (await apiGet(`/app/drafts/${draftId}`));
69
69
  const d = unwrapResp(resp);
@@ -121,21 +121,18 @@ export function registerDraftCommand(program) {
121
121
  // ── Add ──
122
122
  draft
123
123
  .command("add <title> <content>")
124
- .description("Add a draft. Pass '-' for content to read from stdin. Pass --action up to 3 times to attach AI-suggested actions. Requires a chat session the agent runtime exports GOBI_SESSION_ID automatically; outside that, pass --session.")
125
- .option("--session <sessionId>", "Originating chat session UUID. Falls back to $GOBI_SESSION_ID when set.")
124
+ .description("Add a draft. Pass '-' for content to read from stdin. Pass --action up to 3 times to attach AI-suggested actions. Session id is optional: the Gobi agent runtime exports GOBI_SESSION_ID automatically and `--session` takes precedence; if neither is set, the server mints a new chat session anchored to your primary vault and seeds it with the draft so clicking an action later has somewhere to land.")
125
+ .option("--session <sessionId>", "Originating chat session UUID. Falls back to $GOBI_SESSION_ID; if unset, the server creates a new session.")
126
126
  .option("--priority <number>", "Priority (lower = higher), default 100")
127
127
  .option("--action <label[::message]>", "Suggested action (repeatable, max 3). `label` is the button text; an optional `::message` suffix is what the user is taken to be saying to the agent on click. Without the suffix, the message falls back to the label.", (value, prev = []) => [...prev, value], [])
128
128
  .action(async (title, content, opts) => {
129
- const sessionId = opts.session || process.env.GOBI_SESSION_ID || "";
130
- if (!sessionId) {
131
- console.error("Error: missing session id. Pass --session <uuid> or set GOBI_SESSION_ID in the environment.");
132
- process.exit(1);
133
- }
129
+ const sessionId = opts.session || process.env.GOBI_SESSION_ID;
134
130
  const body = {
135
131
  title,
136
132
  content: readContent(content),
137
- sessionId,
138
133
  };
134
+ if (sessionId)
135
+ body.sessionId = sessionId;
139
136
  if (opts.priority)
140
137
  body.priority = parseInt(opts.priority, 10);
141
138
  const actions = parseActionFlags(opts.action);
@@ -156,7 +153,7 @@ export function registerDraftCommand(program) {
156
153
  .action(async (draftId) => {
157
154
  await apiDelete(`/app/drafts/${draftId}`);
158
155
  if (isJsonMode(draft)) {
159
- jsonOut({ deleted: draftId });
156
+ jsonOut({ id: draftId });
160
157
  return;
161
158
  }
162
159
  console.log(`Deleted ${draftId}.`);
@@ -183,8 +180,7 @@ export function registerDraftCommand(program) {
183
180
  .action(async (draftId, actionIndex) => {
184
181
  const idx = parseInt(actionIndex, 10);
185
182
  if (Number.isNaN(idx) || idx < 0 || idx > 2) {
186
- console.error("Error: actionIndex must be 0, 1, or 2.");
187
- process.exit(1);
183
+ throw new Error("actionIndex must be 0, 1, or 2.");
188
184
  }
189
185
  const resp = (await apiPost(`/app/drafts/${draftId}/action`, {
190
186
  actionIndex: idx,
@@ -32,7 +32,7 @@ export function registerGlobalCommand(program) {
32
32
  // ── Feed (unified) ──
33
33
  global
34
34
  .command("feed")
35
- .description("List the global public feed (posts and replies, newest first).")
35
+ .description("List the unified feed (posts and replies, newest first) in the global public feed.")
36
36
  .option("--limit <number>", "Items per page", "20")
37
37
  .option("--cursor <string>", "Pagination cursor from previous response")
38
38
  .option("--following", "Only include posts from authors you follow")
@@ -110,7 +110,7 @@ export function registerGlobalCommand(program) {
110
110
  global
111
111
  .command("get-post <postId>")
112
112
  .description("Get a global post with its ancestors and replies (paginated).")
113
- .option("--limit <number>", "Replies per page", "20")
113
+ .option("--limit <number>", "Items per page", "20")
114
114
  .option("--cursor <string>", "Pagination cursor from previous response")
115
115
  .option("--full", "Show full reply content without truncation")
116
116
  .action(async (postId, opts) => {
@@ -177,12 +177,12 @@ export function registerGlobalCommand(program) {
177
177
  // ── Create post ──
178
178
  global
179
179
  .command("create-post")
180
- .description("Create a post in the global feed (publishes from your vault).")
180
+ .description("Create a post in the global feed. --vault-slug attributes it to a vault you own; defaults to your primary vault.")
181
181
  .option("--title <title>", "Title of the post")
182
182
  .option("--content <content>", "Post content (markdown supported, use \"-\" for stdin)")
183
183
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
184
- .option("--vault-slug <vaultSlug>", "Author vault slug (overrides .gobi/settings.yaml)")
185
- .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before posting")
184
+ .option("--vault-slug <vaultSlug>", "Attribute the post to this vault (sets authorVaultSlug). Defaults to your primary vault.")
185
+ .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before posting (also sets authorVaultSlug to that vault)")
186
186
  .action(async (opts) => {
187
187
  if (!opts.content && !opts.richText) {
188
188
  throw new Error("Provide either --content or --rich-text.");
@@ -190,16 +190,19 @@ export function registerGlobalCommand(program) {
190
190
  if (opts.content && opts.richText) {
191
191
  throw new Error("--content and --rich-text are mutually exclusive.");
192
192
  }
193
- const vaultSlug = resolveVaultSlug(opts);
193
+ let authorVaultSlug;
194
+ if (opts.vaultSlug || opts.autoAttachments) {
195
+ authorVaultSlug = resolveVaultSlug(opts);
196
+ }
194
197
  const body = {};
195
198
  if (opts.title != null)
196
199
  body.title = opts.title;
197
200
  if (opts.content != null) {
198
201
  const content = readContent(opts.content);
199
- if (opts.autoAttachments) {
202
+ if (opts.autoAttachments && authorVaultSlug) {
200
203
  const token = await getValidToken();
201
204
  const links = extractWikiLinks(content);
202
- await uploadAttachments(vaultSlug, links, token, { addToSyncfiles: true });
205
+ await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
203
206
  }
204
207
  body.content = content;
205
208
  }
@@ -213,7 +216,9 @@ export function registerGlobalCommand(program) {
213
216
  }
214
217
  body.richText = parsed;
215
218
  }
216
- const resp = (await apiPost(`/posts/vault/${vaultSlug}`, body));
219
+ if (authorVaultSlug)
220
+ body.authorVaultSlug = authorVaultSlug;
221
+ const resp = (await apiPost(`/posts`, body));
217
222
  const post = unwrapResp(resp);
218
223
  if (isJsonMode(global)) {
219
224
  jsonOut(post);
@@ -231,22 +236,35 @@ export function registerGlobalCommand(program) {
231
236
  .option("--title <title>", "New title")
232
237
  .option("--content <content>", "New content (markdown supported, use \"-\" for stdin)")
233
238
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
234
- .option("--vault-slug <vaultSlug>", "Attribute the post to this vault (sets authorVaultId). Pass an empty string to detach.")
239
+ .option("--vault-slug <vaultSlug>", "Attribute the post to this vault (sets authorVaultSlug).")
240
+ .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before editing (uses --vault-slug or .gobi vault)")
235
241
  .action(async (postId, opts) => {
242
+ const wantsVaultChange = !!(opts.vaultSlug || opts.autoAttachments);
236
243
  if (opts.title == null &&
237
244
  opts.content == null &&
238
245
  opts.richText == null &&
239
- opts.vaultSlug === undefined) {
246
+ !wantsVaultChange) {
240
247
  throw new Error("Provide at least --title, --content, --rich-text, or --vault-slug to update.");
241
248
  }
242
249
  if (opts.content && opts.richText) {
243
250
  throw new Error("--content and --rich-text are mutually exclusive.");
244
251
  }
252
+ let authorVaultSlug;
253
+ if (opts.vaultSlug || opts.autoAttachments) {
254
+ authorVaultSlug = resolveVaultSlug(opts);
255
+ }
245
256
  const body = {};
246
257
  if (opts.title != null)
247
258
  body.title = opts.title;
248
- if (opts.content != null)
249
- body.content = readContent(opts.content);
259
+ if (opts.content != null) {
260
+ const content = readContent(opts.content);
261
+ if (opts.autoAttachments && authorVaultSlug) {
262
+ const token = await getValidToken();
263
+ const links = extractWikiLinks(content);
264
+ await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
265
+ }
266
+ body.content = content;
267
+ }
250
268
  if (opts.richText != null) {
251
269
  let parsed;
252
270
  try {
@@ -257,8 +275,8 @@ export function registerGlobalCommand(program) {
257
275
  }
258
276
  body.richText = parsed;
259
277
  }
260
- if (opts.vaultSlug !== undefined)
261
- body.authorVaultSlug = opts.vaultSlug;
278
+ if (authorVaultSlug !== undefined)
279
+ body.authorVaultSlug = authorVaultSlug;
262
280
  const resp = (await apiPatch(`/posts/${postId}`, body));
263
281
  const post = unwrapResp(resp);
264
282
  if (isJsonMode(global)) {
@@ -282,9 +300,11 @@ export function registerGlobalCommand(program) {
282
300
  // ── Reply ──
283
301
  global
284
302
  .command("create-reply <postId>")
285
- .description("Reply to a post in the global feed.")
303
+ .description("Create a reply to a post in the global feed.")
286
304
  .option("--content <content>", "Reply content (markdown supported, use \"-\" for stdin)")
287
305
  .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
306
+ .option("--vault-slug <vaultSlug>", "Attribute the reply to this vault (sets authorVaultSlug). Also used as upload destination for --auto-attachments.")
307
+ .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before posting (also attributes the reply to that vault)")
288
308
  .action(async (postId, opts) => {
289
309
  if (!opts.content && !opts.richText) {
290
310
  throw new Error("Provide either --content or --rich-text.");
@@ -292,9 +312,20 @@ export function registerGlobalCommand(program) {
292
312
  if (opts.content && opts.richText) {
293
313
  throw new Error("--content and --rich-text are mutually exclusive.");
294
314
  }
315
+ let authorVaultSlug;
316
+ if (opts.vaultSlug || opts.autoAttachments) {
317
+ authorVaultSlug = resolveVaultSlug(opts);
318
+ }
295
319
  const body = {};
296
- if (opts.content != null)
297
- body.content = readContent(opts.content);
320
+ if (opts.content != null) {
321
+ const content = readContent(opts.content);
322
+ if (opts.autoAttachments && authorVaultSlug) {
323
+ const token = await getValidToken();
324
+ const links = extractWikiLinks(content);
325
+ await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
326
+ }
327
+ body.content = content;
328
+ }
298
329
  if (opts.richText != null) {
299
330
  let parsed;
300
331
  try {
@@ -305,6 +336,8 @@ export function registerGlobalCommand(program) {
305
336
  }
306
337
  body.richText = parsed;
307
338
  }
339
+ if (authorVaultSlug)
340
+ body.authorVaultSlug = authorVaultSlug;
308
341
  const resp = (await apiPost(`/posts/${postId}/replies`, body));
309
342
  const reply = unwrapResp(resp);
310
343
  if (isJsonMode(global)) {
@@ -316,12 +349,45 @@ export function registerGlobalCommand(program) {
316
349
  global
317
350
  .command("edit-reply <replyId>")
318
351
  .description("Edit a reply you authored in the global feed.")
319
- .requiredOption("--content <content>", "New reply content (markdown supported, use \"-\" for stdin)")
352
+ .option("--content <content>", "New reply content (markdown supported, use \"-\" for stdin)")
353
+ .option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
354
+ .option("--vault-slug <vaultSlug>", "Attribute the reply to this vault (sets authorVaultSlug). Also used as upload destination for --auto-attachments.")
355
+ .option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before editing (also attributes the reply to that vault)")
320
356
  .action(async (replyId, opts) => {
321
- const content = readContent(opts.content);
322
- const resp = (await apiPatch(`/posts/replies/${replyId}`, {
323
- content,
324
- }));
357
+ const wantsVaultChange = !!(opts.vaultSlug || opts.autoAttachments);
358
+ if (opts.content == null && opts.richText == null && !wantsVaultChange) {
359
+ throw new Error("Provide at least --content, --rich-text, or --vault-slug to update.");
360
+ }
361
+ if (opts.content && opts.richText) {
362
+ throw new Error("--content and --rich-text are mutually exclusive.");
363
+ }
364
+ let authorVaultSlug;
365
+ if (opts.vaultSlug || opts.autoAttachments) {
366
+ authorVaultSlug = resolveVaultSlug(opts);
367
+ }
368
+ const body = {};
369
+ if (opts.content != null) {
370
+ const content = readContent(opts.content);
371
+ if (opts.autoAttachments && authorVaultSlug) {
372
+ const token = await getValidToken();
373
+ const links = extractWikiLinks(content);
374
+ await uploadAttachments(authorVaultSlug, links, token, { addToSyncfiles: true });
375
+ }
376
+ body.content = content;
377
+ }
378
+ if (opts.richText != null) {
379
+ let parsed;
380
+ try {
381
+ parsed = JSON.parse(opts.richText);
382
+ }
383
+ catch {
384
+ throw new Error("Invalid --rich-text JSON.");
385
+ }
386
+ body.richText = parsed;
387
+ }
388
+ if (authorVaultSlug !== undefined)
389
+ body.authorVaultSlug = authorVaultSlug;
390
+ const resp = (await apiPatch(`/posts/replies/${replyId}`, body));
325
391
  const reply = unwrapResp(resp);
326
392
  if (isJsonMode(global)) {
327
393
  jsonOut(reply);
@@ -335,7 +401,7 @@ export function registerGlobalCommand(program) {
335
401
  .action(async (replyId) => {
336
402
  await apiDelete(`/posts/replies/${replyId}`);
337
403
  if (isJsonMode(global)) {
338
- jsonOut({ replyId });
404
+ jsonOut({ id: replyId });
339
405
  return;
340
406
  }
341
407
  console.log(`Reply ${replyId} deleted.`);