@gobi-ai/cli 0.2.0 → 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 +7 -6
- package/dist/commands/astra.js +37 -52
- package/dist/commands/auth.js +52 -58
- package/dist/commands/init.js +25 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -34,12 +34,12 @@ npm link
|
|
|
34
34
|
## Quick start
|
|
35
35
|
|
|
36
36
|
```sh
|
|
37
|
-
#
|
|
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` |
|
|
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 |
|
|
@@ -98,6 +98,7 @@ gobi astra ask-brain --vault-slug my-vault --question "What is RAG?"
|
|
|
98
98
|
| `gobi astra list-sessions` | List your sessions |
|
|
99
99
|
| `gobi astra get-session <id>` | Get a session and its messages |
|
|
100
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) |
|
|
101
102
|
|
|
102
103
|
### Brain updates
|
|
103
104
|
|
package/dist/commands/astra.js
CHANGED
|
@@ -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.
|
|
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 (
|
|
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.")
|
|
@@ -404,13 +384,13 @@ export function registerAstraCommand(program) {
|
|
|
404
384
|
limit,
|
|
405
385
|
offset,
|
|
406
386
|
}));
|
|
407
|
-
const
|
|
408
|
-
const
|
|
409
|
-
const total =
|
|
387
|
+
const items = (resp.data || []);
|
|
388
|
+
const pagination = (resp.pagination || {});
|
|
389
|
+
const total = pagination.total ?? items.length;
|
|
410
390
|
if (isJsonMode(astra)) {
|
|
411
391
|
jsonOut({
|
|
412
|
-
|
|
413
|
-
pagination:
|
|
392
|
+
items,
|
|
393
|
+
pagination: resp.pagination || {},
|
|
414
394
|
});
|
|
415
395
|
return;
|
|
416
396
|
}
|
|
@@ -455,15 +435,20 @@ export function registerAstraCommand(program) {
|
|
|
455
435
|
});
|
|
456
436
|
astra
|
|
457
437
|
.command("update-session <sessionId>")
|
|
458
|
-
.description('Update a session
|
|
459
|
-
.
|
|
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
|
|
462
|
-
throw new Error(
|
|
441
|
+
if (!opts.mode) {
|
|
442
|
+
throw new Error("Provide at least one option to update (e.g. --mode).");
|
|
463
443
|
}
|
|
464
|
-
const
|
|
465
|
-
|
|
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);
|
package/dist/commands/auth.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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")
|
package/dist/commands/init.js
CHANGED
|
@@ -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("
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
164
|
+
console.log("Not logged in. Starting login flow...\n");
|
|
165
|
+
await runLoginFlow();
|
|
166
|
+
console.log("");
|
|
154
167
|
}
|
|
155
|
-
//
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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("
|
|
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
|
});
|