@gobi-ai/cli 0.3.1 → 0.3.3

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/README.md CHANGED
@@ -38,13 +38,13 @@ npm link
38
38
  gobi init
39
39
 
40
40
  # Select a space
41
- gobi astra warp
41
+ gobi space warp
42
42
 
43
- # Search brains in your space
44
- gobi astra search-brain --query "machine learning"
43
+ # Search brains across your spaces
44
+ gobi brain search --query "machine learning"
45
45
 
46
46
  # Ask a brain a question
47
- gobi astra ask-brain --vault-slug my-vault --question "What is RAG?"
47
+ gobi brain ask --vault-slug my-vault --space-slug my-space --question "What is RAG?"
48
48
  ```
49
49
 
50
50
  ## Commands
@@ -62,59 +62,61 @@ gobi astra ask-brain --vault-slug my-vault --question "What is RAG?"
62
62
  | Command | Description |
63
63
  |---------|-------------|
64
64
  | `gobi init` | Log in (if needed) and select or create a vault |
65
- | `gobi astra warp` | Select the active space |
65
+ | `gobi space warp` | Select the active space |
66
66
 
67
67
  ### Brains
68
68
 
69
69
  | Command | Description |
70
70
  |---------|-------------|
71
- | `gobi astra search-brain --query <q>` | Search brains in a space |
72
- | `gobi astra ask-brain --vault-slug <slug> --question <q>` | Ask a brain a question (creates a 1:1 session) |
73
- | `gobi astra publish-brain` | Upload `BRAIN.md` to your vault |
74
- | `gobi astra unpublish-brain` | Remove `BRAIN.md` from your vault |
71
+ | `gobi brain search --query <q>` | Search brains across all your spaces |
72
+ | `gobi brain ask --vault-slug <slug> --space-slug <slug> --question <q>` | Ask a brain a question (creates a 1:1 session) |
73
+ | `gobi brain publish` | Upload `BRAIN.md` to your vault |
74
+ | `gobi brain unpublish` | Remove `BRAIN.md` from your vault |
75
75
 
76
- ### Posts
76
+ ### Brain Updates
77
77
 
78
78
  | Command | Description |
79
79
  |---------|-------------|
80
- | `gobi astra list-posts` | List posts in the current space |
81
- | `gobi astra get-post <id>` | Get a post and its replies |
82
- | `gobi astra create-post --title <t> --content <c>` | Create a post |
83
- | `gobi astra edit-post <id> --title <t>` | Edit a post |
84
- | `gobi astra delete-post <id>` | Delete a post |
80
+ | `gobi brain list-updates` | List brain updates for your vault |
81
+ | `gobi brain list-updates --mine` | List only your own brain updates |
82
+ | `gobi brain post-update --title <t> --content <c>` | Post a brain update |
83
+ | `gobi brain edit-update <id> --title <t>` | Edit a brain update |
84
+ | `gobi brain delete-update <id>` | Delete a brain update |
85
85
 
86
- ### Replies
86
+ ### Threads
87
87
 
88
88
  | Command | Description |
89
89
  |---------|-------------|
90
- | `gobi astra create-reply <postId> --content <c>` | Reply to a post |
91
- | `gobi astra edit-reply <replyId> --content <c>` | Edit a reply |
92
- | `gobi astra delete-reply <replyId>` | Delete a reply |
90
+ | `gobi space list-threads` | List threads in the current space |
91
+ | `gobi space get-thread <id>` | Get a thread and its replies |
92
+ | `gobi space create-thread --title <t> --content <c>` | Create a thread |
93
+ | `gobi space edit-thread <id> --title <t>` | Edit a thread |
94
+ | `gobi space delete-thread <id>` | Delete a thread |
93
95
 
94
- ### Sessions
96
+ ### Replies
95
97
 
96
98
  | Command | Description |
97
99
  |---------|-------------|
98
- | `gobi astra list-sessions` | List your sessions |
99
- | `gobi astra get-session <id>` | Get a session and its messages |
100
- | `gobi astra reply-session <id> --content <c>` | Send a message in a session |
101
- | `gobi astra update-session <id> --mode <mode>` | Set session mode (auto/manual) |
100
+ | `gobi space create-reply <threadId> --content <c>` | Reply to a thread |
101
+ | `gobi space edit-reply <replyId> --content <c>` | Edit a reply |
102
+ | `gobi space delete-reply <replyId>` | Delete a reply |
102
103
 
103
- ### Brain updates
104
+ ### Sessions
104
105
 
105
106
  | Command | Description |
106
107
  |---------|-------------|
107
- | `gobi astra list-brain-updates` | List brain updates in the space |
108
- | `gobi astra create-brain-update --title <t> --content <c>` | Create a brain update |
109
- | `gobi astra edit-brain-update <id> --title <t>` | Edit a brain update |
110
- | `gobi astra delete-brain-update <id>` | Delete a brain update |
108
+ | `gobi session list` | List your sessions |
109
+ | `gobi session get <id>` | Get a session and its messages |
110
+ | `gobi session reply <id> --content <c>` | Send a message in a session |
111
+ | `gobi session update <id> --mode <mode>` | Set session mode (auto/manual) |
111
112
 
112
113
  ### Global options
113
114
 
114
115
  | Option | Description |
115
116
  |--------|-------------|
116
117
  | `--json` | Output results as JSON |
117
- | `--space-slug <slug>` | Override the default space (astra commands) |
118
+ | `--space-slug <slug>` | Override the default space (on `space` commands); required on `brain ask`, optional filter on `session list` |
119
+ | `--vault-slug <slug>` | Override the default vault (on `brain list-updates` and `brain post-update`) |
118
120
 
119
121
  ## Configuration
120
122
 
@@ -1,36 +1,15 @@
1
- import { existsSync, readFileSync } from "fs";
2
- import { join } from "path";
3
1
  import { apiGet, apiPost, apiPatch, apiDelete } from "../client.js";
4
- import { WEBDRIVE_BASE_URL } from "../constants.js";
5
- import { getValidToken } from "../auth/manager.js";
6
- import { getSpaceSlug, getVaultSlug, selectSpace, writeSpaceSetting } from "./init.js";
7
- function isJsonMode(cmd) {
8
- return !!cmd.parent?.opts().json;
9
- }
10
- function jsonOut(data) {
11
- console.log(JSON.stringify({ success: true, data }));
12
- }
13
- function resolveSpaceSlug(cmd) {
14
- return cmd.opts().spaceSlug || getSpaceSlug();
15
- }
16
- function resolveVaultSlug(opts) {
17
- return opts.vaultSlug || getVaultSlug();
18
- }
19
- function unwrapResp(resp) {
20
- if (typeof resp === "object" && resp !== null && "data" in resp) {
21
- return resp.data;
22
- }
23
- return resp;
24
- }
2
+ import { selectSpace, writeSpaceSetting } from "./init.js";
3
+ import { isJsonMode, jsonOut, resolveSpaceSlug, unwrapResp } from "./utils.js";
25
4
  export function registerAstraCommand(program) {
26
- const astra = program
27
- .command("astra")
28
- .description("Astra commands (posts, sessions, brains, brain updates).")
5
+ const space = program
6
+ .command("space")
7
+ .description("Space commands (threads, replies).")
29
8
  .option("--space-slug <slug>", "Space slug (overrides .gobi/settings.yaml)");
30
9
  // ── Warp (space selection) ──
31
- astra
10
+ space
32
11
  .command("warp")
33
- .description("Select the active space for astra commands.")
12
+ .description("Select the active space.")
34
13
  .action(async () => {
35
14
  const result = await selectSpace();
36
15
  if (result === null) {
@@ -38,146 +17,36 @@ export function registerAstraCommand(program) {
38
17
  return;
39
18
  }
40
19
  writeSpaceSetting(result.slug);
41
- if (isJsonMode(astra)) {
20
+ if (isJsonMode(space)) {
42
21
  jsonOut({ spaceSlug: result.slug, spaceName: result.name });
43
22
  return;
44
23
  }
45
24
  console.log(`Warped to space "${result.name}" (${result.slug})`);
46
25
  });
47
- // ── Brains ──
48
- astra
49
- .command("search-brain")
50
- .description("Search brains (second brains/vaults) in a space using text and semantic search.")
51
- .requiredOption("--query <query>", "Search query")
52
- .action(async (opts) => {
53
- const spaceSlug = resolveSpaceSlug(astra);
54
- const resp = (await apiGet(`/spaces/${spaceSlug}/brains`, {
55
- query: opts.query,
56
- }));
57
- const results = (Array.isArray(resp) ? resp : resp.data || resp);
58
- if (isJsonMode(astra)) {
59
- jsonOut(results || []);
60
- return;
61
- }
62
- if (!results || results.length === 0) {
63
- console.log(`No brains found matching "${opts.query}".`);
64
- return;
65
- }
66
- const lines = [];
67
- for (const entry of results) {
68
- const vault = (entry.vault || entry);
69
- const owner = (entry.owner || {});
70
- const ownerName = owner.name ? ` by ${owner.name}` : "";
71
- const sim = entry.similarity != null
72
- ? ` [similarity: ${entry.similarity.toFixed(3)}]`
73
- : "";
74
- lines.push(`- ${vault.name || vault.title || "N/A"} (ID: ${vault.vaultId || vault.id || "N/A"})${ownerName}${sim}`);
75
- }
76
- console.log(`Brains matching "${opts.query}":\n` + lines.join("\n"));
77
- });
78
- astra
79
- .command("ask-brain")
80
- .description("Ask a brain a question. Creates a targeted session (1:1 conversation).")
81
- .requiredOption("--vault-slug <vaultSlug>", "Slug of the brain/vault to ask")
82
- .requiredOption("--question <question>", "The question to ask (markdown supported)")
83
- .option("--mode <mode>", 'Session mode: "auto" or "manual"')
84
- .action(async (opts) => {
85
- const spaceSlug = resolveSpaceSlug(astra);
86
- const body = {
87
- vaultSlug: opts.vaultSlug,
88
- spaceSlug,
89
- question: opts.question,
90
- };
91
- if (opts.mode != null)
92
- body.mode = opts.mode;
93
- const resp = (await apiPost(`/session/targeted`, body));
94
- const data = unwrapResp(resp);
95
- if (isJsonMode(astra)) {
96
- jsonOut(data);
97
- return;
98
- }
99
- const session = (data.session || {});
100
- const members = (data.members || []);
101
- console.log(`Session created!\n` +
102
- ` Session ID: ${session.id}\n` +
103
- ` Mode: ${session.mode}\n` +
104
- ` Members: ${members.length}\n` +
105
- ` Question sent.`);
106
- });
107
- astra
108
- .command("publish-brain")
109
- .description("Upload BRAIN.md to the vault root on webdrive. Triggers post-processing (brain sync, metadata update, Discord notification).")
110
- .action(async () => {
111
- const vaultId = getVaultSlug();
112
- const filePath = join(process.cwd(), "BRAIN.md");
113
- if (!existsSync(filePath)) {
114
- throw new Error(`BRAIN.md not found in ${process.cwd()}`);
115
- }
116
- const content = readFileSync(filePath, "utf-8");
117
- const token = await getValidToken();
118
- const url = `${WEBDRIVE_BASE_URL}/api/v1/vaults/${vaultId}/files/BRAIN.md`;
119
- const res = await fetch(url, {
120
- method: "PUT",
121
- headers: {
122
- Authorization: `Bearer ${token}`,
123
- "Content-Type": "text/markdown",
124
- },
125
- body: content,
126
- });
127
- if (!res.ok) {
128
- throw new Error(`Upload failed: HTTP ${res.status}: ${(await res.text()) || "(no body)"}`);
129
- }
130
- if (isJsonMode(astra)) {
131
- jsonOut({ vaultId });
132
- return;
133
- }
134
- console.log(`Published BRAIN.md to vault "${vaultId}"`);
135
- });
136
- astra
137
- .command("unpublish-brain")
138
- .description("Delete BRAIN.md from the vault on webdrive.")
139
- .action(async () => {
140
- const vaultId = getVaultSlug();
141
- const token = await getValidToken();
142
- const url = `${WEBDRIVE_BASE_URL}/api/v1/vaults/${vaultId}/files/BRAIN.md`;
143
- const res = await fetch(url, {
144
- method: "DELETE",
145
- headers: { Authorization: `Bearer ${token}` },
146
- });
147
- if (!res.ok) {
148
- throw new Error(`Delete failed: HTTP ${res.status}: ${(await res.text()) || "(no body)"}`);
149
- }
150
- if (isJsonMode(astra)) {
151
- jsonOut({ vaultId });
152
- return;
153
- }
154
- console.log(`Deleted BRAIN.md from vault "${vaultId}"`);
155
- });
156
- // ── Posts (get, list, create, edit, delete) ──
157
- astra
158
- .command("get-post <postId>")
159
- .description("Get a post and its replies (paginated).")
26
+ // ── Threads (get, list, create, edit, delete) ──
27
+ space
28
+ .command("get-thread <threadId>")
29
+ .description("Get a thread and its replies (paginated).")
160
30
  .option("--limit <number>", "Replies per page", "20")
161
- .option("--offset <number>", "Offset for reply pagination", "0")
162
- .action(async (postId, opts) => {
163
- const spaceSlug = resolveSpaceSlug(astra);
164
- const resp = (await apiGet(`/spaces/${spaceSlug}/posts/${postId}`, {
31
+ .option("--cursor <string>", "Pagination cursor from previous response")
32
+ .action(async (threadId, opts) => {
33
+ const spaceSlug = resolveSpaceSlug(space);
34
+ const params = {
165
35
  limit: parseInt(opts.limit, 10),
166
- offset: parseInt(opts.offset, 10),
167
- }));
36
+ };
37
+ if (opts.cursor)
38
+ params.cursor = opts.cursor;
39
+ const resp = (await apiGet(`/spaces/${spaceSlug}/threads/${threadId}`, params));
168
40
  const data = unwrapResp(resp);
169
41
  const pagination = (resp.pagination || {});
170
- if (isJsonMode(astra)) {
42
+ if (isJsonMode(space)) {
171
43
  jsonOut({ ...data, pagination });
172
44
  return;
173
45
  }
174
- const msg = (data.post || data);
46
+ const thread = (data.thread || data);
175
47
  const replies = (data.items || []);
176
- const totalReplies = pagination.total ||
177
- msg.replyCount ||
178
- 0;
179
- const author = msg.author?.name ||
180
- `User ${msg.authorId}`;
48
+ const author = thread.author?.name ||
49
+ `User ${thread.authorId}`;
181
50
  const replyLines = [];
182
51
  for (const r of replies) {
183
52
  const rAuthor = r.author?.name ||
@@ -187,28 +56,31 @@ export function registerAstraCommand(program) {
187
56
  replyLines.push(` - ${rAuthor}: ${truncated} (${r.createdAt})`);
188
57
  }
189
58
  const output = [
190
- `Post: ${msg.title}`,
191
- `By: ${author} on ${msg.createdAt}`,
59
+ `Thread: ${thread.title}`,
60
+ `By: ${author} on ${thread.createdAt}`,
192
61
  "",
193
- msg.content,
62
+ thread.content,
194
63
  "",
195
- `Replies (${replies.length} of ${totalReplies}):`,
64
+ `Replies (${replies.length} items):`,
196
65
  ...replyLines,
66
+ ...(pagination.hasMore ? [` Next cursor: ${pagination.nextCursor}`] : []),
197
67
  ].join("\n");
198
68
  console.log(output);
199
69
  });
200
- astra
201
- .command("list-posts")
202
- .description("List posts in a space (paginated).")
70
+ space
71
+ .command("list-threads")
72
+ .description("List threads in a space (paginated).")
203
73
  .option("--limit <number>", "Items per page", "20")
204
- .option("--offset <number>", "Offset for pagination", "0")
74
+ .option("--cursor <string>", "Pagination cursor from previous response")
205
75
  .action(async (opts) => {
206
- const spaceSlug = resolveSpaceSlug(astra);
207
- const resp = (await apiGet(`/spaces/${spaceSlug}/posts`, {
76
+ const spaceSlug = resolveSpaceSlug(space);
77
+ const params = {
208
78
  limit: parseInt(opts.limit, 10),
209
- offset: parseInt(opts.offset, 10),
210
- }));
211
- if (isJsonMode(astra)) {
79
+ };
80
+ if (opts.cursor)
81
+ params.cursor = opts.cursor;
82
+ const resp = (await apiGet(`/spaces/${spaceSlug}/threads`, params));
83
+ if (isJsonMode(space)) {
212
84
  jsonOut({
213
85
  items: resp.data || [],
214
86
  pagination: resp.pagination || {},
@@ -218,344 +90,116 @@ export function registerAstraCommand(program) {
218
90
  const items = (resp.data || []);
219
91
  const pagination = (resp.pagination || {});
220
92
  if (!items.length) {
221
- console.log("No posts found.");
93
+ console.log("No threads found.");
222
94
  return;
223
95
  }
224
96
  const lines = [];
225
- for (const msg of items) {
226
- const author = msg.author?.name ||
227
- `User ${msg.authorId}`;
228
- lines.push(`- [${msg.id}] "${msg.title}" by ${author} (${msg.replyCount} replies, ${msg.createdAt})`);
229
- }
230
- const total = pagination.total || items.length;
231
- console.log(`Posts (${items.length} of ${total}):\n` + lines.join("\n"));
232
- });
233
- astra
234
- .command("create-post")
235
- .description("Create a post in a space.")
236
- .requiredOption("--title <title>", "Title of the post")
237
- .requiredOption("--content <content>", "Post content (markdown supported)")
97
+ for (const t of items) {
98
+ const author = t.author?.name ||
99
+ `User ${t.authorId}`;
100
+ lines.push(`- [${t.id}] "${t.title}" by ${author} (${t.replyCount} replies, ${t.createdAt})`);
101
+ }
102
+ const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
103
+ console.log(`Threads (${items.length} items):\n` + lines.join("\n") + footer);
104
+ });
105
+ space
106
+ .command("create-thread")
107
+ .description("Create a thread in a space.")
108
+ .requiredOption("--title <title>", "Title of the thread")
109
+ .requiredOption("--content <content>", "Thread content (markdown supported)")
238
110
  .action(async (opts) => {
239
- const spaceSlug = resolveSpaceSlug(astra);
240
- const resp = (await apiPost(`/spaces/${spaceSlug}/posts`, {
111
+ const spaceSlug = resolveSpaceSlug(space);
112
+ const resp = (await apiPost(`/spaces/${spaceSlug}/threads`, {
241
113
  title: opts.title,
242
114
  content: opts.content,
243
115
  }));
244
- const msg = unwrapResp(resp);
245
- if (isJsonMode(astra)) {
246
- jsonOut(msg);
247
- return;
248
- }
249
- console.log(`Post created!\n` +
250
- ` ID: ${msg.id}\n` +
251
- ` Title: ${msg.title}\n` +
252
- ` Created: ${msg.createdAt}`);
253
- });
254
- astra
255
- .command("edit-post <postId>")
256
- .description("Edit a post. You must be the author.")
257
- .option("--title <title>", "New title for the post")
258
- .option("--content <content>", "New content for the post (markdown supported)")
259
- .action(async (postId, opts) => {
116
+ const thread = unwrapResp(resp);
117
+ if (isJsonMode(space)) {
118
+ jsonOut(thread);
119
+ return;
120
+ }
121
+ console.log(`Thread created!\n` +
122
+ ` ID: ${thread.id}\n` +
123
+ ` Title: ${thread.title}\n` +
124
+ ` Created: ${thread.createdAt}`);
125
+ });
126
+ space
127
+ .command("edit-thread <threadId>")
128
+ .description("Edit a thread. You must be the author.")
129
+ .option("--title <title>", "New title for the thread")
130
+ .option("--content <content>", "New content for the thread (markdown supported)")
131
+ .action(async (threadId, opts) => {
260
132
  if (!opts.title && !opts.content) {
261
133
  throw new Error("Provide at least --title or --content to update.");
262
134
  }
263
- const spaceSlug = resolveSpaceSlug(astra);
135
+ const spaceSlug = resolveSpaceSlug(space);
264
136
  const body = {};
265
137
  if (opts.title != null)
266
138
  body.title = opts.title;
267
139
  if (opts.content != null)
268
140
  body.content = opts.content;
269
- const resp = (await apiPatch(`/spaces/${spaceSlug}/posts/${postId}`, body));
270
- const msg = unwrapResp(resp);
271
- if (isJsonMode(astra)) {
272
- jsonOut(msg);
141
+ const resp = (await apiPatch(`/spaces/${spaceSlug}/threads/${threadId}`, body));
142
+ const thread = unwrapResp(resp);
143
+ if (isJsonMode(space)) {
144
+ jsonOut(thread);
273
145
  return;
274
146
  }
275
- console.log(`Post edited!\n` +
276
- ` ID: ${msg.id}\n` +
277
- ` Title: ${msg.title}\n` +
278
- ` Edited: ${msg.editedAt}`);
147
+ console.log(`Thread edited!\n` +
148
+ ` ID: ${thread.id}\n` +
149
+ ` Title: ${thread.title}\n` +
150
+ ` Edited: ${thread.editedAt}`);
279
151
  });
280
- astra
281
- .command("delete-post <postId>")
282
- .description("Delete a post. You must be the author.")
283
- .action(async (postId) => {
284
- const spaceSlug = resolveSpaceSlug(astra);
285
- await apiDelete(`/spaces/${spaceSlug}/posts/${postId}`);
286
- if (isJsonMode(astra)) {
287
- jsonOut({ id: postId });
152
+ space
153
+ .command("delete-thread <threadId>")
154
+ .description("Delete a thread. You must be the author.")
155
+ .action(async (threadId) => {
156
+ const spaceSlug = resolveSpaceSlug(space);
157
+ await apiDelete(`/spaces/${spaceSlug}/threads/${threadId}`);
158
+ if (isJsonMode(space)) {
159
+ jsonOut({ id: threadId });
288
160
  return;
289
161
  }
290
- console.log(`Post ${postId} deleted.`);
162
+ console.log(`Thread ${threadId} deleted.`);
291
163
  });
292
164
  // ── Replies (create, edit, delete) ──
293
- astra
294
- .command("create-reply <postId>")
295
- .description("Create a reply to a post in a space.")
165
+ space
166
+ .command("create-reply <threadId>")
167
+ .description("Create a reply to a thread in a space.")
296
168
  .requiredOption("--content <content>", "Reply content (markdown supported)")
297
- .action(async (postId, opts) => {
298
- const spaceSlug = resolveSpaceSlug(astra);
299
- const resp = (await apiPost(`/spaces/${spaceSlug}/posts/${postId}/replies`, { content: opts.content }));
169
+ .action(async (threadId, opts) => {
170
+ const spaceSlug = resolveSpaceSlug(space);
171
+ const resp = (await apiPost(`/spaces/${spaceSlug}/threads/${threadId}/replies`, { content: opts.content }));
300
172
  const msg = unwrapResp(resp);
301
- if (isJsonMode(astra)) {
173
+ if (isJsonMode(space)) {
302
174
  jsonOut(msg);
303
175
  return;
304
176
  }
305
177
  console.log(`Reply created!\n ID: ${msg.id}\n Created: ${msg.createdAt}`);
306
178
  });
307
- astra
179
+ space
308
180
  .command("edit-reply <replyId>")
309
181
  .description("Edit a reply. You must be the author.")
310
182
  .requiredOption("--content <content>", "New content for the reply (markdown supported)")
311
183
  .action(async (replyId, opts) => {
312
- const spaceSlug = resolveSpaceSlug(astra);
184
+ const spaceSlug = resolveSpaceSlug(space);
313
185
  const resp = (await apiPatch(`/spaces/${spaceSlug}/replies/${replyId}`, { content: opts.content }));
314
186
  const msg = unwrapResp(resp);
315
- if (isJsonMode(astra)) {
187
+ if (isJsonMode(space)) {
316
188
  jsonOut(msg);
317
189
  return;
318
190
  }
319
191
  console.log(`Reply edited!\n ID: ${msg.id}\n Edited: ${msg.editedAt}`);
320
192
  });
321
- astra
193
+ space
322
194
  .command("delete-reply <replyId>")
323
195
  .description("Delete a reply. You must be the author.")
324
196
  .action(async (replyId) => {
325
- const spaceSlug = resolveSpaceSlug(astra);
197
+ const spaceSlug = resolveSpaceSlug(space);
326
198
  await apiDelete(`/spaces/${spaceSlug}/replies/${replyId}`);
327
- if (isJsonMode(astra)) {
199
+ if (isJsonMode(space)) {
328
200
  jsonOut({ replyId });
329
201
  return;
330
202
  }
331
203
  console.log(`Reply ${replyId} deleted.`);
332
204
  });
333
- // ── Sessions (get, list, reply) ──
334
- astra
335
- .command("get-session <sessionId>")
336
- .description("Get a session and its messages (paginated).")
337
- .option("--limit <number>", "Messages per page", "20")
338
- .option("--offset <number>", "Offset for message pagination", "0")
339
- .action(async (sessionId, opts) => {
340
- const resp = (await apiGet(`/session/${sessionId}`, {
341
- limit: parseInt(opts.limit, 10),
342
- offset: parseInt(opts.offset, 10),
343
- }));
344
- const data = unwrapResp(resp);
345
- if (isJsonMode(astra)) {
346
- jsonOut(data);
347
- return;
348
- }
349
- const session = (data.session || data);
350
- const messages = (data.messages || []);
351
- const pagination = (data.pagination || {});
352
- const totalMessages = pagination.total || messages.length;
353
- const msgLines = [];
354
- for (const m of messages) {
355
- const author = m.author?.name ||
356
- m.source ||
357
- `User ${m.authorId}`;
358
- const text = m.content;
359
- const truncated = text.length > 200 ? text.slice(0, 200) + "\u2026" : text;
360
- msgLines.push(` - ${author}: ${truncated} (${m.createdAt})`);
361
- }
362
- const output = [
363
- `Session: ${session.title}`,
364
- ` ID: ${session.id}`,
365
- ` Mode: ${session.mode}`,
366
- ` Last activity: ${session.lastMessageAt}`,
367
- "",
368
- `Messages (${messages.length} of ${totalMessages}):`,
369
- ...msgLines,
370
- ].join("\n");
371
- console.log(output);
372
- });
373
- astra
374
- .command("list-sessions")
375
- .description("List all sessions you are part of, sorted by most recent activity.")
376
- .option("--limit <number>", "Items per page", "20")
377
- .option("--offset <number>", "Offset for pagination", "0")
378
- .action(async (opts) => {
379
- const spaceSlug = resolveSpaceSlug(astra);
380
- const limit = parseInt(opts.limit, 10);
381
- const offset = parseInt(opts.offset, 10);
382
- const resp = (await apiGet(`/session/my-sessions`, {
383
- spaceSlug,
384
- limit,
385
- offset,
386
- }));
387
- const items = (resp.data || []);
388
- const pagination = (resp.pagination || {});
389
- const total = pagination.total ?? items.length;
390
- if (isJsonMode(astra)) {
391
- jsonOut({
392
- items,
393
- pagination: resp.pagination || {},
394
- });
395
- return;
396
- }
397
- if (!items.length) {
398
- console.log("No sessions found.");
399
- return;
400
- }
401
- const lines = [];
402
- for (const s of items) {
403
- const title = s.title || "(no title)";
404
- const members = s.members || [];
405
- const memberCount = s.memberCount ?? 0;
406
- let memberInfo = "";
407
- if (members.length > 0) {
408
- const names = members.map((m) => m.vaultName || m.name || "Unknown");
409
- const overflow = memberCount - members.length - 1; // -1 for "me"
410
- memberInfo = ` | with: ${names.join(", ")}`;
411
- if (overflow > 0)
412
- memberInfo += ` +${overflow} more`;
413
- }
414
- lines.push(`- [${s.id}] "${title}" (mode: ${s.mode}, last activity: ${s.lastMessageAt})${memberInfo}`);
415
- }
416
- console.log(`Sessions (${items.length} of ${total}):\n` + lines.join("\n"));
417
- });
418
- astra
419
- .command("reply-session <sessionId>")
420
- .description("Send a human reply to a session you are a member of.")
421
- .requiredOption("--content <content>", "Reply content (markdown supported)")
422
- .action(async (sessionId, opts) => {
423
- const resp = (await apiPost(`/session/${sessionId}/reply`, {
424
- content: opts.content,
425
- }));
426
- const msg = unwrapResp(resp);
427
- if (isJsonMode(astra)) {
428
- jsonOut(msg);
429
- return;
430
- }
431
- console.log(`Reply sent!\n` +
432
- ` Message ID: ${msg.id}\n` +
433
- ` Source: ${msg.source}\n` +
434
- ` Created: ${msg.createdAt}`);
435
- });
436
- astra
437
- .command("update-session <sessionId>")
438
- .description('Update a session. "auto" lets the AI respond automatically; "manual" requires human replies.')
439
- .option("--mode <mode>", 'Session mode: "auto" or "manual"')
440
- .action(async (sessionId, opts) => {
441
- if (!opts.mode) {
442
- throw new Error("Provide at least one option to update (e.g. --mode).");
443
- }
444
- const body = {};
445
- if (opts.mode != null) {
446
- if (opts.mode !== "auto" && opts.mode !== "manual") {
447
- throw new Error('Invalid mode. Must be "auto" or "manual".');
448
- }
449
- body.mode = opts.mode;
450
- }
451
- const resp = (await apiPatch(`/session/${sessionId}`, body));
452
- const data = unwrapResp(resp);
453
- if (isJsonMode(astra)) {
454
- jsonOut(data);
455
- return;
456
- }
457
- const session = (data.session || data);
458
- console.log(`Session updated!\n` +
459
- ` ID: ${session.id}\n` +
460
- ` Mode: ${session.mode}`);
461
- });
462
- // ── Brain Updates (list, create, edit, delete) ──
463
- astra
464
- .command("list-brain-updates")
465
- .description("List recent brain updates in a space (paginated).")
466
- .option("--limit <number>", "Items per page", "20")
467
- .option("--offset <number>", "Offset for pagination", "0")
468
- .action(async (opts) => {
469
- const spaceSlug = resolveSpaceSlug(astra);
470
- const resp = (await apiGet(`/spaces/${spaceSlug}/brain-updates`, {
471
- limit: parseInt(opts.limit, 10),
472
- offset: parseInt(opts.offset, 10),
473
- }));
474
- if (isJsonMode(astra)) {
475
- jsonOut({
476
- items: resp.data || [],
477
- pagination: resp.pagination || {},
478
- });
479
- return;
480
- }
481
- const items = (resp.data || []);
482
- const pagination = (resp.pagination || {});
483
- if (!items.length) {
484
- console.log("No brain updates found.");
485
- return;
486
- }
487
- const lines = [];
488
- for (const u of items) {
489
- const author = u.author?.name ||
490
- `User ${u.authorId}`;
491
- const vaultSlug = u.vault?.vaultSlug ||
492
- "?";
493
- lines.push(`- [${u.id}] "${u.title}" by ${author} (vault: ${vaultSlug}, ${u.createdAt})`);
494
- }
495
- const total = pagination.total || items.length;
496
- console.log(`Brain updates (${items.length} of ${total}):\n` + lines.join("\n"));
497
- });
498
- astra
499
- .command("create-brain-update")
500
- .description("Create a brain update in a space. Uses the vault from settings.")
501
- .option("--vault-slug <vaultSlug>", "Vault slug (overrides .gobi/settings.yaml)")
502
- .requiredOption("--title <title>", "Title of the update")
503
- .requiredOption("--content <content>", "Update content (markdown supported)")
504
- .action(async (opts) => {
505
- const spaceSlug = resolveSpaceSlug(astra);
506
- const vaultSlug = resolveVaultSlug(opts);
507
- const resp = (await apiPost(`/spaces/${spaceSlug}/brain-updates`, {
508
- vaultSlug,
509
- title: opts.title,
510
- content: opts.content,
511
- }));
512
- const u = unwrapResp(resp);
513
- if (isJsonMode(astra)) {
514
- jsonOut(u);
515
- return;
516
- }
517
- console.log(`Brain update created!\n` +
518
- ` ID: ${u.id}\n` +
519
- ` Title: ${u.title}\n` +
520
- ` Vault: ${u.vaultSlug || vaultSlug}\n` +
521
- ` Created: ${u.createdAt}`);
522
- });
523
- astra
524
- .command("edit-brain-update <updateId>")
525
- .description("Edit a published brain update. You must be the author.")
526
- .option("--title <title>", "New title for the update")
527
- .option("--content <content>", "New content for the update (markdown supported)")
528
- .action(async (updateId, opts) => {
529
- if (!opts.title && !opts.content) {
530
- throw new Error("Provide at least --title or --content to update.");
531
- }
532
- const spaceSlug = resolveSpaceSlug(astra);
533
- const body = {};
534
- if (opts.title != null)
535
- body.title = opts.title;
536
- if (opts.content != null)
537
- body.content = opts.content;
538
- const resp = (await apiPatch(`/spaces/${spaceSlug}/brain-updates/${updateId}`, body));
539
- const u = unwrapResp(resp);
540
- if (isJsonMode(astra)) {
541
- jsonOut(u);
542
- return;
543
- }
544
- console.log(`Brain update edited!\n` +
545
- ` ID: ${u.id}\n` +
546
- ` Title: ${u.title}\n` +
547
- ` Updated: ${u.updatedAt}`);
548
- });
549
- astra
550
- .command("delete-brain-update <updateId>")
551
- .description("Delete a published brain update. You must be the author.")
552
- .action(async (updateId) => {
553
- const spaceSlug = resolveSpaceSlug(astra);
554
- await apiDelete(`/spaces/${spaceSlug}/brain-updates/${updateId}`);
555
- if (isJsonMode(astra)) {
556
- jsonOut({ id: updateId });
557
- return;
558
- }
559
- console.log(`Brain update ${updateId} deleted.`);
560
- });
561
205
  }
@@ -0,0 +1,232 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { join } from "path";
3
+ import { apiGet, apiPost, apiPatch, apiDelete } from "../client.js";
4
+ import { WEBDRIVE_BASE_URL } from "../constants.js";
5
+ import { getValidToken } from "../auth/manager.js";
6
+ import { getVaultSlug } from "./init.js";
7
+ import { isJsonMode, jsonOut, resolveVaultSlug, unwrapResp } from "./utils.js";
8
+ export function registerBrainCommand(program) {
9
+ const brain = program
10
+ .command("brain")
11
+ .description("Brain commands (search, ask, publish, unpublish, updates).");
12
+ // ── Search ──
13
+ brain
14
+ .command("search")
15
+ .description("Search public brains by text and semantic similarity.")
16
+ .requiredOption("--query <query>", "Search query")
17
+ .action(async (opts) => {
18
+ const resp = (await apiGet(`/vault/public/search`, {
19
+ query: opts.query,
20
+ }));
21
+ const results = (Array.isArray(resp) ? resp : resp.data || resp);
22
+ if (isJsonMode(brain)) {
23
+ jsonOut(results || []);
24
+ return;
25
+ }
26
+ if (!results || results.length === 0) {
27
+ console.log(`No brains found matching "${opts.query}".`);
28
+ return;
29
+ }
30
+ const lines = [];
31
+ for (const entry of results) {
32
+ const vault = (entry.vault || entry);
33
+ const owner = (entry.owner || {});
34
+ const ownerName = owner.name ? ` by ${owner.name}` : "";
35
+ const sim = entry.similarity != null
36
+ ? ` [similarity: ${entry.similarity.toFixed(3)}]`
37
+ : "";
38
+ const spaceSlug = (entry.spaceSlug || vault.spaceSlug || "");
39
+ const vaultSlug = (vault.slug || vault.vaultSlug || vault.id || "N/A");
40
+ lines.push(`- ${vault.name || vault.title || "N/A"} (vault: ${vaultSlug}, space: ${spaceSlug || "N/A"})${ownerName}${sim}`);
41
+ }
42
+ console.log(`Brains matching "${opts.query}":\n` + lines.join("\n"));
43
+ });
44
+ // ── Ask ──
45
+ brain
46
+ .command("ask")
47
+ .description("Ask a brain a question. Creates a targeted session (1:1 conversation).")
48
+ .requiredOption("--vault-slug <vaultSlug>", "Slug of the brain/vault to ask")
49
+ .requiredOption("--space-slug <spaceSlug>", "Space slug where the brain belongs")
50
+ .requiredOption("--question <question>", "The question to ask (markdown supported)")
51
+ .option("--mode <mode>", 'Session mode: "auto" or "manual"')
52
+ .action(async (opts) => {
53
+ const spaceSlug = opts.spaceSlug;
54
+ const body = {
55
+ vaultSlug: opts.vaultSlug,
56
+ spaceSlug,
57
+ question: opts.question,
58
+ };
59
+ if (opts.mode != null)
60
+ body.mode = opts.mode;
61
+ const resp = (await apiPost(`/session/targeted`, body));
62
+ const data = unwrapResp(resp);
63
+ if (isJsonMode(brain)) {
64
+ jsonOut(data);
65
+ return;
66
+ }
67
+ const session = (data.session || {});
68
+ const members = (data.members || []);
69
+ console.log(`Session created!\n` +
70
+ ` Session ID: ${session.id}\n` +
71
+ ` Mode: ${session.mode}\n` +
72
+ ` Members: ${members.length}\n` +
73
+ ` Question sent.`);
74
+ });
75
+ // ── Publish ──
76
+ brain
77
+ .command("publish")
78
+ .description("Upload BRAIN.md to the vault root on webdrive. Triggers post-processing (brain sync, metadata update, Discord notification).")
79
+ .action(async () => {
80
+ const vaultId = getVaultSlug();
81
+ const filePath = join(process.cwd(), "BRAIN.md");
82
+ if (!existsSync(filePath)) {
83
+ throw new Error(`BRAIN.md not found in ${process.cwd()}`);
84
+ }
85
+ const content = readFileSync(filePath, "utf-8");
86
+ const token = await getValidToken();
87
+ const url = `${WEBDRIVE_BASE_URL}/api/v1/vaults/${vaultId}/files/BRAIN.md`;
88
+ const res = await fetch(url, {
89
+ method: "PUT",
90
+ headers: {
91
+ Authorization: `Bearer ${token}`,
92
+ "Content-Type": "text/markdown",
93
+ },
94
+ body: content,
95
+ });
96
+ if (!res.ok) {
97
+ throw new Error(`Upload failed: HTTP ${res.status}: ${(await res.text()) || "(no body)"}`);
98
+ }
99
+ if (isJsonMode(brain)) {
100
+ jsonOut({ vaultId });
101
+ return;
102
+ }
103
+ console.log(`Published BRAIN.md to vault "${vaultId}"`);
104
+ });
105
+ // ── Unpublish ──
106
+ brain
107
+ .command("unpublish")
108
+ .description("Delete BRAIN.md from the vault on webdrive.")
109
+ .action(async () => {
110
+ const vaultId = getVaultSlug();
111
+ const token = await getValidToken();
112
+ const url = `${WEBDRIVE_BASE_URL}/api/v1/vaults/${vaultId}/files/BRAIN.md`;
113
+ const res = await fetch(url, {
114
+ method: "DELETE",
115
+ headers: { Authorization: `Bearer ${token}` },
116
+ });
117
+ if (!res.ok) {
118
+ throw new Error(`Delete failed: HTTP ${res.status}: ${(await res.text()) || "(no body)"}`);
119
+ }
120
+ if (isJsonMode(brain)) {
121
+ jsonOut({ vaultId });
122
+ return;
123
+ }
124
+ console.log(`Deleted BRAIN.md from vault "${vaultId}"`);
125
+ });
126
+ // ── Updates (list, post, edit, delete) ──
127
+ brain
128
+ .command("list-updates")
129
+ .description("List recent brain updates. Without --space-slug, lists all updates for you. With --space-slug, lists updates for that space. Use --mine to show only updates by you.")
130
+ .option("--vault-slug <vaultSlug>", "Vault slug (overrides .gobi/settings.yaml)")
131
+ .option("--space-slug <spaceSlug>", "List updates for a space")
132
+ .option("--mine", "List only my own brain updates")
133
+ .option("--limit <number>", "Items per page", "20")
134
+ .option("--cursor <string>", "Pagination cursor from previous response")
135
+ .action(async (opts) => {
136
+ const params = {
137
+ limit: parseInt(opts.limit, 10),
138
+ };
139
+ if (opts.cursor)
140
+ params.cursor = opts.cursor;
141
+ if (opts.mine)
142
+ params.mine = true;
143
+ if (opts.vaultSlug)
144
+ params.vaultSlug = opts.vaultSlug;
145
+ const path = opts.spaceSlug
146
+ ? `/spaces/${opts.spaceSlug}/brain-updates`
147
+ : `/brain-updates`;
148
+ const resp = (await apiGet(path, params));
149
+ if (isJsonMode(brain)) {
150
+ jsonOut({
151
+ items: resp.data || [],
152
+ pagination: resp.pagination || {},
153
+ });
154
+ return;
155
+ }
156
+ const items = (resp.data || []);
157
+ const pagination = (resp.pagination || {});
158
+ if (!items.length) {
159
+ console.log("No brain updates found.");
160
+ return;
161
+ }
162
+ const lines = [];
163
+ for (const u of items) {
164
+ const author = u.author?.name ||
165
+ `User ${u.authorId}`;
166
+ const vaultSlug = u.vault?.vaultSlug ||
167
+ "?";
168
+ lines.push(`- [${u.id}] "${u.title}" by ${author} (vault: ${vaultSlug}, ${u.createdAt})`);
169
+ }
170
+ const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
171
+ console.log(`Brain updates (${items.length} items):\n` + lines.join("\n") + footer);
172
+ });
173
+ brain
174
+ .command("post-update")
175
+ .description("Post a brain update for a vault.")
176
+ .option("--vault-slug <vaultSlug>", "Vault slug (overrides .gobi/settings.yaml)")
177
+ .requiredOption("--title <title>", "Title of the update")
178
+ .requiredOption("--content <content>", "Update content (markdown supported)")
179
+ .action(async (opts) => {
180
+ const vaultSlug = resolveVaultSlug(opts);
181
+ const resp = (await apiPost(`/brain-updates/vault/${vaultSlug}`, {
182
+ title: opts.title,
183
+ content: opts.content,
184
+ }));
185
+ const u = unwrapResp(resp);
186
+ if (isJsonMode(brain)) {
187
+ jsonOut(u);
188
+ return;
189
+ }
190
+ console.log(`Brain update posted!\n` +
191
+ ` ID: ${u.id}\n` +
192
+ ` Title: ${u.title}\n` +
193
+ ` Vault: ${u.vaultSlug || vaultSlug}\n` +
194
+ ` Created: ${u.createdAt}`);
195
+ });
196
+ brain
197
+ .command("edit-update <updateId>")
198
+ .description("Edit a published brain update. You must be the author.")
199
+ .option("--title <title>", "New title for the update")
200
+ .option("--content <content>", "New content for the update (markdown supported)")
201
+ .action(async (updateId, opts) => {
202
+ if (!opts.title && !opts.content) {
203
+ throw new Error("Provide at least --title or --content to update.");
204
+ }
205
+ const body = {};
206
+ if (opts.title != null)
207
+ body.title = opts.title;
208
+ if (opts.content != null)
209
+ body.content = opts.content;
210
+ const resp = (await apiPatch(`/brain-updates/${updateId}`, body));
211
+ const u = unwrapResp(resp);
212
+ if (isJsonMode(brain)) {
213
+ jsonOut(u);
214
+ return;
215
+ }
216
+ console.log(`Brain update edited!\n` +
217
+ ` ID: ${u.id}\n` +
218
+ ` Title: ${u.title}\n` +
219
+ ` Updated: ${u.updatedAt}`);
220
+ });
221
+ brain
222
+ .command("delete-update <updateId>")
223
+ .description("Delete a published brain update. You must be the author.")
224
+ .action(async (updateId) => {
225
+ await apiDelete(`/brain-updates/${updateId}`);
226
+ if (isJsonMode(brain)) {
227
+ jsonOut({ id: updateId });
228
+ return;
229
+ }
230
+ console.log(`Brain update ${updateId} deleted.`);
231
+ });
232
+ }
@@ -21,7 +21,7 @@ export function getSpaceSlug() {
21
21
  const settings = readSettings();
22
22
  const slug = settings?.selectedSpaceSlug;
23
23
  if (!slug) {
24
- throw new Error("Space not set. Run 'gobi astra warp' first.");
24
+ throw new Error("Space not set. Run 'gobi space warp' first.");
25
25
  }
26
26
  return slug;
27
27
  }
@@ -38,7 +38,7 @@ export function printContext() {
38
38
  const vault = settings?.vaultSlug;
39
39
  const space = settings?.selectedSpaceSlug;
40
40
  if (!vault && !space) {
41
- console.log("Run 'gobi init' to set up, then 'gobi astra warp' to select a space.");
41
+ console.log("Run 'gobi init' to set up, then 'gobi space warp' to select a space.");
42
42
  return;
43
43
  }
44
44
  if (!vault) {
@@ -46,7 +46,7 @@ export function printContext() {
46
46
  return;
47
47
  }
48
48
  if (!space) {
49
- console.log(`Vault: ${vault} | Space not set. Run 'gobi astra warp' to select a space.`);
49
+ console.log(`Vault: ${vault} | Space not set. Run 'gobi space warp' to select a space.`);
50
50
  return;
51
51
  }
52
52
  console.log(`Space: ${space} | Vault: ${vault}`);
@@ -0,0 +1,140 @@
1
+ import { apiGet, apiPost, apiPatch } from "../client.js";
2
+ import { isJsonMode, jsonOut, unwrapResp } from "./utils.js";
3
+ export function registerSessionsCommand(program) {
4
+ const sessions = program
5
+ .command("session")
6
+ .description("Session commands (get, list, reply, update).");
7
+ // ── Get ──
8
+ sessions
9
+ .command("get <sessionId>")
10
+ .description("Get a session and its messages (paginated).")
11
+ .option("--limit <number>", "Messages per page", "20")
12
+ .option("--cursor <string>", "Pagination cursor from previous response")
13
+ .action(async (sessionId, opts) => {
14
+ const params = {
15
+ limit: parseInt(opts.limit, 10),
16
+ };
17
+ if (opts.cursor)
18
+ params.cursor = opts.cursor;
19
+ const resp = (await apiGet(`/session/${sessionId}`, params));
20
+ const data = unwrapResp(resp);
21
+ if (isJsonMode(sessions)) {
22
+ jsonOut(data);
23
+ return;
24
+ }
25
+ const session = (data.session || data);
26
+ const messages = (data.messages || []);
27
+ const pagination = (data.pagination || {});
28
+ const msgLines = [];
29
+ for (const m of messages) {
30
+ const author = m.author?.name ||
31
+ m.source ||
32
+ `User ${m.authorId}`;
33
+ const text = m.content;
34
+ const truncated = text.length > 200 ? text.slice(0, 200) + "\u2026" : text;
35
+ msgLines.push(` - ${author}: ${truncated} (${m.createdAt})`);
36
+ }
37
+ const output = [
38
+ `Session: ${session.title}`,
39
+ ` ID: ${session.id}`,
40
+ ` Mode: ${session.mode}`,
41
+ ` Last activity: ${session.lastMessageAt}`,
42
+ "",
43
+ `Messages (${messages.length} items):`,
44
+ ...msgLines,
45
+ ...(pagination.hasMore ? [` Next cursor: ${pagination.nextCursor}`] : []),
46
+ ].join("\n");
47
+ console.log(output);
48
+ });
49
+ // ── List ──
50
+ sessions
51
+ .command("list")
52
+ .description("List all sessions you are part of, sorted by most recent activity.")
53
+ .option("--space-slug <spaceSlug>", "Filter by space slug")
54
+ .option("--limit <number>", "Items per page", "20")
55
+ .option("--cursor <string>", "Pagination cursor from previous response")
56
+ .action(async (opts) => {
57
+ const query = { limit: parseInt(opts.limit, 10) };
58
+ if (opts.cursor)
59
+ query.cursor = opts.cursor;
60
+ if (opts.spaceSlug)
61
+ query.spaceSlug = opts.spaceSlug;
62
+ const resp = (await apiGet(`/session/my-sessions`, query));
63
+ const items = (resp.data || []);
64
+ const pagination = (resp.pagination || {});
65
+ if (isJsonMode(sessions)) {
66
+ jsonOut({
67
+ items,
68
+ pagination: resp.pagination || {},
69
+ });
70
+ return;
71
+ }
72
+ if (!items.length) {
73
+ console.log("No sessions found.");
74
+ return;
75
+ }
76
+ const lines = [];
77
+ for (const s of items) {
78
+ const title = s.title || "(no title)";
79
+ const members = s.members || [];
80
+ const memberCount = s.memberCount ?? 0;
81
+ let memberInfo = "";
82
+ if (members.length > 0) {
83
+ const names = members.map((m) => m.vaultName || m.name || "Unknown");
84
+ const overflow = memberCount - members.length - 1; // -1 for "me"
85
+ memberInfo = ` | with: ${names.join(", ")}`;
86
+ if (overflow > 0)
87
+ memberInfo += ` +${overflow} more`;
88
+ }
89
+ lines.push(`- [${s.id}] "${title}" (mode: ${s.mode}, last activity: ${s.lastMessageAt})${memberInfo}`);
90
+ }
91
+ const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
92
+ console.log(`Sessions (${items.length} items):\n` + lines.join("\n") + footer);
93
+ });
94
+ // ── Reply ──
95
+ sessions
96
+ .command("reply <sessionId>")
97
+ .description("Send a human reply to a session you are a member of.")
98
+ .requiredOption("--content <content>", "Reply content (markdown supported)")
99
+ .action(async (sessionId, opts) => {
100
+ const resp = (await apiPost(`/session/${sessionId}/reply`, {
101
+ content: opts.content,
102
+ }));
103
+ const msg = unwrapResp(resp);
104
+ if (isJsonMode(sessions)) {
105
+ jsonOut(msg);
106
+ return;
107
+ }
108
+ console.log(`Reply sent!\n` +
109
+ ` Message ID: ${msg.id}\n` +
110
+ ` Source: ${msg.source}\n` +
111
+ ` Created: ${msg.createdAt}`);
112
+ });
113
+ // ── Update ──
114
+ sessions
115
+ .command("update <sessionId>")
116
+ .description('Update a session. "auto" lets the AI respond automatically; "manual" requires human replies.')
117
+ .option("--mode <mode>", 'Session mode: "auto" or "manual"')
118
+ .action(async (sessionId, opts) => {
119
+ if (!opts.mode) {
120
+ throw new Error("Provide at least one option to update (e.g. --mode).");
121
+ }
122
+ const body = {};
123
+ if (opts.mode != null) {
124
+ if (opts.mode !== "auto" && opts.mode !== "manual") {
125
+ throw new Error('Invalid mode. Must be "auto" or "manual".');
126
+ }
127
+ body.mode = opts.mode;
128
+ }
129
+ const resp = (await apiPatch(`/session/${sessionId}`, body));
130
+ const data = unwrapResp(resp);
131
+ if (isJsonMode(sessions)) {
132
+ jsonOut(data);
133
+ return;
134
+ }
135
+ const session = (data.session || data);
136
+ console.log(`Session updated!\n` +
137
+ ` ID: ${session.id}\n` +
138
+ ` Mode: ${session.mode}`);
139
+ });
140
+ }
@@ -0,0 +1,19 @@
1
+ import { getSpaceSlug, getVaultSlug } from "./init.js";
2
+ export function isJsonMode(cmd) {
3
+ return !!cmd.parent?.opts().json;
4
+ }
5
+ export function jsonOut(data) {
6
+ console.log(JSON.stringify({ success: true, data }));
7
+ }
8
+ export function resolveSpaceSlug(cmd) {
9
+ return cmd.opts().spaceSlug || getSpaceSlug();
10
+ }
11
+ export function resolveVaultSlug(opts) {
12
+ return opts.vaultSlug || getVaultSlug();
13
+ }
14
+ export function unwrapResp(resp) {
15
+ if (typeof resp === "object" && resp !== null && "data" in resp) {
16
+ return resp.data;
17
+ }
18
+ return resp;
19
+ }
package/dist/main.js CHANGED
@@ -5,6 +5,8 @@ import { ApiError, AstraError } from "./errors.js";
5
5
  import { registerAuthCommand } from "./commands/auth.js";
6
6
  import { registerInitCommand, printContext } from "./commands/init.js";
7
7
  import { registerAstraCommand } from "./commands/astra.js";
8
+ import { registerBrainCommand } from "./commands/brain.js";
9
+ import { registerSessionsCommand } from "./commands/sessions.js";
8
10
  const require = createRequire(import.meta.url);
9
11
  const { version } = require("../package.json");
10
12
  const SKIP_BANNER_COMMANDS = new Set(["auth", "init"]);
@@ -26,6 +28,8 @@ export async function cli() {
26
28
  registerAuthCommand(program);
27
29
  registerInitCommand(program);
28
30
  registerAstraCommand(program);
31
+ registerBrainCommand(program);
32
+ registerSessionsCommand(program);
29
33
  // Propagate helpWidth to all subcommands
30
34
  const helpWidth = process.stdout.columns || 200;
31
35
  for (const cmd of program.commands) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobi-ai/cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "CLI client for the Gobi collaborative knowledge platform",
5
5
  "license": "MIT",
6
6
  "type": "module",