@gobi-ai/cli 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,12 +34,12 @@ npm link
34
34
  ## Quick start
35
35
 
36
36
  ```sh
37
- # Authenticate with your Gobi account
38
- gobi auth login
39
-
40
- # Set up your space and vault
37
+ # Initialize logs in and sets up your vault
41
38
  gobi init
42
39
 
40
+ # Select a space
41
+ gobi astra warp
42
+
43
43
  # Search brains in your space
44
44
  gobi astra search-brain --query "machine learning"
45
45
 
@@ -61,7 +61,8 @@ gobi astra ask-brain --vault-slug my-vault --question "What is RAG?"
61
61
 
62
62
  | Command | Description |
63
63
  |---------|-------------|
64
- | `gobi init` | Interactive setup select your vault and space |
64
+ | `gobi init` | Log in (if needed) and select or create a vault |
65
+ | `gobi astra warp` | Select the active space |
65
66
 
66
67
  ### Brains
67
68
 
@@ -86,7 +87,6 @@ gobi astra ask-brain --vault-slug my-vault --question "What is RAG?"
86
87
 
87
88
  | Command | Description |
88
89
  |---------|-------------|
89
- | `gobi astra list-replies <postId>` | List replies to a post |
90
90
  | `gobi astra create-reply <postId> --content <c>` | Reply to a post |
91
91
  | `gobi astra edit-reply <replyId> --content <c>` | Edit a reply |
92
92
  | `gobi astra delete-reply <replyId>` | Delete a reply |
@@ -3,7 +3,7 @@ import { join } from "path";
3
3
  import { apiGet, apiPost, apiPatch, apiDelete } from "../client.js";
4
4
  import { WEBDRIVE_BASE_URL } from "../constants.js";
5
5
  import { getValidToken } from "../auth/manager.js";
6
- import { getSpaceSlug, getVaultSlug } from "./init.js";
6
+ import { getSpaceSlug, getVaultSlug, selectSpace, writeSpaceSetting } from "./init.js";
7
7
  function isJsonMode(cmd) {
8
8
  return !!cmd.parent?.opts().json;
9
9
  }
@@ -27,6 +27,23 @@ export function registerAstraCommand(program) {
27
27
  .command("astra")
28
28
  .description("Astra commands (posts, sessions, brains, brain updates).")
29
29
  .option("--space-slug <slug>", "Space slug (overrides .gobi/settings.yaml)");
30
+ // ── Warp (space selection) ──
31
+ astra
32
+ .command("warp")
33
+ .description("Select the active space for astra commands.")
34
+ .action(async () => {
35
+ const result = await selectSpace();
36
+ if (result === null) {
37
+ console.log("No space selected.");
38
+ return;
39
+ }
40
+ writeSpaceSetting(result.slug);
41
+ if (isJsonMode(astra)) {
42
+ jsonOut({ spaceSlug: result.slug, spaceName: result.name });
43
+ return;
44
+ }
45
+ console.log(`Warped to space "${result.name}" (${result.slug})`);
46
+ });
30
47
  // ── Brains ──
31
48
  astra
32
49
  .command("search-brain")
@@ -155,7 +172,7 @@ export function registerAstraCommand(program) {
155
172
  return;
156
173
  }
157
174
  const msg = (data.post || data);
158
- const replies = (data.replies || []);
175
+ const replies = (data.items || []);
159
176
  const totalReplies = pagination.total ||
160
177
  msg.replyCount ||
161
178
  0;
@@ -272,44 +289,7 @@ export function registerAstraCommand(program) {
272
289
  }
273
290
  console.log(`Post ${postId} deleted.`);
274
291
  });
275
- // ── Replies (list, create, edit, delete) ──
276
- astra
277
- .command("list-replies <postId>")
278
- .description("List replies to a post (paginated).")
279
- .option("--limit <number>", "Replies per page", "20")
280
- .option("--offset <number>", "Offset for reply pagination", "0")
281
- .action(async (postId, opts) => {
282
- const spaceSlug = resolveSpaceSlug(astra);
283
- const resp = (await apiGet(`/spaces/${spaceSlug}/posts/${postId}`, {
284
- limit: parseInt(opts.limit, 10),
285
- offset: parseInt(opts.offset, 10),
286
- }));
287
- const data = unwrapResp(resp);
288
- const pagination = (resp.pagination || {});
289
- if (isJsonMode(astra)) {
290
- jsonOut({
291
- replies: data.replies || [],
292
- pagination,
293
- });
294
- return;
295
- }
296
- const replies = (data.replies || []);
297
- const totalReplies = pagination.total || replies.length;
298
- if (!replies.length) {
299
- console.log("No replies found.");
300
- return;
301
- }
302
- const lines = [];
303
- for (const r of replies) {
304
- const author = r.author?.name ||
305
- `User ${r.authorId}`;
306
- const text = r.content;
307
- const truncated = text.length > 200 ? text.slice(0, 200) + "\u2026" : text;
308
- lines.push(`- [${r.id}] ${author}: ${truncated} (${r.createdAt})`);
309
- }
310
- console.log(`Replies (${replies.length} of ${totalReplies}):\n` +
311
- lines.join("\n"));
312
- });
292
+ // ── Replies (create, edit, delete) ──
313
293
  astra
314
294
  .command("create-reply <postId>")
315
295
  .description("Create a reply to a post in a space.")
@@ -455,15 +435,20 @@ export function registerAstraCommand(program) {
455
435
  });
456
436
  astra
457
437
  .command("update-session <sessionId>")
458
- .description('Update a session\'s mode. "auto" lets the AI respond automatically; "manual" requires human replies.')
459
- .requiredOption("--mode <mode>", 'Session mode: "auto" or "manual"')
438
+ .description('Update a session. "auto" lets the AI respond automatically; "manual" requires human replies.')
439
+ .option("--mode <mode>", 'Session mode: "auto" or "manual"')
460
440
  .action(async (sessionId, opts) => {
461
- if (opts.mode !== "auto" && opts.mode !== "manual") {
462
- throw new Error('Invalid mode. Must be "auto" or "manual".');
441
+ if (!opts.mode) {
442
+ throw new Error("Provide at least one option to update (e.g. --mode).");
463
443
  }
464
- const resp = (await apiPatch(`/session/${sessionId}`, {
465
- mode: opts.mode,
466
- }));
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));
467
452
  const data = unwrapResp(resp);
468
453
  if (isJsonMode(astra)) {
469
454
  jsonOut(data);
@@ -1,10 +1,60 @@
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, readSettings, runInitFlow } from "./init.js";
4
+ import { printContext } from "./init.js";
5
5
  function sleep(ms) {
6
6
  return new Promise((resolve) => setTimeout(resolve, ms));
7
7
  }
8
+ export async function runLoginFlow() {
9
+ const res = await fetch(`${BASE_URL}/auth/device`, {
10
+ method: "POST",
11
+ headers: { "Content-Type": "application/json" },
12
+ });
13
+ if (!res.ok) {
14
+ const body = (await res.text()) || "(no body)";
15
+ throw new DeviceCodeError(`Failed to initiate login: HTTP ${res.status}: ${body}`);
16
+ }
17
+ const deviceData = (await res.json());
18
+ const intervalS = deviceData.interval || 5;
19
+ const startMs = Date.now();
20
+ console.log(`Open this URL in your browser to log in:\n ${deviceData.verificationUri}`);
21
+ console.log(`Your user code: ${deviceData.userCode}`);
22
+ console.log("Waiting for authentication...");
23
+ while (Date.now() - startMs < POLL_MAX_DURATION_MS) {
24
+ await sleep(intervalS * 1000);
25
+ const tokenRes = await fetch(`${BASE_URL}/auth/device/token`, {
26
+ method: "POST",
27
+ headers: { "Content-Type": "application/json" },
28
+ body: JSON.stringify({ deviceCode: deviceData.deviceCode }),
29
+ });
30
+ if (!tokenRes.ok) {
31
+ const body = (await tokenRes.text()) || "(no body)";
32
+ throw new DeviceCodeError(`Token poll failed: HTTP ${tokenRes.status}: ${body}`);
33
+ }
34
+ const tokenData = (await tokenRes.json());
35
+ if ("accessToken" in tokenData) {
36
+ const user = tokenData.user;
37
+ const creds = {
38
+ accessToken: tokenData.accessToken,
39
+ refreshToken: tokenData.refreshToken,
40
+ expiresAt: Date.now() + tokenData.expiresIn * 1000,
41
+ user: {
42
+ id: user.id,
43
+ email: user.email,
44
+ name: user.name,
45
+ pictureUrl: user.pictureUrl || null,
46
+ },
47
+ };
48
+ await storeTokens(creds);
49
+ console.log(`Successfully logged in as ${user.name} (${user.email}).`);
50
+ return;
51
+ }
52
+ if (tokenData.status === "expired") {
53
+ throw new DeviceCodeError("Login session expired. Please try 'gobi auth login' again.");
54
+ }
55
+ }
56
+ throw new DeviceCodeError("Login timed out. Please try 'gobi auth login' again.");
57
+ }
8
58
  export function registerAuthCommand(program) {
9
59
  const auth = program
10
60
  .command("auth")
@@ -13,63 +63,7 @@ export function registerAuthCommand(program) {
13
63
  .command("login")
14
64
  .description("Log in to Gobi. Opens a browser URL for Google OAuth, then polls until authentication is complete.")
15
65
  .action(async () => {
16
- const res = await fetch(`${BASE_URL}/auth/device`, {
17
- method: "POST",
18
- headers: { "Content-Type": "application/json" },
19
- });
20
- if (!res.ok) {
21
- const body = (await res.text()) || "(no body)";
22
- throw new DeviceCodeError(`Failed to initiate login: HTTP ${res.status}: ${body}`);
23
- }
24
- const deviceData = (await res.json());
25
- const intervalS = deviceData.interval || 5;
26
- const startMs = Date.now();
27
- console.log(`Open this URL in your browser to log in:\n ${deviceData.verificationUri}`);
28
- console.log(`Your user code: ${deviceData.userCode}`);
29
- console.log("Waiting for authentication...");
30
- while (Date.now() - startMs < POLL_MAX_DURATION_MS) {
31
- await sleep(intervalS * 1000);
32
- const tokenRes = await fetch(`${BASE_URL}/auth/device/token`, {
33
- method: "POST",
34
- headers: { "Content-Type": "application/json" },
35
- body: JSON.stringify({ deviceCode: deviceData.deviceCode }),
36
- });
37
- if (!tokenRes.ok) {
38
- const body = (await tokenRes.text()) || "(no body)";
39
- throw new DeviceCodeError(`Token poll failed: HTTP ${tokenRes.status}: ${body}`);
40
- }
41
- const tokenData = (await tokenRes.json());
42
- if ("accessToken" in tokenData) {
43
- const user = tokenData.user;
44
- const creds = {
45
- accessToken: tokenData.accessToken,
46
- refreshToken: tokenData.refreshToken,
47
- expiresAt: Date.now() + tokenData.expiresIn * 1000,
48
- user: {
49
- id: user.id,
50
- email: user.email,
51
- name: user.name,
52
- pictureUrl: user.pictureUrl || null,
53
- },
54
- };
55
- await storeTokens(creds);
56
- console.log(`Successfully logged in as ${user.name} (${user.email}).`);
57
- const settings = readSettings();
58
- if (settings && settings.selectedSpaceSlug) {
59
- printContext();
60
- console.log("Run 'gobi init' to change.");
61
- }
62
- else {
63
- console.log("");
64
- await runInitFlow();
65
- }
66
- return;
67
- }
68
- if (tokenData.status === "expired") {
69
- throw new DeviceCodeError("Login session expired. Please try 'gobi auth login' again.");
70
- }
71
- }
72
- throw new DeviceCodeError("Login timed out. Please try 'gobi auth login' again.");
66
+ await runLoginFlow();
73
67
  });
74
68
  auth
75
69
  .command("status")
@@ -4,6 +4,7 @@ import inquirer from "inquirer";
4
4
  import yaml from "js-yaml";
5
5
  import { apiGet, apiPost } from "../client.js";
6
6
  import { isAuthenticated } from "../auth/manager.js";
7
+ import { runLoginFlow } from "./auth.js";
7
8
  const SETTINGS_DIR = ".gobi";
8
9
  const SETTINGS_FILE = "settings.yaml";
9
10
  function settingsPath() {
@@ -20,7 +21,7 @@ export function getSpaceSlug() {
20
21
  const settings = readSettings();
21
22
  const slug = settings?.selectedSpaceSlug;
22
23
  if (!slug) {
23
- throw new Error("Not initialized. Run 'gobi init' first.");
24
+ throw new Error("Space not set. Run 'gobi astra warp' first.");
24
25
  }
25
26
  return slug;
26
27
  }
@@ -42,16 +43,26 @@ export function printContext() {
42
43
  const vaultId = settings?.vaultSlug || "?";
43
44
  console.log(`Space: ${slug} | Vault: ${vaultId}`);
44
45
  }
45
- function writeSettings(vaultId, spaceSlug) {
46
- const path = settingsPath();
46
+ function ensureSettingsDir() {
47
47
  const dir = join(process.cwd(), SETTINGS_DIR);
48
48
  if (!existsSync(dir)) {
49
49
  mkdirSync(dir, { recursive: true });
50
50
  }
51
- const content = yaml.dump({ vaultSlug: vaultId, selectedSpaceSlug: spaceSlug }, { flowLevel: -1 });
52
- writeFileSync(path, content, "utf-8");
53
51
  }
54
- async function selectSpace() {
52
+ function writeSetting(key, value) {
53
+ ensureSettingsDir();
54
+ const path = settingsPath();
55
+ const existing = readSettings() || {};
56
+ existing[key] = value;
57
+ writeFileSync(path, yaml.dump(existing, { flowLevel: -1 }), "utf-8");
58
+ }
59
+ export function writeVaultSetting(vaultId) {
60
+ writeSetting("vaultSlug", vaultId);
61
+ }
62
+ export function writeSpaceSetting(spaceSlug) {
63
+ writeSetting("selectedSpaceSlug", spaceSlug);
64
+ }
65
+ export async function selectSpace() {
55
66
  const resp = (await apiGet("/spaces"));
56
67
  const spaces = (Array.isArray(resp) ? resp : resp.data || resp);
57
68
  if (!spaces || spaces.length === 0) {
@@ -150,9 +161,11 @@ async function createNewVault() {
150
161
  }
151
162
  export async function runInitFlow() {
152
163
  if (!isAuthenticated()) {
153
- throw new Error("Not authenticated. Run 'gobi auth login' first.");
164
+ console.log("Not logged in. Starting login flow...\n");
165
+ await runLoginFlow();
166
+ console.log("");
154
167
  }
155
- // Step 1: Select or create vault
168
+ // Select or create vault
156
169
  let vaultId;
157
170
  let vaultName;
158
171
  while (true) {
@@ -181,20 +194,9 @@ export async function runInitFlow() {
181
194
  }
182
195
  break;
183
196
  }
184
- // Step 2: Select space
185
- let spaceSlug;
186
- let spaceName;
187
- while (true) {
188
- const result = await selectSpace();
189
- if (result === null)
190
- continue;
191
- spaceSlug = result.slug;
192
- spaceName = result.name;
193
- break;
194
- }
195
- writeSettings(vaultId, spaceSlug);
196
- console.log(`Linked to space "${spaceName}" (${spaceSlug}) with vault "${vaultName}" (${vaultId})`);
197
- console.log(`Created ${SETTINGS_DIR}/${SETTINGS_FILE}`);
197
+ writeVaultSetting(vaultId);
198
+ console.log(`Vault set to "${vaultName}" (${vaultId})`);
199
+ console.log(`Updated ${SETTINGS_DIR}/${SETTINGS_FILE}`);
198
200
  // Create default BRAIN.md if it doesn't exist
199
201
  const brainPath = join(process.cwd(), "BRAIN.md");
200
202
  if (!existsSync(brainPath)) {
@@ -205,7 +207,7 @@ export async function runInitFlow() {
205
207
  export function registerInitCommand(program) {
206
208
  program
207
209
  .command("init")
208
- .description("Set up or change the space and vault linked to the current directory.")
210
+ .description("Log in (if needed) and select or create the vault for the current directory.")
209
211
  .action(async () => {
210
212
  await runInitFlow();
211
213
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobi-ai/cli",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "CLI client for the Gobi collaborative knowledge platform",
5
5
  "license": "MIT",
6
6
  "type": "module",