@gobi-ai/cli 0.3.0 → 0.3.2
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 +32 -30
- package/dist/commands/astra.js +89 -447
- package/dist/commands/brain.js +227 -0
- package/dist/commands/init.js +14 -6
- package/dist/commands/sessions.js +138 -0
- package/dist/commands/utils.js +19 -0
- package/dist/main.js +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,13 +38,13 @@ npm link
|
|
|
38
38
|
gobi init
|
|
39
39
|
|
|
40
40
|
# Select a space
|
|
41
|
-
gobi
|
|
41
|
+
gobi space warp
|
|
42
42
|
|
|
43
|
-
# Search brains
|
|
44
|
-
gobi
|
|
43
|
+
# Search brains across your spaces
|
|
44
|
+
gobi brain search --query "machine learning"
|
|
45
45
|
|
|
46
46
|
# Ask a brain a question
|
|
47
|
-
gobi
|
|
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
|
|
65
|
+
| `gobi space warp` | Select the active space |
|
|
66
66
|
|
|
67
67
|
### Brains
|
|
68
68
|
|
|
69
69
|
| Command | Description |
|
|
70
70
|
|---------|-------------|
|
|
71
|
-
| `gobi
|
|
72
|
-
| `gobi
|
|
73
|
-
| `gobi
|
|
74
|
-
| `gobi
|
|
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
|
-
###
|
|
76
|
+
### Brain Updates
|
|
77
77
|
|
|
78
78
|
| Command | Description |
|
|
79
79
|
|---------|-------------|
|
|
80
|
-
| `gobi
|
|
81
|
-
| `gobi
|
|
82
|
-
| `gobi
|
|
83
|
-
| `gobi
|
|
84
|
-
| `gobi
|
|
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
|
-
###
|
|
86
|
+
### Threads
|
|
87
87
|
|
|
88
88
|
| Command | Description |
|
|
89
89
|
|---------|-------------|
|
|
90
|
-
| `gobi
|
|
91
|
-
| `gobi
|
|
92
|
-
| `gobi
|
|
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
|
-
###
|
|
96
|
+
### Replies
|
|
95
97
|
|
|
96
98
|
| Command | Description |
|
|
97
99
|
|---------|-------------|
|
|
98
|
-
| `gobi
|
|
99
|
-
| `gobi
|
|
100
|
-
| `gobi
|
|
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
|
-
###
|
|
104
|
+
### Sessions
|
|
104
105
|
|
|
105
106
|
| Command | Description |
|
|
106
107
|
|---------|-------------|
|
|
107
|
-
| `gobi
|
|
108
|
-
| `gobi
|
|
109
|
-
| `gobi
|
|
110
|
-
| `gobi
|
|
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 (
|
|
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
|
|
package/dist/commands/astra.js
CHANGED
|
@@ -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 {
|
|
5
|
-
import {
|
|
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
|
|
27
|
-
.command("
|
|
28
|
-
.description("
|
|
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
|
-
|
|
10
|
+
space
|
|
32
11
|
.command("warp")
|
|
33
|
-
.description("Select the active space
|
|
12
|
+
.description("Select the active space.")
|
|
34
13
|
.action(async () => {
|
|
35
14
|
const result = await selectSpace();
|
|
36
15
|
if (result === null) {
|
|
@@ -38,146 +17,37 @@ export function registerAstraCommand(program) {
|
|
|
38
17
|
return;
|
|
39
18
|
}
|
|
40
19
|
writeSpaceSetting(result.slug);
|
|
41
|
-
if (isJsonMode(
|
|
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
|
-
// ──
|
|
48
|
-
|
|
49
|
-
.command("
|
|
50
|
-
.description("
|
|
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
31
|
.option("--offset <number>", "Offset for reply pagination", "0")
|
|
162
|
-
.action(async (
|
|
163
|
-
const spaceSlug = resolveSpaceSlug(
|
|
164
|
-
const resp = (await apiGet(`/spaces/${spaceSlug}/
|
|
32
|
+
.action(async (threadId, opts) => {
|
|
33
|
+
const spaceSlug = resolveSpaceSlug(space);
|
|
34
|
+
const resp = (await apiGet(`/spaces/${spaceSlug}/threads/${threadId}`, {
|
|
165
35
|
limit: parseInt(opts.limit, 10),
|
|
166
36
|
offset: parseInt(opts.offset, 10),
|
|
167
37
|
}));
|
|
168
38
|
const data = unwrapResp(resp);
|
|
169
39
|
const pagination = (resp.pagination || {});
|
|
170
|
-
if (isJsonMode(
|
|
40
|
+
if (isJsonMode(space)) {
|
|
171
41
|
jsonOut({ ...data, pagination });
|
|
172
42
|
return;
|
|
173
43
|
}
|
|
174
|
-
const
|
|
44
|
+
const thread = (data.thread || data);
|
|
175
45
|
const replies = (data.items || []);
|
|
176
46
|
const totalReplies = pagination.total ||
|
|
177
|
-
|
|
47
|
+
thread.replyCount ||
|
|
178
48
|
0;
|
|
179
|
-
const author =
|
|
180
|
-
`User ${
|
|
49
|
+
const author = thread.author?.name ||
|
|
50
|
+
`User ${thread.authorId}`;
|
|
181
51
|
const replyLines = [];
|
|
182
52
|
for (const r of replies) {
|
|
183
53
|
const rAuthor = r.author?.name ||
|
|
@@ -187,28 +57,28 @@ export function registerAstraCommand(program) {
|
|
|
187
57
|
replyLines.push(` - ${rAuthor}: ${truncated} (${r.createdAt})`);
|
|
188
58
|
}
|
|
189
59
|
const output = [
|
|
190
|
-
`
|
|
191
|
-
`By: ${author} on ${
|
|
60
|
+
`Thread: ${thread.title}`,
|
|
61
|
+
`By: ${author} on ${thread.createdAt}`,
|
|
192
62
|
"",
|
|
193
|
-
|
|
63
|
+
thread.content,
|
|
194
64
|
"",
|
|
195
65
|
`Replies (${replies.length} of ${totalReplies}):`,
|
|
196
66
|
...replyLines,
|
|
197
67
|
].join("\n");
|
|
198
68
|
console.log(output);
|
|
199
69
|
});
|
|
200
|
-
|
|
201
|
-
.command("list-
|
|
202
|
-
.description("List
|
|
70
|
+
space
|
|
71
|
+
.command("list-threads")
|
|
72
|
+
.description("List threads in a space (paginated).")
|
|
203
73
|
.option("--limit <number>", "Items per page", "20")
|
|
204
74
|
.option("--offset <number>", "Offset for pagination", "0")
|
|
205
75
|
.action(async (opts) => {
|
|
206
|
-
const spaceSlug = resolveSpaceSlug(
|
|
207
|
-
const resp = (await apiGet(`/spaces/${spaceSlug}/
|
|
76
|
+
const spaceSlug = resolveSpaceSlug(space);
|
|
77
|
+
const resp = (await apiGet(`/spaces/${spaceSlug}/threads`, {
|
|
208
78
|
limit: parseInt(opts.limit, 10),
|
|
209
79
|
offset: parseInt(opts.offset, 10),
|
|
210
80
|
}));
|
|
211
|
-
if (isJsonMode(
|
|
81
|
+
if (isJsonMode(space)) {
|
|
212
82
|
jsonOut({
|
|
213
83
|
items: resp.data || [],
|
|
214
84
|
pagination: resp.pagination || {},
|
|
@@ -218,344 +88,116 @@ export function registerAstraCommand(program) {
|
|
|
218
88
|
const items = (resp.data || []);
|
|
219
89
|
const pagination = (resp.pagination || {});
|
|
220
90
|
if (!items.length) {
|
|
221
|
-
console.log("No
|
|
91
|
+
console.log("No threads found.");
|
|
222
92
|
return;
|
|
223
93
|
}
|
|
224
94
|
const lines = [];
|
|
225
|
-
for (const
|
|
226
|
-
const author =
|
|
227
|
-
`User ${
|
|
228
|
-
lines.push(`- [${
|
|
95
|
+
for (const t of items) {
|
|
96
|
+
const author = t.author?.name ||
|
|
97
|
+
`User ${t.authorId}`;
|
|
98
|
+
lines.push(`- [${t.id}] "${t.title}" by ${author} (${t.replyCount} replies, ${t.createdAt})`);
|
|
229
99
|
}
|
|
230
100
|
const total = pagination.total || items.length;
|
|
231
|
-
console.log(`
|
|
101
|
+
console.log(`Threads (${items.length} of ${total}):\n` + lines.join("\n"));
|
|
232
102
|
});
|
|
233
|
-
|
|
234
|
-
.command("create-
|
|
235
|
-
.description("Create a
|
|
236
|
-
.requiredOption("--title <title>", "Title of the
|
|
237
|
-
.requiredOption("--content <content>", "
|
|
103
|
+
space
|
|
104
|
+
.command("create-thread")
|
|
105
|
+
.description("Create a thread in a space.")
|
|
106
|
+
.requiredOption("--title <title>", "Title of the thread")
|
|
107
|
+
.requiredOption("--content <content>", "Thread content (markdown supported)")
|
|
238
108
|
.action(async (opts) => {
|
|
239
|
-
const spaceSlug = resolveSpaceSlug(
|
|
240
|
-
const resp = (await apiPost(`/spaces/${spaceSlug}/
|
|
109
|
+
const spaceSlug = resolveSpaceSlug(space);
|
|
110
|
+
const resp = (await apiPost(`/spaces/${spaceSlug}/threads`, {
|
|
241
111
|
title: opts.title,
|
|
242
112
|
content: opts.content,
|
|
243
113
|
}));
|
|
244
|
-
const
|
|
245
|
-
if (isJsonMode(
|
|
246
|
-
jsonOut(
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
console.log(`
|
|
250
|
-
` ID: ${
|
|
251
|
-
` Title: ${
|
|
252
|
-
` Created: ${
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
.command("edit-
|
|
256
|
-
.description("Edit a
|
|
257
|
-
.option("--title <title>", "New title for the
|
|
258
|
-
.option("--content <content>", "New content for the
|
|
259
|
-
.action(async (
|
|
114
|
+
const thread = unwrapResp(resp);
|
|
115
|
+
if (isJsonMode(space)) {
|
|
116
|
+
jsonOut(thread);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
console.log(`Thread created!\n` +
|
|
120
|
+
` ID: ${thread.id}\n` +
|
|
121
|
+
` Title: ${thread.title}\n` +
|
|
122
|
+
` Created: ${thread.createdAt}`);
|
|
123
|
+
});
|
|
124
|
+
space
|
|
125
|
+
.command("edit-thread <threadId>")
|
|
126
|
+
.description("Edit a thread. You must be the author.")
|
|
127
|
+
.option("--title <title>", "New title for the thread")
|
|
128
|
+
.option("--content <content>", "New content for the thread (markdown supported)")
|
|
129
|
+
.action(async (threadId, opts) => {
|
|
260
130
|
if (!opts.title && !opts.content) {
|
|
261
131
|
throw new Error("Provide at least --title or --content to update.");
|
|
262
132
|
}
|
|
263
|
-
const spaceSlug = resolveSpaceSlug(
|
|
133
|
+
const spaceSlug = resolveSpaceSlug(space);
|
|
264
134
|
const body = {};
|
|
265
135
|
if (opts.title != null)
|
|
266
136
|
body.title = opts.title;
|
|
267
137
|
if (opts.content != null)
|
|
268
138
|
body.content = opts.content;
|
|
269
|
-
const resp = (await apiPatch(`/spaces/${spaceSlug}/
|
|
270
|
-
const
|
|
271
|
-
if (isJsonMode(
|
|
272
|
-
jsonOut(
|
|
139
|
+
const resp = (await apiPatch(`/spaces/${spaceSlug}/threads/${threadId}`, body));
|
|
140
|
+
const thread = unwrapResp(resp);
|
|
141
|
+
if (isJsonMode(space)) {
|
|
142
|
+
jsonOut(thread);
|
|
273
143
|
return;
|
|
274
144
|
}
|
|
275
|
-
console.log(`
|
|
276
|
-
` ID: ${
|
|
277
|
-
` Title: ${
|
|
278
|
-
` Edited: ${
|
|
145
|
+
console.log(`Thread edited!\n` +
|
|
146
|
+
` ID: ${thread.id}\n` +
|
|
147
|
+
` Title: ${thread.title}\n` +
|
|
148
|
+
` Edited: ${thread.editedAt}`);
|
|
279
149
|
});
|
|
280
|
-
|
|
281
|
-
.command("delete-
|
|
282
|
-
.description("Delete a
|
|
283
|
-
.action(async (
|
|
284
|
-
const spaceSlug = resolveSpaceSlug(
|
|
285
|
-
await apiDelete(`/spaces/${spaceSlug}/
|
|
286
|
-
if (isJsonMode(
|
|
287
|
-
jsonOut({ id:
|
|
150
|
+
space
|
|
151
|
+
.command("delete-thread <threadId>")
|
|
152
|
+
.description("Delete a thread. You must be the author.")
|
|
153
|
+
.action(async (threadId) => {
|
|
154
|
+
const spaceSlug = resolveSpaceSlug(space);
|
|
155
|
+
await apiDelete(`/spaces/${spaceSlug}/threads/${threadId}`);
|
|
156
|
+
if (isJsonMode(space)) {
|
|
157
|
+
jsonOut({ id: threadId });
|
|
288
158
|
return;
|
|
289
159
|
}
|
|
290
|
-
console.log(`
|
|
160
|
+
console.log(`Thread ${threadId} deleted.`);
|
|
291
161
|
});
|
|
292
162
|
// ── Replies (create, edit, delete) ──
|
|
293
|
-
|
|
294
|
-
.command("create-reply <
|
|
295
|
-
.description("Create a reply to a
|
|
163
|
+
space
|
|
164
|
+
.command("create-reply <threadId>")
|
|
165
|
+
.description("Create a reply to a thread in a space.")
|
|
296
166
|
.requiredOption("--content <content>", "Reply content (markdown supported)")
|
|
297
|
-
.action(async (
|
|
298
|
-
const spaceSlug = resolveSpaceSlug(
|
|
299
|
-
const resp = (await apiPost(`/spaces/${spaceSlug}/
|
|
167
|
+
.action(async (threadId, opts) => {
|
|
168
|
+
const spaceSlug = resolveSpaceSlug(space);
|
|
169
|
+
const resp = (await apiPost(`/spaces/${spaceSlug}/threads/${threadId}/replies`, { content: opts.content }));
|
|
300
170
|
const msg = unwrapResp(resp);
|
|
301
|
-
if (isJsonMode(
|
|
171
|
+
if (isJsonMode(space)) {
|
|
302
172
|
jsonOut(msg);
|
|
303
173
|
return;
|
|
304
174
|
}
|
|
305
175
|
console.log(`Reply created!\n ID: ${msg.id}\n Created: ${msg.createdAt}`);
|
|
306
176
|
});
|
|
307
|
-
|
|
177
|
+
space
|
|
308
178
|
.command("edit-reply <replyId>")
|
|
309
179
|
.description("Edit a reply. You must be the author.")
|
|
310
180
|
.requiredOption("--content <content>", "New content for the reply (markdown supported)")
|
|
311
181
|
.action(async (replyId, opts) => {
|
|
312
|
-
const spaceSlug = resolveSpaceSlug(
|
|
182
|
+
const spaceSlug = resolveSpaceSlug(space);
|
|
313
183
|
const resp = (await apiPatch(`/spaces/${spaceSlug}/replies/${replyId}`, { content: opts.content }));
|
|
314
184
|
const msg = unwrapResp(resp);
|
|
315
|
-
if (isJsonMode(
|
|
185
|
+
if (isJsonMode(space)) {
|
|
316
186
|
jsonOut(msg);
|
|
317
187
|
return;
|
|
318
188
|
}
|
|
319
189
|
console.log(`Reply edited!\n ID: ${msg.id}\n Edited: ${msg.editedAt}`);
|
|
320
190
|
});
|
|
321
|
-
|
|
191
|
+
space
|
|
322
192
|
.command("delete-reply <replyId>")
|
|
323
193
|
.description("Delete a reply. You must be the author.")
|
|
324
194
|
.action(async (replyId) => {
|
|
325
|
-
const spaceSlug = resolveSpaceSlug(
|
|
195
|
+
const spaceSlug = resolveSpaceSlug(space);
|
|
326
196
|
await apiDelete(`/spaces/${spaceSlug}/replies/${replyId}`);
|
|
327
|
-
if (isJsonMode(
|
|
197
|
+
if (isJsonMode(space)) {
|
|
328
198
|
jsonOut({ replyId });
|
|
329
199
|
return;
|
|
330
200
|
}
|
|
331
201
|
console.log(`Reply ${replyId} deleted.`);
|
|
332
202
|
});
|
|
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
203
|
}
|
|
@@ -0,0 +1,227 @@
|
|
|
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 for a vault (paginated).")
|
|
130
|
+
.option("--vault-slug <vaultSlug>", "Vault slug (overrides .gobi/settings.yaml)")
|
|
131
|
+
.option("--mine", "List only my own brain updates")
|
|
132
|
+
.option("--limit <number>", "Items per page", "20")
|
|
133
|
+
.option("--offset <number>", "Offset for pagination", "0")
|
|
134
|
+
.action(async (opts) => {
|
|
135
|
+
const params = {
|
|
136
|
+
limit: parseInt(opts.limit, 10),
|
|
137
|
+
offset: parseInt(opts.offset, 10),
|
|
138
|
+
};
|
|
139
|
+
if (opts.mine)
|
|
140
|
+
params.mine = true;
|
|
141
|
+
if (opts.vaultSlug)
|
|
142
|
+
params.vaultSlug = opts.vaultSlug;
|
|
143
|
+
const resp = (await apiGet(`/brain-updates`, params));
|
|
144
|
+
if (isJsonMode(brain)) {
|
|
145
|
+
jsonOut({
|
|
146
|
+
items: resp.data || [],
|
|
147
|
+
pagination: resp.pagination || {},
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const items = (resp.data || []);
|
|
152
|
+
const pagination = (resp.pagination || {});
|
|
153
|
+
if (!items.length) {
|
|
154
|
+
console.log("No brain updates found.");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const lines = [];
|
|
158
|
+
for (const u of items) {
|
|
159
|
+
const author = u.author?.name ||
|
|
160
|
+
`User ${u.authorId}`;
|
|
161
|
+
const vaultSlug = u.vault?.vaultSlug ||
|
|
162
|
+
"?";
|
|
163
|
+
lines.push(`- [${u.id}] "${u.title}" by ${author} (vault: ${vaultSlug}, ${u.createdAt})`);
|
|
164
|
+
}
|
|
165
|
+
const total = pagination.total || items.length;
|
|
166
|
+
console.log(`Brain updates (${items.length} of ${total}):\n` + lines.join("\n"));
|
|
167
|
+
});
|
|
168
|
+
brain
|
|
169
|
+
.command("post-update")
|
|
170
|
+
.description("Post a brain update for a vault.")
|
|
171
|
+
.option("--vault-slug <vaultSlug>", "Vault slug (overrides .gobi/settings.yaml)")
|
|
172
|
+
.requiredOption("--title <title>", "Title of the update")
|
|
173
|
+
.requiredOption("--content <content>", "Update content (markdown supported)")
|
|
174
|
+
.action(async (opts) => {
|
|
175
|
+
const vaultSlug = resolveVaultSlug(opts);
|
|
176
|
+
const resp = (await apiPost(`/brain-updates/vault/${vaultSlug}`, {
|
|
177
|
+
title: opts.title,
|
|
178
|
+
content: opts.content,
|
|
179
|
+
}));
|
|
180
|
+
const u = unwrapResp(resp);
|
|
181
|
+
if (isJsonMode(brain)) {
|
|
182
|
+
jsonOut(u);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
console.log(`Brain update posted!\n` +
|
|
186
|
+
` ID: ${u.id}\n` +
|
|
187
|
+
` Title: ${u.title}\n` +
|
|
188
|
+
` Vault: ${u.vaultSlug || vaultSlug}\n` +
|
|
189
|
+
` Created: ${u.createdAt}`);
|
|
190
|
+
});
|
|
191
|
+
brain
|
|
192
|
+
.command("edit-update <updateId>")
|
|
193
|
+
.description("Edit a published brain update. You must be the author.")
|
|
194
|
+
.option("--title <title>", "New title for the update")
|
|
195
|
+
.option("--content <content>", "New content for the update (markdown supported)")
|
|
196
|
+
.action(async (updateId, opts) => {
|
|
197
|
+
if (!opts.title && !opts.content) {
|
|
198
|
+
throw new Error("Provide at least --title or --content to update.");
|
|
199
|
+
}
|
|
200
|
+
const body = {};
|
|
201
|
+
if (opts.title != null)
|
|
202
|
+
body.title = opts.title;
|
|
203
|
+
if (opts.content != null)
|
|
204
|
+
body.content = opts.content;
|
|
205
|
+
const resp = (await apiPatch(`/brain-updates/${updateId}`, body));
|
|
206
|
+
const u = unwrapResp(resp);
|
|
207
|
+
if (isJsonMode(brain)) {
|
|
208
|
+
jsonOut(u);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
console.log(`Brain update edited!\n` +
|
|
212
|
+
` ID: ${u.id}\n` +
|
|
213
|
+
` Title: ${u.title}\n` +
|
|
214
|
+
` Updated: ${u.updatedAt}`);
|
|
215
|
+
});
|
|
216
|
+
brain
|
|
217
|
+
.command("delete-update <updateId>")
|
|
218
|
+
.description("Delete a published brain update. You must be the author.")
|
|
219
|
+
.action(async (updateId) => {
|
|
220
|
+
await apiDelete(`/brain-updates/${updateId}`);
|
|
221
|
+
if (isJsonMode(brain)) {
|
|
222
|
+
jsonOut({ id: updateId });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
console.log(`Brain update ${updateId} deleted.`);
|
|
226
|
+
});
|
|
227
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
|
24
|
+
throw new Error("Space not set. Run 'gobi space warp' first.");
|
|
25
25
|
}
|
|
26
26
|
return slug;
|
|
27
27
|
}
|
|
@@ -35,13 +35,21 @@ export function getVaultSlug() {
|
|
|
35
35
|
}
|
|
36
36
|
export function printContext() {
|
|
37
37
|
const settings = readSettings();
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
const vault = settings?.vaultSlug;
|
|
39
|
+
const space = settings?.selectedSpaceSlug;
|
|
40
|
+
if (!vault && !space) {
|
|
41
|
+
console.log("Run 'gobi init' to set up, then 'gobi space warp' to select a space.");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (!vault) {
|
|
45
|
+
console.log("Vault not set. Run 'gobi init' to set up.");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!space) {
|
|
49
|
+
console.log(`Vault: ${vault} | Space not set. Run 'gobi space warp' to select a space.`);
|
|
41
50
|
return;
|
|
42
51
|
}
|
|
43
|
-
|
|
44
|
-
console.log(`Space: ${slug} | Vault: ${vaultId}`);
|
|
52
|
+
console.log(`Space: ${space} | Vault: ${vault}`);
|
|
45
53
|
}
|
|
46
54
|
function ensureSettingsDir() {
|
|
47
55
|
const dir = join(process.cwd(), SETTINGS_DIR);
|
|
@@ -0,0 +1,138 @@
|
|
|
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("--offset <number>", "Offset for message pagination", "0")
|
|
13
|
+
.action(async (sessionId, opts) => {
|
|
14
|
+
const resp = (await apiGet(`/session/${sessionId}`, {
|
|
15
|
+
limit: parseInt(opts.limit, 10),
|
|
16
|
+
offset: parseInt(opts.offset, 10),
|
|
17
|
+
}));
|
|
18
|
+
const data = unwrapResp(resp);
|
|
19
|
+
if (isJsonMode(sessions)) {
|
|
20
|
+
jsonOut(data);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const session = (data.session || data);
|
|
24
|
+
const messages = (data.messages || []);
|
|
25
|
+
const pagination = (data.pagination || {});
|
|
26
|
+
const totalMessages = pagination.total || messages.length;
|
|
27
|
+
const msgLines = [];
|
|
28
|
+
for (const m of messages) {
|
|
29
|
+
const author = m.author?.name ||
|
|
30
|
+
m.source ||
|
|
31
|
+
`User ${m.authorId}`;
|
|
32
|
+
const text = m.content;
|
|
33
|
+
const truncated = text.length > 200 ? text.slice(0, 200) + "\u2026" : text;
|
|
34
|
+
msgLines.push(` - ${author}: ${truncated} (${m.createdAt})`);
|
|
35
|
+
}
|
|
36
|
+
const output = [
|
|
37
|
+
`Session: ${session.title}`,
|
|
38
|
+
` ID: ${session.id}`,
|
|
39
|
+
` Mode: ${session.mode}`,
|
|
40
|
+
` Last activity: ${session.lastMessageAt}`,
|
|
41
|
+
"",
|
|
42
|
+
`Messages (${messages.length} of ${totalMessages}):`,
|
|
43
|
+
...msgLines,
|
|
44
|
+
].join("\n");
|
|
45
|
+
console.log(output);
|
|
46
|
+
});
|
|
47
|
+
// ── List ──
|
|
48
|
+
sessions
|
|
49
|
+
.command("list")
|
|
50
|
+
.description("List all sessions you are part of, sorted by most recent activity.")
|
|
51
|
+
.option("--space-slug <spaceSlug>", "Filter by space slug")
|
|
52
|
+
.option("--limit <number>", "Items per page", "20")
|
|
53
|
+
.option("--offset <number>", "Offset for pagination", "0")
|
|
54
|
+
.action(async (opts) => {
|
|
55
|
+
const limit = parseInt(opts.limit, 10);
|
|
56
|
+
const offset = parseInt(opts.offset, 10);
|
|
57
|
+
const query = { limit, offset };
|
|
58
|
+
if (opts.spaceSlug)
|
|
59
|
+
query.spaceSlug = opts.spaceSlug;
|
|
60
|
+
const resp = (await apiGet(`/session/my-sessions`, query));
|
|
61
|
+
const items = (resp.data || []);
|
|
62
|
+
const pagination = (resp.pagination || {});
|
|
63
|
+
const total = pagination.total ?? items.length;
|
|
64
|
+
if (isJsonMode(sessions)) {
|
|
65
|
+
jsonOut({
|
|
66
|
+
items,
|
|
67
|
+
pagination: resp.pagination || {},
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (!items.length) {
|
|
72
|
+
console.log("No sessions found.");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const lines = [];
|
|
76
|
+
for (const s of items) {
|
|
77
|
+
const title = s.title || "(no title)";
|
|
78
|
+
const members = s.members || [];
|
|
79
|
+
const memberCount = s.memberCount ?? 0;
|
|
80
|
+
let memberInfo = "";
|
|
81
|
+
if (members.length > 0) {
|
|
82
|
+
const names = members.map((m) => m.vaultName || m.name || "Unknown");
|
|
83
|
+
const overflow = memberCount - members.length - 1; // -1 for "me"
|
|
84
|
+
memberInfo = ` | with: ${names.join(", ")}`;
|
|
85
|
+
if (overflow > 0)
|
|
86
|
+
memberInfo += ` +${overflow} more`;
|
|
87
|
+
}
|
|
88
|
+
lines.push(`- [${s.id}] "${title}" (mode: ${s.mode}, last activity: ${s.lastMessageAt})${memberInfo}`);
|
|
89
|
+
}
|
|
90
|
+
console.log(`Sessions (${items.length} of ${total}):\n` + lines.join("\n"));
|
|
91
|
+
});
|
|
92
|
+
// ── Reply ──
|
|
93
|
+
sessions
|
|
94
|
+
.command("reply <sessionId>")
|
|
95
|
+
.description("Send a human reply to a session you are a member of.")
|
|
96
|
+
.requiredOption("--content <content>", "Reply content (markdown supported)")
|
|
97
|
+
.action(async (sessionId, opts) => {
|
|
98
|
+
const resp = (await apiPost(`/session/${sessionId}/reply`, {
|
|
99
|
+
content: opts.content,
|
|
100
|
+
}));
|
|
101
|
+
const msg = unwrapResp(resp);
|
|
102
|
+
if (isJsonMode(sessions)) {
|
|
103
|
+
jsonOut(msg);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
console.log(`Reply sent!\n` +
|
|
107
|
+
` Message ID: ${msg.id}\n` +
|
|
108
|
+
` Source: ${msg.source}\n` +
|
|
109
|
+
` Created: ${msg.createdAt}`);
|
|
110
|
+
});
|
|
111
|
+
// ── Update ──
|
|
112
|
+
sessions
|
|
113
|
+
.command("update <sessionId>")
|
|
114
|
+
.description('Update a session. "auto" lets the AI respond automatically; "manual" requires human replies.')
|
|
115
|
+
.option("--mode <mode>", 'Session mode: "auto" or "manual"')
|
|
116
|
+
.action(async (sessionId, opts) => {
|
|
117
|
+
if (!opts.mode) {
|
|
118
|
+
throw new Error("Provide at least one option to update (e.g. --mode).");
|
|
119
|
+
}
|
|
120
|
+
const body = {};
|
|
121
|
+
if (opts.mode != null) {
|
|
122
|
+
if (opts.mode !== "auto" && opts.mode !== "manual") {
|
|
123
|
+
throw new Error('Invalid mode. Must be "auto" or "manual".');
|
|
124
|
+
}
|
|
125
|
+
body.mode = opts.mode;
|
|
126
|
+
}
|
|
127
|
+
const resp = (await apiPatch(`/session/${sessionId}`, body));
|
|
128
|
+
const data = unwrapResp(resp);
|
|
129
|
+
if (isJsonMode(sessions)) {
|
|
130
|
+
jsonOut(data);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const session = (data.session || data);
|
|
134
|
+
console.log(`Session updated!\n` +
|
|
135
|
+
` ID: ${session.id}\n` +
|
|
136
|
+
` Mode: ${session.mode}`);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
@@ -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) {
|