@gobi-ai/cli 1.0.0 → 1.2.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 +5 -4
- package/.claude-plugin/plugin.json +3 -2
- package/README.md +27 -31
- package/commands/space-explore.md +5 -3
- package/commands/space-share.md +5 -5
- package/dist/commands/brain.js +301 -0
- package/dist/commands/feed.js +116 -0
- package/dist/commands/space.js +1 -89
- package/dist/main.js +4 -4
- package/package.json +1 -1
- package/skills/{gobi-vault → gobi-brain}/SKILL.md +30 -27
- package/skills/gobi-brain/references/brain.md +163 -0
- package/skills/gobi-core/SKILL.md +4 -3
- package/skills/gobi-core/references/space.md +1 -4
- package/skills/gobi-feed/SKILL.md +43 -0
- package/skills/gobi-feed/references/feed.md +55 -0
- package/skills/gobi-space/SKILL.md +6 -31
- package/skills/gobi-space/references/space.md +1 -39
- package/dist/commands/global.js +0 -203
- package/dist/commands/vault.js +0 -143
- package/skills/gobi-space/references/global.md +0 -82
- package/skills/gobi-vault/references/vault.md +0 -66
|
@@ -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": "1.
|
|
7
|
+
"version": "1.2.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": "1.
|
|
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": "1.2.0",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "gobi-ai"
|
|
15
15
|
},
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"skills": [
|
|
19
19
|
"./skills/gobi-core",
|
|
20
20
|
"./skills/gobi-space",
|
|
21
|
-
"./skills/gobi-
|
|
21
|
+
"./skills/gobi-brain",
|
|
22
|
+
"./skills/gobi-feed",
|
|
22
23
|
"./skills/gobi-media",
|
|
23
24
|
"./skills/gobi-sense",
|
|
24
25
|
"./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": "1.
|
|
4
|
+
"version": "1.2.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "gobi-ai"
|
|
7
7
|
},
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"skills": [
|
|
10
10
|
"./skills/gobi-core",
|
|
11
11
|
"./skills/gobi-space",
|
|
12
|
-
"./skills/gobi-
|
|
12
|
+
"./skills/gobi-brain",
|
|
13
|
+
"./skills/gobi-feed",
|
|
13
14
|
"./skills/gobi-media",
|
|
14
15
|
"./skills/gobi-sense",
|
|
15
16
|
"./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 brains across your spaces
|
|
44
|
+
gobi brain search --query "machine learning"
|
|
45
45
|
|
|
46
|
-
# Ask a
|
|
47
|
-
gobi
|
|
46
|
+
# Ask a brain a question
|
|
47
|
+
gobi brain ask --vault-slug my-vault --question "What is RAG?"
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
## Commands
|
|
@@ -65,32 +65,40 @@ gobi vault 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
|
+
### Brains
|
|
69
69
|
|
|
70
70
|
| Command | Description |
|
|
71
71
|
|---------|-------------|
|
|
72
|
-
| `gobi
|
|
73
|
-
| `gobi
|
|
74
|
-
| `gobi
|
|
75
|
-
| `gobi
|
|
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 |
|
|
76
76
|
|
|
77
|
-
Public
|
|
77
|
+
Public brains are accessible at `https://gobispace.com/@{vaultSlug}`.
|
|
78
78
|
|
|
79
|
-
`
|
|
79
|
+
`brain ask` also accepts `--rich-text <json>` (mutually exclusive with `--question`) and `--mode <auto|manual>`.
|
|
80
80
|
|
|
81
|
-
###
|
|
81
|
+
### Brain Updates
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
| Command | Description |
|
|
84
|
+
|---------|-------------|
|
|
85
|
+
| `gobi brain post-update --title <t> --content <c>` | Post a brain update |
|
|
86
|
+
| `gobi brain edit-update <id> [--title <t>] [--content <c>]` | Edit a brain update (at least one required) |
|
|
87
|
+
| `gobi brain delete-update <id>` | Delete a brain update |
|
|
88
|
+
|
|
89
|
+
`post-update` and `edit-update` accept `--auto-attachments` to upload wiki-linked `[[files]]` before posting.
|
|
90
|
+
|
|
91
|
+
### Feed
|
|
84
92
|
|
|
85
93
|
| Command | Description |
|
|
86
94
|
|---------|-------------|
|
|
87
|
-
| `gobi
|
|
88
|
-
| `gobi
|
|
89
|
-
| `gobi
|
|
95
|
+
| `gobi feed list` | List recent brain updates from the global public feed |
|
|
96
|
+
| `gobi feed get <updateId>` | Get a feed brain update and its replies |
|
|
97
|
+
| `gobi feed reply <updateId> --content <c>` | Reply to a brain update in the feed |
|
|
90
98
|
|
|
91
|
-
|
|
99
|
+
`feed list` and `feed get` accept `--limit`/`--cursor` for pagination.
|
|
92
100
|
|
|
93
|
-
|
|
101
|
+
### Threads
|
|
94
102
|
|
|
95
103
|
| Command | Description |
|
|
96
104
|
|---------|-------------|
|
|
@@ -108,18 +116,6 @@ Public vaults are accessible at `https://gobispace.com/@{vaultSlug}`.
|
|
|
108
116
|
| `gobi space edit-reply <replyId> --content <c>` | Edit a reply |
|
|
109
117
|
| `gobi space delete-reply <replyId>` | Delete a reply |
|
|
110
118
|
|
|
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
|
-
|
|
123
119
|
### Sessions
|
|
124
120
|
|
|
125
121
|
| Command | Description |
|
|
@@ -164,7 +160,7 @@ Times are ISO 8601 UTC (e.g. `2026-03-20T00:00:00Z`).
|
|
|
164
160
|
|--------|-------|-------------|
|
|
165
161
|
| `--json` | All commands | Output results as JSON |
|
|
166
162
|
| `--space-slug <slug>` | `space` commands | Override the default space (from `.gobi/settings.yaml`) |
|
|
167
|
-
| `--vault-slug <slug>` | Per-command | Override the default vault; available on `space create-thread`, `space edit-thread`, `space edit-reply` |
|
|
163
|
+
| `--vault-slug <slug>` | Per-command | Override the default vault; available on `brain post-update`, `brain edit-update`, `space create-thread`, `space edit-thread`, `space edit-reply` |
|
|
168
164
|
|
|
169
165
|
## Configuration
|
|
170
166
|
|
|
@@ -8,11 +8,13 @@ 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 commands in parallel:
|
|
12
|
-
- `gobi --json space
|
|
11
|
+
1. Run these three commands in parallel:
|
|
12
|
+
- `gobi --json space list-threads` — recent discussions in the space
|
|
13
13
|
- `gobi --json space list-topics` — available topics across the platform
|
|
14
|
-
|
|
14
|
+
- `gobi --json feed list` — learnings and brain updates shared by members across the platform
|
|
15
|
+
2. Display results in a readable summary, grouped by type (Topics / Discussions / Learnings).
|
|
15
16
|
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.
|
|
16
17
|
4. Ask the user if they'd like to read anything in full:
|
|
17
18
|
- For a topic: run `gobi space list-topic-threads <topicSlug>` and show the threads.
|
|
18
19
|
- For a thread: run `gobi space get-thread <threadId>` and show it with replies.
|
|
20
|
+
- For a feed brain update: run `gobi feed get <updateId>` to show the update with its replies.
|
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 brain update 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 brain update
|
|
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 brain update (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:
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
gobi
|
|
42
|
+
gobi brain post-update --title "<short title>" --content "<confirmed content>"
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
Confirm success and show the user the result.
|
|
@@ -0,0 +1,301 @@
|
|
|
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
|
+
import { extractWikiLinks, uploadAttachments } from "../attachments.js";
|
|
9
|
+
export function registerBrainCommand(program) {
|
|
10
|
+
const brain = program
|
|
11
|
+
.command("brain")
|
|
12
|
+
.description("Brain commands (search, ask, publish, unpublish, updates).");
|
|
13
|
+
// ── Search ──
|
|
14
|
+
brain
|
|
15
|
+
.command("search")
|
|
16
|
+
.description("Search public brains by text and semantic similarity.")
|
|
17
|
+
.requiredOption("--query <query>", "Search query")
|
|
18
|
+
.action(async (opts) => {
|
|
19
|
+
const resp = (await apiGet(`/vault/public/search`, {
|
|
20
|
+
query: opts.query,
|
|
21
|
+
}));
|
|
22
|
+
const results = (Array.isArray(resp) ? resp : resp.data || resp);
|
|
23
|
+
if (isJsonMode(brain)) {
|
|
24
|
+
jsonOut(results || []);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!results || results.length === 0) {
|
|
28
|
+
console.log(`No brains found matching "${opts.query}".`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const lines = [];
|
|
32
|
+
for (const entry of results) {
|
|
33
|
+
const vault = (entry.vault || entry);
|
|
34
|
+
const owner = (entry.owner || {});
|
|
35
|
+
const ownerName = owner.name ? ` by ${owner.name}` : "";
|
|
36
|
+
const sim = entry.similarity != null
|
|
37
|
+
? ` [similarity: ${entry.similarity.toFixed(3)}]`
|
|
38
|
+
: "";
|
|
39
|
+
const spaceSlug = (entry.spaceSlug || vault.spaceSlug || "");
|
|
40
|
+
const vaultSlug = (vault.slug || vault.vaultSlug || vault.id || "N/A");
|
|
41
|
+
lines.push(`- ${vault.name || vault.title || "N/A"} (vault: ${vaultSlug}, space: ${spaceSlug || "N/A"})${ownerName}${sim}`);
|
|
42
|
+
}
|
|
43
|
+
console.log(`Brains matching "${opts.query}":\n` + lines.join("\n"));
|
|
44
|
+
});
|
|
45
|
+
// ── Ask ──
|
|
46
|
+
brain
|
|
47
|
+
.command("ask")
|
|
48
|
+
.description("Ask a brain a question. Creates a targeted session (1:1 conversation).")
|
|
49
|
+
.requiredOption("--vault-slug <vaultSlug>", "Slug of the brain/vault to ask")
|
|
50
|
+
.option("--question <question>", "The question to ask (markdown supported)")
|
|
51
|
+
.option("--rich-text <richText>", "Rich-text JSON array (e.g. [{\"type\":\"text\",\"text\":\"hello\"}])")
|
|
52
|
+
.option("--mode <mode>", 'Session mode: "auto" or "manual"')
|
|
53
|
+
.action(async (opts) => {
|
|
54
|
+
if (!opts.question && !opts.richText) {
|
|
55
|
+
throw new Error("Provide either --question or --rich-text.");
|
|
56
|
+
}
|
|
57
|
+
if (opts.question && opts.richText) {
|
|
58
|
+
throw new Error("--question and --rich-text are mutually exclusive.");
|
|
59
|
+
}
|
|
60
|
+
const body = {
|
|
61
|
+
vaultSlug: opts.vaultSlug,
|
|
62
|
+
};
|
|
63
|
+
if (opts.question != null)
|
|
64
|
+
body.question = opts.question;
|
|
65
|
+
if (opts.richText != null) {
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(opts.richText);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new Error("Invalid --rich-text JSON.");
|
|
72
|
+
}
|
|
73
|
+
body.richText = parsed;
|
|
74
|
+
}
|
|
75
|
+
if (opts.mode != null)
|
|
76
|
+
body.mode = opts.mode;
|
|
77
|
+
const resp = (await apiPost(`/chat/targeted`, body));
|
|
78
|
+
const data = unwrapResp(resp);
|
|
79
|
+
if (isJsonMode(brain)) {
|
|
80
|
+
jsonOut(data);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const session = (data.session || {});
|
|
84
|
+
const members = (data.members || []);
|
|
85
|
+
console.log(`Session created!\n` +
|
|
86
|
+
` Session ID: ${session.id}\n` +
|
|
87
|
+
` Mode: ${session.mode}\n` +
|
|
88
|
+
` Members: ${members.length}\n` +
|
|
89
|
+
` Question sent.`);
|
|
90
|
+
});
|
|
91
|
+
// ── Publish ──
|
|
92
|
+
brain
|
|
93
|
+
.command("publish")
|
|
94
|
+
.description("Upload BRAIN.md to the vault root on webdrive. Triggers post-processing (brain sync, metadata update, Discord notification).")
|
|
95
|
+
.action(async () => {
|
|
96
|
+
const vaultId = getVaultSlug();
|
|
97
|
+
const filePath = join(process.cwd(), "BRAIN.md");
|
|
98
|
+
if (!existsSync(filePath)) {
|
|
99
|
+
throw new Error(`BRAIN.md not found in ${process.cwd()}`);
|
|
100
|
+
}
|
|
101
|
+
const content = readFileSync(filePath, "utf-8");
|
|
102
|
+
const token = await getValidToken();
|
|
103
|
+
const url = `${WEBDRIVE_BASE_URL}/api/v1/vaults/${vaultId}/file/BRAIN.md`;
|
|
104
|
+
const res = await fetch(url, {
|
|
105
|
+
method: "PUT",
|
|
106
|
+
headers: {
|
|
107
|
+
Authorization: `Bearer ${token}`,
|
|
108
|
+
"Content-Type": "text/markdown",
|
|
109
|
+
},
|
|
110
|
+
body: content,
|
|
111
|
+
});
|
|
112
|
+
if (!res.ok) {
|
|
113
|
+
throw new Error(`Upload failed: HTTP ${res.status}: ${(await res.text()) || "(no body)"}`);
|
|
114
|
+
}
|
|
115
|
+
if (isJsonMode(brain)) {
|
|
116
|
+
jsonOut({ vaultId });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
console.log(`Published BRAIN.md to vault "${vaultId}"`);
|
|
120
|
+
});
|
|
121
|
+
// ── Unpublish ──
|
|
122
|
+
brain
|
|
123
|
+
.command("unpublish")
|
|
124
|
+
.description("Delete BRAIN.md from the vault on webdrive.")
|
|
125
|
+
.action(async () => {
|
|
126
|
+
const vaultId = getVaultSlug();
|
|
127
|
+
const token = await getValidToken();
|
|
128
|
+
const url = `${WEBDRIVE_BASE_URL}/api/v1/vaults/${vaultId}/file/BRAIN.md`;
|
|
129
|
+
const res = await fetch(url, {
|
|
130
|
+
method: "DELETE",
|
|
131
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
132
|
+
});
|
|
133
|
+
if (!res.ok) {
|
|
134
|
+
throw new Error(`Delete failed: HTTP ${res.status}: ${(await res.text()) || "(no body)"}`);
|
|
135
|
+
}
|
|
136
|
+
if (isJsonMode(brain)) {
|
|
137
|
+
jsonOut({ vaultId });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
console.log(`Deleted BRAIN.md from vault "${vaultId}"`);
|
|
141
|
+
});
|
|
142
|
+
// ── Updates (post, edit, delete) ──
|
|
143
|
+
brain
|
|
144
|
+
.command("post-update")
|
|
145
|
+
.description("Post a brain update for a vault.")
|
|
146
|
+
.option("--vault-slug <vaultSlug>", "Vault slug (overrides .gobi/settings.yaml)")
|
|
147
|
+
.requiredOption("--title <title>", "Title of the update")
|
|
148
|
+
.requiredOption("--content <content>", "Update content (markdown supported)")
|
|
149
|
+
.option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before posting")
|
|
150
|
+
.action(async (opts) => {
|
|
151
|
+
const vaultSlug = resolveVaultSlug(opts);
|
|
152
|
+
if (opts.autoAttachments) {
|
|
153
|
+
const token = await getValidToken();
|
|
154
|
+
const links = extractWikiLinks(opts.content);
|
|
155
|
+
await uploadAttachments(vaultSlug, links, token, { addToSyncfiles: true });
|
|
156
|
+
}
|
|
157
|
+
const resp = (await apiPost(`/brain-updates/vault/${vaultSlug}`, {
|
|
158
|
+
title: opts.title,
|
|
159
|
+
content: opts.content,
|
|
160
|
+
}));
|
|
161
|
+
const u = unwrapResp(resp);
|
|
162
|
+
if (isJsonMode(brain)) {
|
|
163
|
+
jsonOut(u);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
console.log(`Brain update posted!\n` +
|
|
167
|
+
` ID: ${u.id}\n` +
|
|
168
|
+
` Title: ${u.title}\n` +
|
|
169
|
+
` Vault: ${u.vaultSlug || vaultSlug}\n` +
|
|
170
|
+
` Created: ${u.createdAt}`);
|
|
171
|
+
});
|
|
172
|
+
brain
|
|
173
|
+
.command("edit-update <updateId>")
|
|
174
|
+
.description("Edit a published brain update. You must be the author.")
|
|
175
|
+
.option("--title <title>", "New title for the update")
|
|
176
|
+
.option("--content <content>", "New content for the update (markdown supported)")
|
|
177
|
+
.option("--vault-slug <vaultSlug>", "Vault slug for attachment uploads (overrides .gobi/settings.yaml)")
|
|
178
|
+
.option("--auto-attachments", "Upload wiki-linked [[files]] to webdrive before editing")
|
|
179
|
+
.action(async (updateId, opts) => {
|
|
180
|
+
if (!opts.title && !opts.content) {
|
|
181
|
+
throw new Error("Provide at least --title or --content to update.");
|
|
182
|
+
}
|
|
183
|
+
if (opts.autoAttachments && opts.content) {
|
|
184
|
+
const vaultSlug = resolveVaultSlug(opts);
|
|
185
|
+
const token = await getValidToken();
|
|
186
|
+
const links = extractWikiLinks(opts.content);
|
|
187
|
+
await uploadAttachments(vaultSlug, links, token, { addToSyncfiles: true });
|
|
188
|
+
}
|
|
189
|
+
const body = {};
|
|
190
|
+
if (opts.title != null)
|
|
191
|
+
body.title = opts.title;
|
|
192
|
+
if (opts.content != null)
|
|
193
|
+
body.content = opts.content;
|
|
194
|
+
const resp = (await apiPatch(`/brain-updates/${updateId}`, body));
|
|
195
|
+
const u = unwrapResp(resp);
|
|
196
|
+
if (isJsonMode(brain)) {
|
|
197
|
+
jsonOut(u);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
console.log(`Brain update edited!\n` +
|
|
201
|
+
` ID: ${u.id}\n` +
|
|
202
|
+
` Title: ${u.title}\n` +
|
|
203
|
+
` Updated: ${u.updatedAt}`);
|
|
204
|
+
});
|
|
205
|
+
brain
|
|
206
|
+
.command("delete-update <updateId>")
|
|
207
|
+
.description("Delete a published brain update. You must be the author.")
|
|
208
|
+
.action(async (updateId) => {
|
|
209
|
+
await apiDelete(`/brain-updates/${updateId}`);
|
|
210
|
+
if (isJsonMode(brain)) {
|
|
211
|
+
jsonOut({ id: updateId });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
console.log(`Brain update ${updateId} deleted.`);
|
|
215
|
+
});
|
|
216
|
+
// ── Update Replies (get-update, reply-to-update, edit-update-reply, delete-update-reply) ──
|
|
217
|
+
brain
|
|
218
|
+
.command("get-update <updateId>")
|
|
219
|
+
.description("Get a brain update and its replies (paginated).")
|
|
220
|
+
.option("--limit <number>", "Replies per page", "20")
|
|
221
|
+
.option("--cursor <string>", "Pagination cursor from previous response")
|
|
222
|
+
.option("--full", "Show full reply content without truncation")
|
|
223
|
+
.action(async (updateId, opts) => {
|
|
224
|
+
const params = {
|
|
225
|
+
limit: parseInt(opts.limit, 10),
|
|
226
|
+
};
|
|
227
|
+
if (opts.cursor)
|
|
228
|
+
params.cursor = opts.cursor;
|
|
229
|
+
const resp = (await apiGet(`/brain-updates/${updateId}`, params));
|
|
230
|
+
const data = unwrapResp(resp);
|
|
231
|
+
const pagination = (resp.pagination || {});
|
|
232
|
+
if (isJsonMode(brain)) {
|
|
233
|
+
jsonOut({ ...data, pagination });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const update = (data.update || data);
|
|
237
|
+
const replies = (data.replies || []);
|
|
238
|
+
const author = update.author?.name ||
|
|
239
|
+
`User ${update.authorId}`;
|
|
240
|
+
const vault = update.vault?.vaultSlug || "?";
|
|
241
|
+
const replyLines = [];
|
|
242
|
+
for (const r of replies) {
|
|
243
|
+
const rAuthor = r.author?.name ||
|
|
244
|
+
`User ${r.authorId}`;
|
|
245
|
+
const text = r.content;
|
|
246
|
+
const truncated = opts.full || text.length <= 200 ? text : text.slice(0, 200) + "\u2026";
|
|
247
|
+
replyLines.push(` - ${rAuthor}: ${truncated} (${r.createdAt})`);
|
|
248
|
+
}
|
|
249
|
+
const output = [
|
|
250
|
+
`Brain Update: ${update.title || "(no title)"}`,
|
|
251
|
+
`By: ${author} (vault: ${vault}) on ${update.createdAt}`,
|
|
252
|
+
"",
|
|
253
|
+
update.content,
|
|
254
|
+
"",
|
|
255
|
+
`Replies (${replies.length} items):`,
|
|
256
|
+
...replyLines,
|
|
257
|
+
...(pagination.hasMore
|
|
258
|
+
? [` Next cursor: ${pagination.nextCursor}`]
|
|
259
|
+
: []),
|
|
260
|
+
].join("\n");
|
|
261
|
+
console.log(output);
|
|
262
|
+
});
|
|
263
|
+
brain
|
|
264
|
+
.command("reply-to-update <updateId>")
|
|
265
|
+
.description("Reply to a brain update.")
|
|
266
|
+
.requiredOption("--content <content>", 'Reply content (markdown supported, use "-" for stdin)')
|
|
267
|
+
.action(async (updateId, opts) => {
|
|
268
|
+
const content = opts.content === "-" ? readFileSync("/dev/stdin", "utf8") : opts.content;
|
|
269
|
+
const resp = (await apiPost(`/brain-updates/${updateId}/replies`, { content }));
|
|
270
|
+
const reply = unwrapResp(resp);
|
|
271
|
+
if (isJsonMode(brain)) {
|
|
272
|
+
jsonOut(reply);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
console.log(`Reply created!\n ID: ${reply.id}\n Created: ${reply.createdAt}`);
|
|
276
|
+
});
|
|
277
|
+
brain
|
|
278
|
+
.command("edit-update-reply <replyId>")
|
|
279
|
+
.description("Edit a brain update reply. You must be the author.")
|
|
280
|
+
.requiredOption("--content <content>", "New content for the reply (markdown supported)")
|
|
281
|
+
.action(async (replyId, opts) => {
|
|
282
|
+
const resp = (await apiPatch(`/brain-updates/replies/${replyId}`, { content: opts.content }));
|
|
283
|
+
const reply = unwrapResp(resp);
|
|
284
|
+
if (isJsonMode(brain)) {
|
|
285
|
+
jsonOut(reply);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
console.log(`Reply edited!\n ID: ${reply.id}\n Edited: ${reply.editedAt}`);
|
|
289
|
+
});
|
|
290
|
+
brain
|
|
291
|
+
.command("delete-update-reply <replyId>")
|
|
292
|
+
.description("Delete a brain update reply. You must be the author.")
|
|
293
|
+
.action(async (replyId) => {
|
|
294
|
+
await apiDelete(`/brain-updates/replies/${replyId}`);
|
|
295
|
+
if (isJsonMode(brain)) {
|
|
296
|
+
jsonOut({ replyId });
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
console.log(`Brain update reply ${replyId} deleted.`);
|
|
300
|
+
});
|
|
301
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
import { apiGet, apiPost } from "../client.js";
|
|
3
|
+
import { isJsonMode, jsonOut, unwrapResp } from "./utils.js";
|
|
4
|
+
export function registerFeedCommand(program) {
|
|
5
|
+
const feed = program
|
|
6
|
+
.command("feed")
|
|
7
|
+
.description("Feed of brain updates from people across the platform.");
|
|
8
|
+
// ── List ──
|
|
9
|
+
feed
|
|
10
|
+
.command("list")
|
|
11
|
+
.description("List recent brain updates from the global public feed.")
|
|
12
|
+
.option("--limit <number>", "Items per page", "20")
|
|
13
|
+
.option("--cursor <string>", "Pagination cursor from previous response")
|
|
14
|
+
.action(async (opts) => {
|
|
15
|
+
const params = {
|
|
16
|
+
limit: parseInt(opts.limit, 10),
|
|
17
|
+
};
|
|
18
|
+
if (opts.cursor)
|
|
19
|
+
params.cursor = opts.cursor;
|
|
20
|
+
const resp = (await apiGet(`/feed`, params));
|
|
21
|
+
if (isJsonMode(feed)) {
|
|
22
|
+
jsonOut({
|
|
23
|
+
items: resp.data || [],
|
|
24
|
+
pagination: resp.pagination || {},
|
|
25
|
+
});
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const items = (resp.data || []);
|
|
29
|
+
const pagination = (resp.pagination || {});
|
|
30
|
+
if (!items.length) {
|
|
31
|
+
console.log("No feed items found.");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const lines = [];
|
|
35
|
+
for (const u of items) {
|
|
36
|
+
const author = u.author?.name ||
|
|
37
|
+
`User ${u.authorId}`;
|
|
38
|
+
const vaultSlug = u.vault?.vaultSlug ||
|
|
39
|
+
u.authorVault?.vaultSlug ||
|
|
40
|
+
"?";
|
|
41
|
+
const replyCount = u.replyCount ?? 0;
|
|
42
|
+
const replies = replyCount ? `, ${replyCount} ${replyCount === 1 ? "reply" : "replies"}` : "";
|
|
43
|
+
lines.push(`- [${u.id}] "${u.title}" by ${author} (vault: ${vaultSlug}, ${u.createdAt}${replies})`);
|
|
44
|
+
}
|
|
45
|
+
const footer = pagination.hasMore
|
|
46
|
+
? `\n Next cursor: ${pagination.nextCursor}`
|
|
47
|
+
: "";
|
|
48
|
+
console.log(`Feed (${items.length} items):\n` + lines.join("\n") + footer);
|
|
49
|
+
});
|
|
50
|
+
// ── Get ──
|
|
51
|
+
feed
|
|
52
|
+
.command("get <updateId>")
|
|
53
|
+
.description("Get a feed brain update and its replies (paginated).")
|
|
54
|
+
.option("--limit <number>", "Replies per page", "20")
|
|
55
|
+
.option("--cursor <string>", "Pagination cursor from previous response")
|
|
56
|
+
.option("--full", "Show full reply content without truncation")
|
|
57
|
+
.action(async (updateId, opts) => {
|
|
58
|
+
const params = {
|
|
59
|
+
limit: parseInt(opts.limit, 10),
|
|
60
|
+
};
|
|
61
|
+
if (opts.cursor)
|
|
62
|
+
params.cursor = opts.cursor;
|
|
63
|
+
const resp = (await apiGet(`/feed/${updateId}`, params));
|
|
64
|
+
const data = unwrapResp(resp);
|
|
65
|
+
const pagination = (resp.pagination || {});
|
|
66
|
+
if (isJsonMode(feed)) {
|
|
67
|
+
jsonOut({ ...data, pagination });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const update = (data.update || data);
|
|
71
|
+
const replies = (data.replies || []);
|
|
72
|
+
const author = update.author?.name ||
|
|
73
|
+
`User ${update.authorId}`;
|
|
74
|
+
const vault = update.vault?.vaultSlug ||
|
|
75
|
+
update.authorVault?.vaultSlug ||
|
|
76
|
+
"?";
|
|
77
|
+
const replyLines = [];
|
|
78
|
+
for (const r of replies) {
|
|
79
|
+
const rAuthor = r.author?.name ||
|
|
80
|
+
`User ${r.authorId}`;
|
|
81
|
+
const text = r.content || "";
|
|
82
|
+
const truncated = opts.full || text.length <= 200 ? text : text.slice(0, 200) + "…";
|
|
83
|
+
replyLines.push(` - ${rAuthor}: ${truncated} (${r.createdAt})`);
|
|
84
|
+
}
|
|
85
|
+
const output = [
|
|
86
|
+
`Feed Update: ${update.title || "(no title)"}`,
|
|
87
|
+
`By: ${author} (vault: ${vault}) on ${update.createdAt}`,
|
|
88
|
+
"",
|
|
89
|
+
update.content || "",
|
|
90
|
+
"",
|
|
91
|
+
`Replies (${replies.length} items):`,
|
|
92
|
+
...replyLines,
|
|
93
|
+
...(pagination.hasMore
|
|
94
|
+
? [` Next cursor: ${pagination.nextCursor}`]
|
|
95
|
+
: []),
|
|
96
|
+
].join("\n");
|
|
97
|
+
console.log(output);
|
|
98
|
+
});
|
|
99
|
+
// ── Reply ──
|
|
100
|
+
feed
|
|
101
|
+
.command("reply <updateId>")
|
|
102
|
+
.description("Reply to a brain update in the feed.")
|
|
103
|
+
.requiredOption("--content <content>", 'Reply content (markdown supported, use "-" for stdin)')
|
|
104
|
+
.action(async (updateId, opts) => {
|
|
105
|
+
const content = opts.content === "-" ? readFileSync("/dev/stdin", "utf8") : opts.content;
|
|
106
|
+
const resp = (await apiPost(`/feed/${updateId}/replies`, {
|
|
107
|
+
content,
|
|
108
|
+
}));
|
|
109
|
+
const reply = unwrapResp(resp);
|
|
110
|
+
if (isJsonMode(feed)) {
|
|
111
|
+
jsonOut(reply);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
console.log(`Reply created!\n ID: ${reply.id}\n Created: ${reply.createdAt}`);
|
|
115
|
+
});
|
|
116
|
+
}
|