@gobi-ai/cli 0.9.13 → 1.1.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.
@@ -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.9.13",
7
+ "version": "1.1.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 brains, publish brain documents, create threads, manage sessions, generate images and videos.",
12
- "version": "0.9.13",
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.1.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-brain",
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.9.13",
4
+ "version": "1.1.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-brain",
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 brains across your spaces
44
- gobi brain search --query "machine learning"
43
+ # Search public vaults
44
+ gobi vault search --query "machine learning"
45
45
 
46
- # Ask a brain a question
47
- gobi brain ask --vault-slug my-vault --question "What is RAG?"
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
- ### Brains
68
+ ### Vaults
69
69
 
70
70
  | Command | Description |
71
71
  |---------|-------------|
72
- | `gobi brain search --query <q>` | Search public brains by text and semantic similarity |
73
- | `gobi brain ask --vault-slug <slug> --question <q>` | Ask a brain a question (creates a 1:1 session) |
74
- | `gobi brain publish` | Upload `BRAIN.md` to your vault |
75
- | `gobi brain unpublish` | Remove `BRAIN.md` from your vault |
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 `PUBLISH.md` to your vault |
75
+ | `gobi vault unpublish` | Remove `PUBLISH.md` from your vault |
76
76
 
77
- Public brains are accessible at `https://gobispace.com/@{vaultSlug}`.
77
+ Public vaults are accessible at `https://gobispace.com/@{vaultSlug}`.
78
78
 
79
- `brain ask` also accepts `--rich-text <json>` (mutually exclusive with `--question`) and `--mode <auto|manual>`.
79
+ `vault ask` also accepts `--rich-text <json>` (mutually exclusive with `--question`) and `--mode <auto|manual>`.
80
80
 
81
- ### Brain Updates
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 brain list-updates` | List brain updates |
86
- | `gobi brain list-updates --mine` | List only your own brain updates |
87
- | `gobi brain post-update --title <t> --content <c>` | Post a brain update |
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:** Vault-update (formerly 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 `brain list-updates`, `brain post-update`, `brain edit-update`, `space create-thread`, `space edit-thread`, `space edit-reply` |
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 three commands in parallel:
12
- - `gobi --json space list-threads` — recent discussions in the 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
- - `gobi --json brain list-updates` learnings and brain updates shared by members
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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: space-share
3
- description: Summarize recent learnings from this session and draft a brain update to share to the active Gobi space.
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 brain update
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 brain update (2–5 bullet points max). Show it to the user and ask for confirmation before posting.
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 brain post-update --title "<short title>" --content "<confirmed content>"
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
+ }
@@ -205,11 +205,11 @@ export async function runInitFlow() {
205
205
  writeVaultSetting(vaultId);
206
206
  console.log(`Vault set to "${vaultName}" (${vaultId})`);
207
207
  console.log(`Updated ${SETTINGS_DIR}/${SETTINGS_FILE}`);
208
- // Create default BRAIN.md if it doesn't exist
209
- const brainPath = join(process.cwd(), "BRAIN.md");
210
- if (!existsSync(brainPath)) {
211
- writeFileSync(brainPath, `---\ntitle: ${vaultName}\ntags: []\ndescription:\nthumbnail:\nprompt:\n---\n`, "utf-8");
212
- console.log("Created BRAIN.md");
208
+ // Create default PUBLISH.md if it doesn't exist
209
+ const publishMdPath = join(process.cwd(), "PUBLISH.md");
210
+ if (!existsSync(publishMdPath)) {
211
+ writeFileSync(publishMdPath, `---\ntitle: ${vaultName}\ntags: []\ndescription:\nthumbnail:\nprompt:\n---\n`, "utf-8");
212
+ console.log("Created PUBLISH.md");
213
213
  }
214
214
  }
215
215
  export function registerInitCommand(program) {
@@ -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 (threads, replies).")
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,141 @@
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 a PUBLISH.md to make a vault public.");
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
+ vaultCmd
92
+ .command("publish")
93
+ .description("Upload PUBLISH.md to the vault root on webdrive. Triggers post-processing (vault sync, metadata update, Discord notification).")
94
+ .action(async () => {
95
+ const vaultId = getVaultSlug();
96
+ const filePath = join(process.cwd(), "PUBLISH.md");
97
+ if (!existsSync(filePath)) {
98
+ throw new Error(`PUBLISH.md not found in ${process.cwd()}`);
99
+ }
100
+ const content = readFileSync(filePath, "utf-8");
101
+ const token = await getValidToken();
102
+ const url = `${WEBDRIVE_BASE_URL}/api/v1/vaults/${vaultId}/file/PUBLISH.md`;
103
+ const res = await fetch(url, {
104
+ method: "PUT",
105
+ headers: {
106
+ Authorization: `Bearer ${token}`,
107
+ "Content-Type": "text/markdown",
108
+ },
109
+ body: content,
110
+ });
111
+ if (!res.ok) {
112
+ throw new Error(`Upload failed: HTTP ${res.status}: ${(await res.text()) || "(no body)"}`);
113
+ }
114
+ if (isJsonMode(vaultCmd)) {
115
+ jsonOut({ vaultId });
116
+ return;
117
+ }
118
+ console.log(`Published PUBLISH.md to vault "${vaultId}"`);
119
+ });
120
+ // ── Unpublish ──
121
+ vaultCmd
122
+ .command("unpublish")
123
+ .description("Delete PUBLISH.md from the vault on webdrive.")
124
+ .action(async () => {
125
+ const vaultId = getVaultSlug();
126
+ const token = await getValidToken();
127
+ const url = `${WEBDRIVE_BASE_URL}/api/v1/vaults/${vaultId}/file/PUBLISH.md`;
128
+ const res = await fetch(url, {
129
+ method: "DELETE",
130
+ headers: { Authorization: `Bearer ${token}` },
131
+ });
132
+ if (!res.ok) {
133
+ throw new Error(`Delete failed: HTTP ${res.status}: ${(await res.text()) || "(no body)"}`);
134
+ }
135
+ if (isJsonMode(vaultCmd)) {
136
+ jsonOut({ vaultId });
137
+ return;
138
+ }
139
+ console.log(`Deleted PUBLISH.md from vault "${vaultId}"`);
140
+ });
141
+ }