@gobi-ai/cli 0.9.10 → 0.9.12
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.
|
|
7
|
+
"version": "0.9.12",
|
|
8
8
|
"plugins": [
|
|
9
9
|
{
|
|
10
10
|
"name": "gobi",
|
|
11
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.
|
|
12
|
+
"version": "0.9.12",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "gobi-ai"
|
|
15
15
|
},
|
package/dist/commands/brain.js
CHANGED
|
@@ -259,4 +259,89 @@ export function registerBrainCommand(program) {
|
|
|
259
259
|
}
|
|
260
260
|
console.log(`Brain update ${updateId} deleted.`);
|
|
261
261
|
});
|
|
262
|
+
// ── Update Replies (get-update, reply-to-update, edit-update-reply, delete-update-reply) ──
|
|
263
|
+
brain
|
|
264
|
+
.command("get-update <updateId>")
|
|
265
|
+
.description("Get a brain update and its replies (paginated).")
|
|
266
|
+
.option("--limit <number>", "Replies per page", "20")
|
|
267
|
+
.option("--cursor <string>", "Pagination cursor from previous response")
|
|
268
|
+
.option("--full", "Show full reply content without truncation")
|
|
269
|
+
.action(async (updateId, opts) => {
|
|
270
|
+
const params = {
|
|
271
|
+
limit: parseInt(opts.limit, 10),
|
|
272
|
+
};
|
|
273
|
+
if (opts.cursor)
|
|
274
|
+
params.cursor = opts.cursor;
|
|
275
|
+
const resp = (await apiGet(`/brain-updates/${updateId}`, params));
|
|
276
|
+
const data = unwrapResp(resp);
|
|
277
|
+
const pagination = (resp.pagination || {});
|
|
278
|
+
if (isJsonMode(brain)) {
|
|
279
|
+
jsonOut({ ...data, pagination });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const update = (data.update || data);
|
|
283
|
+
const replies = (data.replies || []);
|
|
284
|
+
const author = update.author?.name ||
|
|
285
|
+
`User ${update.authorId}`;
|
|
286
|
+
const vault = update.vault?.vaultSlug || "?";
|
|
287
|
+
const replyLines = [];
|
|
288
|
+
for (const r of replies) {
|
|
289
|
+
const rAuthor = r.author?.name ||
|
|
290
|
+
`User ${r.authorId}`;
|
|
291
|
+
const text = r.content;
|
|
292
|
+
const truncated = opts.full || text.length <= 200 ? text : text.slice(0, 200) + "\u2026";
|
|
293
|
+
replyLines.push(` - ${rAuthor}: ${truncated} (${r.createdAt})`);
|
|
294
|
+
}
|
|
295
|
+
const output = [
|
|
296
|
+
`Brain Update: ${update.title || "(no title)"}`,
|
|
297
|
+
`By: ${author} (vault: ${vault}) on ${update.createdAt}`,
|
|
298
|
+
"",
|
|
299
|
+
update.content,
|
|
300
|
+
"",
|
|
301
|
+
`Replies (${replies.length} items):`,
|
|
302
|
+
...replyLines,
|
|
303
|
+
...(pagination.hasMore
|
|
304
|
+
? [` Next cursor: ${pagination.nextCursor}`]
|
|
305
|
+
: []),
|
|
306
|
+
].join("\n");
|
|
307
|
+
console.log(output);
|
|
308
|
+
});
|
|
309
|
+
brain
|
|
310
|
+
.command("reply-to-update <updateId>")
|
|
311
|
+
.description("Reply to a brain update.")
|
|
312
|
+
.requiredOption("--content <content>", 'Reply content (markdown supported, use "-" for stdin)')
|
|
313
|
+
.action(async (updateId, opts) => {
|
|
314
|
+
const content = opts.content === "-" ? readFileSync("/dev/stdin", "utf8") : opts.content;
|
|
315
|
+
const resp = (await apiPost(`/brain-updates/${updateId}/replies`, { content }));
|
|
316
|
+
const reply = unwrapResp(resp);
|
|
317
|
+
if (isJsonMode(brain)) {
|
|
318
|
+
jsonOut(reply);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
console.log(`Reply created!\n ID: ${reply.id}\n Created: ${reply.createdAt}`);
|
|
322
|
+
});
|
|
323
|
+
brain
|
|
324
|
+
.command("edit-update-reply <replyId>")
|
|
325
|
+
.description("Edit a brain update reply. You must be the author.")
|
|
326
|
+
.requiredOption("--content <content>", "New content for the reply (markdown supported)")
|
|
327
|
+
.action(async (replyId, opts) => {
|
|
328
|
+
const resp = (await apiPatch(`/brain-updates/replies/${replyId}`, { content: opts.content }));
|
|
329
|
+
const reply = unwrapResp(resp);
|
|
330
|
+
if (isJsonMode(brain)) {
|
|
331
|
+
jsonOut(reply);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
console.log(`Reply edited!\n ID: ${reply.id}\n Edited: ${reply.editedAt}`);
|
|
335
|
+
});
|
|
336
|
+
brain
|
|
337
|
+
.command("delete-update-reply <replyId>")
|
|
338
|
+
.description("Delete a brain update reply. You must be the author.")
|
|
339
|
+
.action(async (replyId) => {
|
|
340
|
+
await apiDelete(`/brain-updates/replies/${replyId}`);
|
|
341
|
+
if (isJsonMode(brain)) {
|
|
342
|
+
jsonOut({ replyId });
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
console.log(`Brain update reply ${replyId} deleted.`);
|
|
346
|
+
});
|
|
262
347
|
}
|
package/package.json
CHANGED
|
@@ -6,19 +6,23 @@ Usage: gobi brain [options] [command]
|
|
|
6
6
|
Brain commands (search, ask, publish, unpublish, updates).
|
|
7
7
|
|
|
8
8
|
Options:
|
|
9
|
-
-h, --help
|
|
9
|
+
-h, --help display help for command
|
|
10
10
|
|
|
11
11
|
Commands:
|
|
12
|
-
search [options]
|
|
13
|
-
ask [options]
|
|
14
|
-
publish
|
|
15
|
-
unpublish
|
|
16
|
-
list-updates [options]
|
|
17
|
-
|
|
18
|
-
post-update [options]
|
|
19
|
-
edit-update [options] <updateId>
|
|
20
|
-
delete-update <updateId>
|
|
21
|
-
|
|
12
|
+
search [options] Search public brains by text and semantic similarity.
|
|
13
|
+
ask [options] Ask a brain a question. Creates a targeted session (1:1 conversation).
|
|
14
|
+
publish Upload BRAIN.md to the vault root on webdrive. Triggers post-processing (brain sync, metadata update, Discord notification).
|
|
15
|
+
unpublish Delete BRAIN.md from the vault on webdrive.
|
|
16
|
+
list-updates [options] List recent brain updates. Without --space-slug, lists all updates for you. With --space-slug, lists updates for that space. Use --mine to show only updates
|
|
17
|
+
by you.
|
|
18
|
+
post-update [options] Post a brain update for a vault.
|
|
19
|
+
edit-update [options] <updateId> Edit a published brain update. You must be the author.
|
|
20
|
+
delete-update <updateId> Delete a published brain update. You must be the author.
|
|
21
|
+
get-update [options] <updateId> Get a brain update and its replies (paginated).
|
|
22
|
+
reply-to-update [options] <updateId> Reply to a brain update.
|
|
23
|
+
edit-update-reply [options] <replyId> Edit a brain update reply. You must be the author.
|
|
24
|
+
delete-update-reply <replyId> Delete a brain update reply. You must be the author.
|
|
25
|
+
help [command] display help for command
|
|
22
26
|
```
|
|
23
27
|
|
|
24
28
|
## search
|
|
@@ -126,3 +130,52 @@ Delete a published brain update. You must be the author.
|
|
|
126
130
|
Options:
|
|
127
131
|
-h, --help display help for command
|
|
128
132
|
```
|
|
133
|
+
|
|
134
|
+
## get-update
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
Usage: gobi brain get-update [options] <updateId>
|
|
138
|
+
|
|
139
|
+
Get a brain update and its replies (paginated).
|
|
140
|
+
|
|
141
|
+
Options:
|
|
142
|
+
--limit <number> Replies per page (default: "20")
|
|
143
|
+
--cursor <string> Pagination cursor from previous response
|
|
144
|
+
--full Show full reply content without truncation
|
|
145
|
+
-h, --help display help for command
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## reply-to-update
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
Usage: gobi brain reply-to-update [options] <updateId>
|
|
152
|
+
|
|
153
|
+
Reply to a brain update.
|
|
154
|
+
|
|
155
|
+
Options:
|
|
156
|
+
--content <content> Reply content (markdown supported, use "-" for stdin)
|
|
157
|
+
-h, --help display help for command
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## edit-update-reply
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
Usage: gobi brain edit-update-reply [options] <replyId>
|
|
164
|
+
|
|
165
|
+
Edit a brain update reply. You must be the author.
|
|
166
|
+
|
|
167
|
+
Options:
|
|
168
|
+
--content <content> New content for the reply (markdown supported)
|
|
169
|
+
-h, --help display help for command
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## delete-update-reply
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
Usage: gobi brain delete-update-reply [options] <replyId>
|
|
176
|
+
|
|
177
|
+
Delete a brain update reply. You must be the author.
|
|
178
|
+
|
|
179
|
+
Options:
|
|
180
|
+
-h, --help display help for command
|
|
181
|
+
```
|
|
@@ -165,6 +165,164 @@ function resolveWikiImages(md) {
|
|
|
165
165
|
const html = marked.parse(resolveWikiImages(update.content));
|
|
166
166
|
```
|
|
167
167
|
|
|
168
|
+
**Open links in a new tab.** The homepage runs in a sandboxed iframe — clicking a rendered link replaces the iframe with the external page. Override the renderer so every `<a>` opens in a new tab:
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
const renderer = new marked.Renderer();
|
|
172
|
+
const origLink = renderer.link.bind(renderer);
|
|
173
|
+
renderer.link = (href, title, text) =>
|
|
174
|
+
origLink(href, title, text).replace('<a ', '<a target="_blank" rel="noopener" ');
|
|
175
|
+
marked.setOptions({ renderer });
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Plain-text previews.** For BU list cards, render a truncated preview with `escapeHtml(content.substring(0, 200))` — don't run markdown on a random substring, it produces broken HTML. Use `marked.parse(resolveWikiImages(content))` only for the full expanded view. Same for chat: `marked.parse(content)` for assistant messages, `escapeHtml(content)` for human messages.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Polished Homepage Patterns
|
|
183
|
+
|
|
184
|
+
Optional patterns that go beyond the minimal example. Pick the ones you need.
|
|
185
|
+
|
|
186
|
+
### Design tokens
|
|
187
|
+
|
|
188
|
+
Centralize colors and spacing in CSS custom properties so restyling is a one-line change:
|
|
189
|
+
|
|
190
|
+
```css
|
|
191
|
+
:root {
|
|
192
|
+
--bg: #000;
|
|
193
|
+
--fg: #fff;
|
|
194
|
+
--accent: #ccff00;
|
|
195
|
+
--grey-900: #111; /* card bg */
|
|
196
|
+
--grey-700: #2a2a2a; /* borders */
|
|
197
|
+
--grey-500: #606060; /* secondary text */
|
|
198
|
+
--border: 2px;
|
|
199
|
+
--transition: 0.15s ease;
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Pair with Google Fonts (e.g. Space Grotesk for headings, IBM Plex Mono for meta, Inter for body) via CDN `<link>`.
|
|
204
|
+
|
|
205
|
+
### Knowledge Graph from BU topics
|
|
206
|
+
|
|
207
|
+
Brain updates carry a `topics` array. Treat each topic as a node and any two topics co-occurring in the same BU as an edge — you get a force-directed graph of the vault's themes for free. Use [d3](https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js).
|
|
208
|
+
|
|
209
|
+
```js
|
|
210
|
+
// Separate data-building from rendering so the same graph can be drawn at multiple sizes.
|
|
211
|
+
function buildGraphData(updates) {
|
|
212
|
+
const counts = new Map(); // name → frequency
|
|
213
|
+
const edges = new Map(); // "a|b" → weight
|
|
214
|
+
for (const u of updates) {
|
|
215
|
+
const names = (u.topics || []).map(t => t.name);
|
|
216
|
+
for (const n of names) counts.set(n, (counts.get(n) || 0) + 1);
|
|
217
|
+
for (let i = 0; i < names.length; i++)
|
|
218
|
+
for (let j = i + 1; j < names.length; j++) {
|
|
219
|
+
const key = [names[i], names[j]].sort().join('|');
|
|
220
|
+
edges.set(key, (edges.get(key) || 0) + 1);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Top 20 by frequency, then drop orphans (nodes with no surviving edges).
|
|
224
|
+
const top = [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 20).map(([n]) => n);
|
|
225
|
+
const keep = new Set(top);
|
|
226
|
+
const links = [...edges].flatMap(([k, w]) => {
|
|
227
|
+
const [a, b] = k.split('|');
|
|
228
|
+
return keep.has(a) && keep.has(b) ? [{ source: a, target: b, weight: w }] : [];
|
|
229
|
+
});
|
|
230
|
+
const connected = new Set(links.flatMap(l => [l.source, l.target]));
|
|
231
|
+
const nodes = top.filter(n => connected.has(n)).map(n => ({ id: n, count: counts.get(n) }));
|
|
232
|
+
return { nodes, links };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function drawGraph(containerId, w, h, data, opts = {}) {
|
|
236
|
+
const { nodeRange = [4, 16], fontSize = '9px', distance = 60, charge = -80 } = opts;
|
|
237
|
+
const max = Math.max(...data.nodes.map(n => n.count), 1);
|
|
238
|
+
const r = d3.scaleSqrt().domain([1, max]).range(nodeRange);
|
|
239
|
+
const svg = d3.select('#' + containerId).append('svg').attr('width', w).attr('height', h);
|
|
240
|
+
// ... standard d3.forceSimulation with link/charge/center, then clamp in tick:
|
|
241
|
+
// node.attr('cx', d => d.x = Math.max(20, Math.min(w - 20, d.x)))
|
|
242
|
+
// node.attr('cy', d => d.y = Math.max(20, Math.min(h - 20, d.y)))
|
|
243
|
+
// Node fill: accent with opacity 0.3 + 0.7 * (count/max).
|
|
244
|
+
// Call d3.drag() on nodes so visitors can rearrange the graph.
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Tips:
|
|
249
|
+
- **Enrich the data.** One page of 8 BUs makes a sparse graph. Paginate 3–4 times (cap at ~32 BUs) before building.
|
|
250
|
+
- **Cache the built data** in a module-level variable so the full-screen overlay can reuse it without refetching.
|
|
251
|
+
- **Mini vs full presets.** Pass different `opts` — e.g. mini `{nodeRange:[4,16], fontSize:'9px', distance:60, charge:-80}`, full `{nodeRange:[8,32], fontSize:'12px', distance:120, charge:-200}`.
|
|
252
|
+
- Run a **separate simulation** for the full-scale instance — copy the nodes/links rather than sharing references, otherwise both graphs fight over the same positions.
|
|
253
|
+
|
|
254
|
+
### Full-screen overlay
|
|
255
|
+
|
|
256
|
+
Useful for expanding the K-Graph (or any small component) into a focused view:
|
|
257
|
+
|
|
258
|
+
```js
|
|
259
|
+
function openOverlay(renderInto) {
|
|
260
|
+
const o = document.createElement('div');
|
|
261
|
+
o.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.95);z-index:1000';
|
|
262
|
+
o.innerHTML = '<button id="x" style="position:absolute;top:16px;right:16px">CLOSE</button><div id="body" style="width:100%;height:100%"></div>';
|
|
263
|
+
document.body.appendChild(o);
|
|
264
|
+
document.body.style.overflow = 'hidden';
|
|
265
|
+
const close = () => { o.remove(); document.body.style.overflow = ''; document.removeEventListener('keydown', onKey); };
|
|
266
|
+
const onKey = e => { if (e.key === 'Escape') close(); };
|
|
267
|
+
o.querySelector('#x').onclick = close;
|
|
268
|
+
document.addEventListener('keydown', onKey);
|
|
269
|
+
renderInto(o.querySelector('#body'));
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Always restore `body.overflow` on close, and always remove the `keydown` listener.
|
|
274
|
+
|
|
275
|
+
### Brain update card — preview/full toggle
|
|
276
|
+
|
|
277
|
+
Show a truncated card that expands in place on click:
|
|
278
|
+
|
|
279
|
+
```js
|
|
280
|
+
card.onclick = (event) => {
|
|
281
|
+
// Link click guard — don't toggle when the user clicked a link inside the card.
|
|
282
|
+
if (event.target.closest('a')) return;
|
|
283
|
+
card.classList.toggle('expanded');
|
|
284
|
+
card.querySelector('.body').innerHTML = card.classList.contains('expanded')
|
|
285
|
+
? marked.parse(resolveWikiImages(update.content))
|
|
286
|
+
: escapeHtml(update.content.substring(0, 200));
|
|
287
|
+
};
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Chat suggestion chips
|
|
291
|
+
|
|
292
|
+
Empty chat looks dead. Show clickable prompt chips until the first message is sent:
|
|
293
|
+
|
|
294
|
+
```js
|
|
295
|
+
const prompts = ['What is this brain about?', 'Summarize the latest update', 'What topics come up most?'];
|
|
296
|
+
chips.innerHTML = prompts.map(p => `<button class="chip">${escapeHtml(p)}</button>`).join('');
|
|
297
|
+
chips.querySelectorAll('.chip').forEach((btn, i) => {
|
|
298
|
+
btn.onclick = () => { input.value = prompts[i]; chips.remove(); input.focus(); };
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Footer — POWERED BY GOBI
|
|
303
|
+
|
|
304
|
+
Link back to the vault's public page with `?og=1` so the link preview uses the vault's Open Graph metadata:
|
|
305
|
+
|
|
306
|
+
```html
|
|
307
|
+
<footer>
|
|
308
|
+
<a href="https://www.gobispace.com/@${gobi.vault.slug}?og=1" target="_blank" rel="noopener">
|
|
309
|
+
POWERED BY GOBI
|
|
310
|
+
</a>
|
|
311
|
+
</footer>
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Mobile responsive
|
|
315
|
+
|
|
316
|
+
Single breakpoint at `768px` is enough for most homepages:
|
|
317
|
+
|
|
318
|
+
```css
|
|
319
|
+
@media (max-width: 768px) {
|
|
320
|
+
.hero-grid, .updates-grid { grid-template-columns: 1fr; }
|
|
321
|
+
.hero-content { flex-direction: column; }
|
|
322
|
+
.btn { width: 100%; }
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
168
326
|
---
|
|
169
327
|
|
|
170
328
|
## Complete Example
|