@gobi-ai/cli 0.9.13 → 1.0.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/.claude-plugin/marketplace.json +4 -4
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +32 -21
- package/commands/space-explore.md +3 -5
- package/commands/space-share.md +5 -5
- package/dist/commands/global.js +203 -0
- package/dist/commands/space.js +89 -1
- package/dist/commands/vault.js +143 -0
- package/dist/main.js +4 -2
- package/package.json +1 -1
- package/skills/gobi-core/SKILL.md +3 -4
- package/skills/gobi-core/references/space.md +4 -1
- package/skills/gobi-space/SKILL.md +31 -6
- package/skills/gobi-space/references/global.md +82 -0
- package/skills/gobi-space/references/space.md +39 -1
- package/skills/{gobi-brain → gobi-vault}/SKILL.md +26 -39
- package/skills/gobi-vault/references/vault.md +66 -0
- package/dist/commands/brain.js +0 -347
- package/skills/gobi-brain/references/brain.md +0 -181
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
"name": "gobi-ai"
|
|
5
5
|
},
|
|
6
6
|
"description": "Claude Code plugin for the Gobi collaborative knowledge platform CLI",
|
|
7
|
-
"version": "0.
|
|
7
|
+
"version": "1.0.0",
|
|
8
8
|
"plugins": [
|
|
9
9
|
{
|
|
10
10
|
"name": "gobi",
|
|
11
|
-
"description": "Manage the Gobi collaborative knowledge platform from the command line. Search and ask
|
|
12
|
-
"version": "0.
|
|
11
|
+
"description": "Manage the Gobi collaborative knowledge platform from the command line. Search and ask vaults, publish vault documents, create threads, manage sessions, generate images and videos.",
|
|
12
|
+
"version": "1.0.0",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "gobi-ai"
|
|
15
15
|
},
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"skills": [
|
|
19
19
|
"./skills/gobi-core",
|
|
20
20
|
"./skills/gobi-space",
|
|
21
|
-
"./skills/gobi-
|
|
21
|
+
"./skills/gobi-vault",
|
|
22
22
|
"./skills/gobi-media",
|
|
23
23
|
"./skills/gobi-sense",
|
|
24
24
|
"./skills/gobi-homepage"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gobi",
|
|
3
3
|
"description": "Manage the Gobi collaborative knowledge platform from the command line",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "1.0.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "gobi-ai"
|
|
7
7
|
},
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"skills": [
|
|
10
10
|
"./skills/gobi-core",
|
|
11
11
|
"./skills/gobi-space",
|
|
12
|
-
"./skills/gobi-
|
|
12
|
+
"./skills/gobi-vault",
|
|
13
13
|
"./skills/gobi-media",
|
|
14
14
|
"./skills/gobi-sense",
|
|
15
15
|
"./skills/gobi-homepage"
|
package/README.md
CHANGED
|
@@ -40,11 +40,11 @@ gobi init
|
|
|
40
40
|
# Select a space
|
|
41
41
|
gobi space warp
|
|
42
42
|
|
|
43
|
-
# Search
|
|
44
|
-
gobi
|
|
43
|
+
# Search public vaults
|
|
44
|
+
gobi vault search --query "machine learning"
|
|
45
45
|
|
|
46
|
-
# Ask a
|
|
47
|
-
gobi
|
|
46
|
+
# Ask a vault a question
|
|
47
|
+
gobi vault ask --vault-slug my-vault --question "What is RAG?"
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
## Commands
|
|
@@ -65,34 +65,33 @@ gobi brain ask --vault-slug my-vault --question "What is RAG?"
|
|
|
65
65
|
| `gobi space list` | List spaces you are a member of |
|
|
66
66
|
| `gobi space warp [spaceSlug]` | Select the active space (interactive if slug omitted) |
|
|
67
67
|
|
|
68
|
-
###
|
|
68
|
+
### Vaults
|
|
69
69
|
|
|
70
70
|
| Command | Description |
|
|
71
71
|
|---------|-------------|
|
|
72
|
-
| `gobi
|
|
73
|
-
| `gobi
|
|
74
|
-
| `gobi
|
|
75
|
-
| `gobi
|
|
72
|
+
| `gobi vault search --query <q>` | Search public vaults by text and semantic similarity |
|
|
73
|
+
| `gobi vault ask --vault-slug <slug> --question <q>` | Ask a vault a question (creates a 1:1 session) |
|
|
74
|
+
| `gobi vault publish` | Upload `BRAIN.md` to your vault |
|
|
75
|
+
| `gobi vault unpublish` | Remove `BRAIN.md` from your vault |
|
|
76
76
|
|
|
77
|
-
Public
|
|
77
|
+
Public vaults are accessible at `https://gobispace.com/@{vaultSlug}`.
|
|
78
78
|
|
|
79
|
-
`
|
|
79
|
+
`vault ask` also accepts `--rich-text <json>` (mutually exclusive with `--question`) and `--mode <auto|manual>`.
|
|
80
80
|
|
|
81
|
-
###
|
|
81
|
+
### Spaces
|
|
82
|
+
|
|
83
|
+
> Space and member administration (creating spaces, inviting/approving members, joining/leaving) is web-UI only and not available in the CLI.
|
|
82
84
|
|
|
83
85
|
| Command | Description |
|
|
84
86
|
|---------|-------------|
|
|
85
|
-
| `gobi
|
|
86
|
-
| `gobi
|
|
87
|
-
| `gobi
|
|
88
|
-
| `gobi brain edit-update <id> [--title <t>] [--content <c>]` | Edit a brain update (at least one required) |
|
|
89
|
-
| `gobi brain delete-update <id>` | Delete a brain update |
|
|
90
|
-
|
|
91
|
-
`list-updates` also accepts `--space-slug <slug>` to scope to a space, and `--limit`/`--cursor` for pagination.
|
|
92
|
-
`post-update` and `edit-update` accept `--auto-attachments` to upload wiki-linked `[[files]]` before posting.
|
|
87
|
+
| `gobi space get [spaceSlug]` | Show space details (uses current space if slug omitted) |
|
|
88
|
+
| `gobi space messages` | Unified message feed (threads + replies, newest first) |
|
|
89
|
+
| `gobi space ancestors <threadId>` | Walk a thread/reply's lineage from root → immediate parent |
|
|
93
90
|
|
|
94
91
|
### Threads
|
|
95
92
|
|
|
93
|
+
> **Migration note:** Brain-update commands have been removed. To post user-level content, use `gobi global create-thread` (platform-wide global) or `gobi space create-thread` (a specific space).
|
|
94
|
+
|
|
96
95
|
| Command | Description |
|
|
97
96
|
|---------|-------------|
|
|
98
97
|
| `gobi space list-threads` | List threads in the current space |
|
|
@@ -109,6 +108,18 @@ Public brains are accessible at `https://gobispace.com/@{vaultSlug}`.
|
|
|
109
108
|
| `gobi space edit-reply <replyId> --content <c>` | Edit a reply |
|
|
110
109
|
| `gobi space delete-reply <replyId>` | Delete a reply |
|
|
111
110
|
|
|
111
|
+
### Global thread space
|
|
112
|
+
|
|
113
|
+
The global thread space is a slugless message feed visible across all spaces.
|
|
114
|
+
|
|
115
|
+
| Command | Description |
|
|
116
|
+
|---------|-------------|
|
|
117
|
+
| `gobi global messages` | List the global unified message feed (newest first) |
|
|
118
|
+
| `gobi global get-thread <id>` | Get a global thread and its direct replies |
|
|
119
|
+
| `gobi global ancestors <id>` | Walk a global thread/reply's lineage |
|
|
120
|
+
| `gobi global create-thread [--title <t>] (--content <c> \| --rich-text <json>)` | Create a thread in the global space |
|
|
121
|
+
| `gobi global reply <threadId> (--content <c> \| --rich-text <json>)` | Reply to a global thread |
|
|
122
|
+
|
|
112
123
|
### Sessions
|
|
113
124
|
|
|
114
125
|
| Command | Description |
|
|
@@ -153,7 +164,7 @@ Times are ISO 8601 UTC (e.g. `2026-03-20T00:00:00Z`).
|
|
|
153
164
|
|--------|-------|-------------|
|
|
154
165
|
| `--json` | All commands | Output results as JSON |
|
|
155
166
|
| `--space-slug <slug>` | `space` commands | Override the default space (from `.gobi/settings.yaml`) |
|
|
156
|
-
| `--vault-slug <slug>` | Per-command | Override the default vault; available on `
|
|
167
|
+
| `--vault-slug <slug>` | Per-command | Override the default vault; available on `space create-thread`, `space edit-thread`, `space edit-reply` |
|
|
157
168
|
|
|
158
169
|
## Configuration
|
|
159
170
|
|
|
@@ -8,13 +8,11 @@ Always use the globally installed `gobi` binary (not via npx or ts-node).
|
|
|
8
8
|
|
|
9
9
|
Explore the active Gobi space to surface discussions, topics, and learnings from others:
|
|
10
10
|
|
|
11
|
-
1. Run these
|
|
12
|
-
- `gobi --json space
|
|
11
|
+
1. Run these commands in parallel:
|
|
12
|
+
- `gobi --json space messages` — unified feed of recent threads and replies in the space
|
|
13
13
|
- `gobi --json space list-topics` — available topics across the platform
|
|
14
|
-
|
|
15
|
-
2. Display results in a readable summary, grouped by type (Topics / Discussions / Learnings).
|
|
14
|
+
2. Display results in a readable summary, grouped by type (Topics / Discussions).
|
|
16
15
|
3. If `$ARGUMENTS` is provided, filter or highlight entries relevant to that topic or keyword. If a matching topic is found, also run `gobi --json space list-topic-threads <topicSlug>` to show threads tagged with that topic.
|
|
17
16
|
4. Ask the user if they'd like to read anything in full:
|
|
18
17
|
- For a topic: run `gobi space list-topic-threads <topicSlug>` and show the threads.
|
|
19
18
|
- For a thread: run `gobi space get-thread <threadId>` and show it with replies.
|
|
20
|
-
- For a brain update: show the full content from the list output.
|
package/commands/space-share.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: space-share
|
|
3
|
-
description: Summarize recent learnings from this session and draft a
|
|
3
|
+
description: Summarize recent learnings from this session and draft a thread to share to the active Gobi space.
|
|
4
4
|
argument-hint: "[context]"
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -16,7 +16,7 @@ gobi --json auth status
|
|
|
16
16
|
|
|
17
17
|
Check that `.gobi/settings.yaml` exists and contains both `vaultSlug` and `selectedSpaceSlug`. If not warped, stop and ask the user to run `/gobi:warp` first.
|
|
18
18
|
|
|
19
|
-
## Draft a
|
|
19
|
+
## Draft a thread
|
|
20
20
|
|
|
21
21
|
If `$ARGUMENTS` is provided, treat it as additional context or emphasis to guide the draft (e.g. "Emphasize the auth fix" or "Focus on the API design decision").
|
|
22
22
|
|
|
@@ -34,12 +34,12 @@ Focus on:
|
|
|
34
34
|
|
|
35
35
|
## Present to the user
|
|
36
36
|
|
|
37
|
-
Format the draft as a short
|
|
37
|
+
Format the draft as a short thread (2–5 bullet points max). Show it to the user and ask for confirmation before posting.
|
|
38
38
|
|
|
39
|
-
Once confirmed, post it:
|
|
39
|
+
Once confirmed, post it as a thread in the active space:
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
gobi
|
|
42
|
+
gobi space create-thread --title "<short title>" --content "<confirmed content>"
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
Confirm success and show the user the result.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { apiGet, apiPost } from "../client.js";
|
|
3
|
+
import { isJsonMode, jsonOut, unwrapResp } from "./utils.js";
|
|
4
|
+
function readContent(value) {
|
|
5
|
+
if (value === "-")
|
|
6
|
+
return readFileSync("/dev/stdin", "utf8");
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
function formatMessageLine(m) {
|
|
10
|
+
const isReply = m.parentThreadId != null;
|
|
11
|
+
const id = `[${isReply ? "r" : "t"}:${m.id}]`;
|
|
12
|
+
const kind = isReply ? "reply " : "thread";
|
|
13
|
+
const author = m.author?.name ||
|
|
14
|
+
`User ${m.authorId ?? "?"}`;
|
|
15
|
+
let label;
|
|
16
|
+
if (isReply) {
|
|
17
|
+
const text = m.content || "";
|
|
18
|
+
label = text.length > 80 ? text.slice(0, 80) + "…" : text;
|
|
19
|
+
label = label.replace(/\s+/g, " ").trim();
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
label = m.title || m.content || "";
|
|
23
|
+
}
|
|
24
|
+
return `${id} ${kind} ${author} "${label}" ${m.createdAt}`;
|
|
25
|
+
}
|
|
26
|
+
export function registerGlobalCommand(program) {
|
|
27
|
+
const global = program
|
|
28
|
+
.command("global")
|
|
29
|
+
.description("Global thread commands. Global is the platform-wide thread feed visible to everyone on Gobi.");
|
|
30
|
+
// ── Messages (unified feed) ──
|
|
31
|
+
global
|
|
32
|
+
.command("messages")
|
|
33
|
+
.description("List the global unified message feed (threads and replies, newest first).")
|
|
34
|
+
.option("--limit <number>", "Items per page", "20")
|
|
35
|
+
.option("--cursor <string>", "Pagination cursor from previous response")
|
|
36
|
+
.action(async (opts) => {
|
|
37
|
+
const params = {
|
|
38
|
+
limit: parseInt(opts.limit, 10),
|
|
39
|
+
};
|
|
40
|
+
if (opts.cursor)
|
|
41
|
+
params.cursor = opts.cursor;
|
|
42
|
+
const resp = (await apiGet(`/global/messages`, params));
|
|
43
|
+
if (isJsonMode(global)) {
|
|
44
|
+
jsonOut({
|
|
45
|
+
items: resp.data || [],
|
|
46
|
+
pagination: resp.pagination || {},
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const items = (resp.data || []);
|
|
51
|
+
const pagination = (resp.pagination || {});
|
|
52
|
+
if (!items.length) {
|
|
53
|
+
console.log("No messages found.");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const lines = items.map(formatMessageLine);
|
|
57
|
+
const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
|
|
58
|
+
console.log(`Global messages (${items.length} items, newest first):\n` + lines.join("\n") + footer);
|
|
59
|
+
});
|
|
60
|
+
// ── Get thread ──
|
|
61
|
+
global
|
|
62
|
+
.command("get-thread <threadId>")
|
|
63
|
+
.description("Get a global thread and its direct replies (paginated).")
|
|
64
|
+
.option("--limit <number>", "Replies per page", "20")
|
|
65
|
+
.option("--cursor <string>", "Pagination cursor from previous response")
|
|
66
|
+
.action(async (threadId, opts) => {
|
|
67
|
+
const params = {
|
|
68
|
+
limit: parseInt(opts.limit, 10),
|
|
69
|
+
};
|
|
70
|
+
if (opts.cursor)
|
|
71
|
+
params.cursor = opts.cursor;
|
|
72
|
+
const resp = (await apiGet(`/global/threads/${threadId}`, params));
|
|
73
|
+
const data = unwrapResp(resp);
|
|
74
|
+
const pagination = (resp.pagination || {});
|
|
75
|
+
if (isJsonMode(global)) {
|
|
76
|
+
jsonOut({ ...data, pagination });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const thread = (data.thread || data);
|
|
80
|
+
const replies = (data.items || []);
|
|
81
|
+
const author = thread.author?.name ||
|
|
82
|
+
`User ${thread.authorId}`;
|
|
83
|
+
const replyLines = [];
|
|
84
|
+
for (const r of replies) {
|
|
85
|
+
const rAuthor = r.author?.name ||
|
|
86
|
+
`User ${r.authorId}`;
|
|
87
|
+
const text = r.content || "";
|
|
88
|
+
const truncated = text.length > 200 ? text.slice(0, 200) + "…" : text;
|
|
89
|
+
replyLines.push(` - ${rAuthor}: ${truncated} (${r.createdAt})`);
|
|
90
|
+
}
|
|
91
|
+
const isReply = thread.parentThreadId != null;
|
|
92
|
+
const heading = isReply
|
|
93
|
+
? `Reply [r:${thread.id}]`
|
|
94
|
+
: `Thread: ${thread.title || "(no title)"}`;
|
|
95
|
+
const output = [
|
|
96
|
+
heading,
|
|
97
|
+
`By: ${author} on ${thread.createdAt}`,
|
|
98
|
+
"",
|
|
99
|
+
thread.content,
|
|
100
|
+
"",
|
|
101
|
+
`Replies (${replies.length} items):`,
|
|
102
|
+
...replyLines,
|
|
103
|
+
...(pagination.hasMore ? [` Next cursor: ${pagination.nextCursor}`] : []),
|
|
104
|
+
].join("\n");
|
|
105
|
+
console.log(output);
|
|
106
|
+
});
|
|
107
|
+
// ── Ancestors ──
|
|
108
|
+
global
|
|
109
|
+
.command("ancestors <threadId>")
|
|
110
|
+
.description("Show the ancestor lineage of a global thread or reply (root → immediate parent).")
|
|
111
|
+
.action(async (threadId) => {
|
|
112
|
+
const resp = (await apiGet(`/global/threads/${threadId}/ancestors`));
|
|
113
|
+
const data = unwrapResp(resp);
|
|
114
|
+
const ancestors = (data.ancestors || []);
|
|
115
|
+
if (isJsonMode(global)) {
|
|
116
|
+
jsonOut({ ancestors });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (!ancestors.length) {
|
|
120
|
+
console.log("No ancestors (this is a root thread).");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const lines = [];
|
|
124
|
+
ancestors.forEach((a, i) => {
|
|
125
|
+
lines.push(`${i + 1}. ${formatMessageLine(a)}`);
|
|
126
|
+
});
|
|
127
|
+
console.log(`Ancestors (${ancestors.length} items, root first):\n` + lines.join("\n"));
|
|
128
|
+
});
|
|
129
|
+
// ── Create thread ──
|
|
130
|
+
global
|
|
131
|
+
.command("create-thread")
|
|
132
|
+
.description("Create a global thread (visible platform-wide).")
|
|
133
|
+
.option("--title <title>", "Title of the thread")
|
|
134
|
+
.option("--content <content>", "Thread content (markdown supported, use \"-\" for stdin)")
|
|
135
|
+
.option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
|
|
136
|
+
.action(async (opts) => {
|
|
137
|
+
if (!opts.content && !opts.richText) {
|
|
138
|
+
throw new Error("Provide either --content or --rich-text.");
|
|
139
|
+
}
|
|
140
|
+
if (opts.content && opts.richText) {
|
|
141
|
+
throw new Error("--content and --rich-text are mutually exclusive.");
|
|
142
|
+
}
|
|
143
|
+
const body = {};
|
|
144
|
+
if (opts.title != null)
|
|
145
|
+
body.title = opts.title;
|
|
146
|
+
if (opts.content != null)
|
|
147
|
+
body.content = readContent(opts.content);
|
|
148
|
+
if (opts.richText != null) {
|
|
149
|
+
let parsed;
|
|
150
|
+
try {
|
|
151
|
+
parsed = JSON.parse(opts.richText);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
throw new Error("Invalid --rich-text JSON.");
|
|
155
|
+
}
|
|
156
|
+
body.richText = parsed;
|
|
157
|
+
}
|
|
158
|
+
const resp = (await apiPost(`/global/threads`, body));
|
|
159
|
+
const thread = unwrapResp(resp);
|
|
160
|
+
if (isJsonMode(global)) {
|
|
161
|
+
jsonOut(thread);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
console.log(`Global thread created!\n` +
|
|
165
|
+
` ID: ${thread.id}\n` +
|
|
166
|
+
(thread.title ? ` Title: ${thread.title}\n` : "") +
|
|
167
|
+
` Created: ${thread.createdAt}`);
|
|
168
|
+
});
|
|
169
|
+
// ── Reply ──
|
|
170
|
+
global
|
|
171
|
+
.command("reply <threadId>")
|
|
172
|
+
.description("Reply to a global thread.")
|
|
173
|
+
.option("--content <content>", "Reply content (markdown supported, use \"-\" for stdin)")
|
|
174
|
+
.option("--rich-text <richText>", "Rich-text JSON array (mutually exclusive with --content)")
|
|
175
|
+
.action(async (threadId, opts) => {
|
|
176
|
+
if (!opts.content && !opts.richText) {
|
|
177
|
+
throw new Error("Provide either --content or --rich-text.");
|
|
178
|
+
}
|
|
179
|
+
if (opts.content && opts.richText) {
|
|
180
|
+
throw new Error("--content and --rich-text are mutually exclusive.");
|
|
181
|
+
}
|
|
182
|
+
const body = {};
|
|
183
|
+
if (opts.content != null)
|
|
184
|
+
body.content = readContent(opts.content);
|
|
185
|
+
if (opts.richText != null) {
|
|
186
|
+
let parsed;
|
|
187
|
+
try {
|
|
188
|
+
parsed = JSON.parse(opts.richText);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
throw new Error("Invalid --rich-text JSON.");
|
|
192
|
+
}
|
|
193
|
+
body.richText = parsed;
|
|
194
|
+
}
|
|
195
|
+
const resp = (await apiPost(`/global/threads/${threadId}/replies`, body));
|
|
196
|
+
const reply = unwrapResp(resp);
|
|
197
|
+
if (isJsonMode(global)) {
|
|
198
|
+
jsonOut(reply);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
console.log(`Reply created!\n ID: ${reply.id}\n Created: ${reply.createdAt}`);
|
|
202
|
+
});
|
|
203
|
+
}
|
package/dist/commands/space.js
CHANGED
|
@@ -9,10 +9,27 @@ function readContent(value) {
|
|
|
9
9
|
return readFileSync("/dev/stdin", "utf8");
|
|
10
10
|
return value;
|
|
11
11
|
}
|
|
12
|
+
function formatMessageLine(m) {
|
|
13
|
+
const isReply = m.parentThreadId != null;
|
|
14
|
+
const id = `[${isReply ? "r" : "t"}:${m.id}]`;
|
|
15
|
+
const kind = isReply ? "reply " : "thread";
|
|
16
|
+
const author = m.author?.name ||
|
|
17
|
+
`User ${m.authorId ?? "?"}`;
|
|
18
|
+
let label;
|
|
19
|
+
if (isReply) {
|
|
20
|
+
const text = m.content || "";
|
|
21
|
+
label = text.length > 80 ? text.slice(0, 80) + "…" : text;
|
|
22
|
+
label = label.replace(/\s+/g, " ").trim();
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
label = m.title || m.content || "";
|
|
26
|
+
}
|
|
27
|
+
return `${id} ${kind} ${author} "${label}" ${m.createdAt}`;
|
|
28
|
+
}
|
|
12
29
|
export function registerSpaceCommand(program) {
|
|
13
30
|
const space = program
|
|
14
31
|
.command("space")
|
|
15
|
-
.description("Space commands
|
|
32
|
+
.description("Space commands. A Space is a shared room of members where they post threads and replies, organized by topics.")
|
|
16
33
|
.option("--space-slug <slug>", "Space slug (overrides .gobi/settings.yaml)");
|
|
17
34
|
// ── List spaces ──
|
|
18
35
|
space
|
|
@@ -36,6 +53,23 @@ export function registerSpaceCommand(program) {
|
|
|
36
53
|
}
|
|
37
54
|
console.log(`Spaces (${items.length}):\n` + lines.join("\n"));
|
|
38
55
|
});
|
|
56
|
+
// ── Get space ──
|
|
57
|
+
space
|
|
58
|
+
.command("get [spaceSlug]")
|
|
59
|
+
.description("Get details for a space. Pass a slug or omit to use the current space (from .gobi/settings.yaml or --space-slug).")
|
|
60
|
+
.action(async (spaceSlug) => {
|
|
61
|
+
const slug = spaceSlug || resolveSpaceSlug(space);
|
|
62
|
+
const resp = (await apiGet(`/spaces/${slug}`));
|
|
63
|
+
const s = unwrapResp(resp);
|
|
64
|
+
if (isJsonMode(space)) {
|
|
65
|
+
jsonOut(s);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const desc = s.description ? `\n Description: ${s.description}` : "";
|
|
69
|
+
console.log(`Space [${s.slug}] ${s.name}${desc}\n` +
|
|
70
|
+
` ID: ${s.id}\n` +
|
|
71
|
+
` Created: ${s.createdAt}`);
|
|
72
|
+
});
|
|
39
73
|
// ── Warp (space selection) ──
|
|
40
74
|
space
|
|
41
75
|
.command("warp [spaceSlug]")
|
|
@@ -126,6 +160,60 @@ export function registerSpaceCommand(program) {
|
|
|
126
160
|
lines.join("\n") +
|
|
127
161
|
footer);
|
|
128
162
|
});
|
|
163
|
+
// ── Messages (unified feed) ──
|
|
164
|
+
space
|
|
165
|
+
.command("messages")
|
|
166
|
+
.description("List the unified message feed (threads and replies, newest first) in a space.")
|
|
167
|
+
.option("--limit <number>", "Items per page", "20")
|
|
168
|
+
.option("--cursor <string>", "Pagination cursor from previous response")
|
|
169
|
+
.action(async (opts) => {
|
|
170
|
+
const spaceSlug = resolveSpaceSlug(space);
|
|
171
|
+
const params = {
|
|
172
|
+
limit: parseInt(opts.limit, 10),
|
|
173
|
+
};
|
|
174
|
+
if (opts.cursor)
|
|
175
|
+
params.cursor = opts.cursor;
|
|
176
|
+
const resp = (await apiGet(`/spaces/${spaceSlug}/messages`, params));
|
|
177
|
+
if (isJsonMode(space)) {
|
|
178
|
+
jsonOut({
|
|
179
|
+
items: resp.data || [],
|
|
180
|
+
pagination: resp.pagination || {},
|
|
181
|
+
});
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const items = (resp.data || []);
|
|
185
|
+
const pagination = (resp.pagination || {});
|
|
186
|
+
if (!items.length) {
|
|
187
|
+
console.log("No messages found.");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const lines = items.map(formatMessageLine);
|
|
191
|
+
const footer = pagination.hasMore ? `\n Next cursor: ${pagination.nextCursor}` : "";
|
|
192
|
+
console.log(`Messages (${items.length} items, newest first):\n` + lines.join("\n") + footer);
|
|
193
|
+
});
|
|
194
|
+
// ── Ancestors ──
|
|
195
|
+
space
|
|
196
|
+
.command("ancestors <threadId>")
|
|
197
|
+
.description("Show the ancestor lineage of a thread or reply (root → immediate parent).")
|
|
198
|
+
.action(async (threadId) => {
|
|
199
|
+
const spaceSlug = resolveSpaceSlug(space);
|
|
200
|
+
const resp = (await apiGet(`/spaces/${spaceSlug}/threads/${threadId}/ancestors`));
|
|
201
|
+
const data = unwrapResp(resp);
|
|
202
|
+
const ancestors = (data.ancestors || []);
|
|
203
|
+
if (isJsonMode(space)) {
|
|
204
|
+
jsonOut({ ancestors });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (!ancestors.length) {
|
|
208
|
+
console.log("No ancestors (this is a root thread).");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const lines = [];
|
|
212
|
+
ancestors.forEach((a, i) => {
|
|
213
|
+
lines.push(`${i + 1}. ${formatMessageLine(a)}`);
|
|
214
|
+
});
|
|
215
|
+
console.log(`Ancestors (${ancestors.length} items, root first):\n` + lines.join("\n"));
|
|
216
|
+
});
|
|
129
217
|
// ── Threads (get, list, create, edit, delete) ──
|
|
130
218
|
space
|
|
131
219
|
.command("get-thread <threadId>")
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { apiGet, apiPost } 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, unwrapResp } from "./utils.js";
|
|
8
|
+
export function registerVaultCommand(program) {
|
|
9
|
+
const vaultCmd = program
|
|
10
|
+
.command("vault")
|
|
11
|
+
.description("Vault commands. A Vault is your personal knowledge container — search public vaults, ask them questions, and publish your BRAIN.md profile.");
|
|
12
|
+
// ── Search ──
|
|
13
|
+
vaultCmd
|
|
14
|
+
.command("search")
|
|
15
|
+
.description("Search public vaults 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(vaultCmd)) {
|
|
23
|
+
jsonOut(results || []);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (!results || results.length === 0) {
|
|
27
|
+
console.log(`No vaults 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(`Vaults matching "${opts.query}":\n` + lines.join("\n"));
|
|
43
|
+
});
|
|
44
|
+
// ── Ask ──
|
|
45
|
+
vaultCmd
|
|
46
|
+
.command("ask")
|
|
47
|
+
.description("Ask a vault a question. Creates a targeted session (1:1 conversation).")
|
|
48
|
+
.requiredOption("--vault-slug <vaultSlug>", "Slug of the vault to ask")
|
|
49
|
+
.option("--question <question>", "The question to ask (markdown supported)")
|
|
50
|
+
.option("--rich-text <richText>", "Rich-text JSON array (e.g. [{\"type\":\"text\",\"text\":\"hello\"}])")
|
|
51
|
+
.option("--mode <mode>", 'Session mode: "auto" or "manual"')
|
|
52
|
+
.action(async (opts) => {
|
|
53
|
+
if (!opts.question && !opts.richText) {
|
|
54
|
+
throw new Error("Provide either --question or --rich-text.");
|
|
55
|
+
}
|
|
56
|
+
if (opts.question && opts.richText) {
|
|
57
|
+
throw new Error("--question and --rich-text are mutually exclusive.");
|
|
58
|
+
}
|
|
59
|
+
const body = {
|
|
60
|
+
vaultSlug: opts.vaultSlug,
|
|
61
|
+
};
|
|
62
|
+
if (opts.question != null)
|
|
63
|
+
body.question = opts.question;
|
|
64
|
+
if (opts.richText != null) {
|
|
65
|
+
let parsed;
|
|
66
|
+
try {
|
|
67
|
+
parsed = JSON.parse(opts.richText);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
throw new Error("Invalid --rich-text JSON.");
|
|
71
|
+
}
|
|
72
|
+
body.richText = parsed;
|
|
73
|
+
}
|
|
74
|
+
if (opts.mode != null)
|
|
75
|
+
body.mode = opts.mode;
|
|
76
|
+
const resp = (await apiPost(`/chat/targeted`, body));
|
|
77
|
+
const data = unwrapResp(resp);
|
|
78
|
+
if (isJsonMode(vaultCmd)) {
|
|
79
|
+
jsonOut(data);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const session = (data.session || {});
|
|
83
|
+
const members = (data.members || []);
|
|
84
|
+
console.log(`Session created!\n` +
|
|
85
|
+
` Session ID: ${session.id}\n` +
|
|
86
|
+
` Mode: ${session.mode}\n` +
|
|
87
|
+
` Members: ${members.length}\n` +
|
|
88
|
+
` Question sent.`);
|
|
89
|
+
});
|
|
90
|
+
// ── Publish ──
|
|
91
|
+
// Note: file is still named BRAIN.md on webdrive — the post-processing pipeline
|
|
92
|
+
// (gobi-webdrive) hardcodes the filename. Rename the file here once that ships.
|
|
93
|
+
vaultCmd
|
|
94
|
+
.command("publish")
|
|
95
|
+
.description("Upload BRAIN.md to the vault root on webdrive. Triggers post-processing (vault sync, metadata update, Discord notification).")
|
|
96
|
+
.action(async () => {
|
|
97
|
+
const vaultId = getVaultSlug();
|
|
98
|
+
const filePath = join(process.cwd(), "BRAIN.md");
|
|
99
|
+
if (!existsSync(filePath)) {
|
|
100
|
+
throw new Error(`BRAIN.md not found in ${process.cwd()}`);
|
|
101
|
+
}
|
|
102
|
+
const content = readFileSync(filePath, "utf-8");
|
|
103
|
+
const token = await getValidToken();
|
|
104
|
+
const url = `${WEBDRIVE_BASE_URL}/api/v1/vaults/${vaultId}/file/BRAIN.md`;
|
|
105
|
+
const res = await fetch(url, {
|
|
106
|
+
method: "PUT",
|
|
107
|
+
headers: {
|
|
108
|
+
Authorization: `Bearer ${token}`,
|
|
109
|
+
"Content-Type": "text/markdown",
|
|
110
|
+
},
|
|
111
|
+
body: content,
|
|
112
|
+
});
|
|
113
|
+
if (!res.ok) {
|
|
114
|
+
throw new Error(`Upload failed: HTTP ${res.status}: ${(await res.text()) || "(no body)"}`);
|
|
115
|
+
}
|
|
116
|
+
if (isJsonMode(vaultCmd)) {
|
|
117
|
+
jsonOut({ vaultId });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
console.log(`Published BRAIN.md to vault "${vaultId}"`);
|
|
121
|
+
});
|
|
122
|
+
// ── Unpublish ──
|
|
123
|
+
vaultCmd
|
|
124
|
+
.command("unpublish")
|
|
125
|
+
.description("Delete BRAIN.md from the vault on webdrive.")
|
|
126
|
+
.action(async () => {
|
|
127
|
+
const vaultId = getVaultSlug();
|
|
128
|
+
const token = await getValidToken();
|
|
129
|
+
const url = `${WEBDRIVE_BASE_URL}/api/v1/vaults/${vaultId}/file/BRAIN.md`;
|
|
130
|
+
const res = await fetch(url, {
|
|
131
|
+
method: "DELETE",
|
|
132
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
133
|
+
});
|
|
134
|
+
if (!res.ok) {
|
|
135
|
+
throw new Error(`Delete failed: HTTP ${res.status}: ${(await res.text()) || "(no body)"}`);
|
|
136
|
+
}
|
|
137
|
+
if (isJsonMode(vaultCmd)) {
|
|
138
|
+
jsonOut({ vaultId });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
console.log(`Deleted BRAIN.md from vault "${vaultId}"`);
|
|
142
|
+
});
|
|
143
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -5,7 +5,8 @@ import { ApiError, GobiError } from "./errors.js";
|
|
|
5
5
|
import { registerAuthCommand } from "./commands/auth.js";
|
|
6
6
|
import { registerInitCommand, printContext } from "./commands/init.js";
|
|
7
7
|
import { registerSpaceCommand } from "./commands/space.js";
|
|
8
|
-
import {
|
|
8
|
+
import { registerGlobalCommand } from "./commands/global.js";
|
|
9
|
+
import { registerVaultCommand } from "./commands/vault.js";
|
|
9
10
|
import { registerSessionsCommand } from "./commands/sessions.js";
|
|
10
11
|
import { registerSenseCommand } from "./commands/sense.js";
|
|
11
12
|
import { registerSyncCommand } from "./commands/sync.js";
|
|
@@ -32,7 +33,8 @@ export async function cli() {
|
|
|
32
33
|
registerAuthCommand(program);
|
|
33
34
|
registerInitCommand(program);
|
|
34
35
|
registerSpaceCommand(program);
|
|
35
|
-
|
|
36
|
+
registerGlobalCommand(program);
|
|
37
|
+
registerVaultCommand(program);
|
|
36
38
|
registerSessionsCommand(program);
|
|
37
39
|
registerSenseCommand(program);
|
|
38
40
|
registerSyncCommand(program);
|