@gobi-ai/cli 1.1.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/init.js +5 -5
- package/dist/commands/space.js +1 -89
- package/dist/main.js +4 -4
- package/package.json +1 -1
- package/skills/gobi-brain/SKILL.md +103 -0
- package/skills/gobi-brain/references/brain.md +163 -0
- package/skills/gobi-core/SKILL.md +5 -4
- 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-homepage/SKILL.md +125 -19
- 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 -141
- package/skills/gobi-space/references/global.md +0 -82
- package/skills/gobi-vault/SKILL.md +0 -100
- package/skills/gobi-vault/references/vault.md +0 -66
|
@@ -8,7 +8,7 @@ description: >-
|
|
|
8
8
|
|
|
9
9
|
# Gobi Homepage Developer Guide
|
|
10
10
|
|
|
11
|
-
A **Gobi Homepage** is a custom HTML page hosted on a vault's webdrive and served as its public homepage at `https://gobispace.com/@{vaultSlug}`. Gobi injects a `window.gobi` bridge before any scripts run, giving the homepage access to vault data, files, and chat.
|
|
11
|
+
A **Gobi Homepage** is a custom HTML page hosted on a vault's webdrive and served as its public homepage at `https://gobispace.com/@{vaultSlug}`. Gobi injects a `window.gobi` bridge before any scripts run, giving the homepage access to vault data, files, brain updates, and chat.
|
|
12
12
|
|
|
13
13
|
> **Sandbox:** The homepage runs in a sandboxed iframe with `origin: null`. Direct `fetch()` / `XMLHttpRequest` calls are blocked by CORS. All data access must go through `window.gobi.*`.
|
|
14
14
|
|
|
@@ -20,7 +20,7 @@ A **Gobi Homepage** is a custom HTML page hosted on a vault's webdrive and serve
|
|
|
20
20
|
```bash
|
|
21
21
|
gobi sync
|
|
22
22
|
```
|
|
23
|
-
2. Set `homepage` in
|
|
23
|
+
2. Set `homepage` in BRAIN.md (homepage property):
|
|
24
24
|
- `homepage: "[[app/home.html]]"` — Gobi sidebars visible alongside the homepage
|
|
25
25
|
- `homepage: "[[app/home.html?nav=false]]"` — full-screen, no Gobi chrome
|
|
26
26
|
|
|
@@ -67,6 +67,31 @@ function getFileUrl(path) {
|
|
|
67
67
|
}
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
+
### Brain Updates
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
const { data: updates, pagination } = await gobi.listBrainUpdates({ limit: 10, cursor: null });
|
|
74
|
+
// updates[i] → {
|
|
75
|
+
// id: 42,
|
|
76
|
+
// title: 'New insights',
|
|
77
|
+
// content: '## ...', // markdown — MAY contain ![[file|width]] wiki image embeds
|
|
78
|
+
// topics: [{ id: 3, name: 'AI', slug: 'ai' }],
|
|
79
|
+
// createdAt: '2025-03-01T12:00:00Z'
|
|
80
|
+
// }
|
|
81
|
+
// pagination → { hasMore: true, nextCursor: 'abc...' }
|
|
82
|
+
|
|
83
|
+
// ⚠️ Always call resolveWikiImages() on content before rendering — see Rendering Markdown below.
|
|
84
|
+
for (const u of updates) {
|
|
85
|
+
el.innerHTML += marked.parse(resolveWikiImages(u.content));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Pagination — load the next page using the cursor
|
|
89
|
+
if (pagination.hasMore) {
|
|
90
|
+
const { data: moreUpdates, pagination: nextPage } =
|
|
91
|
+
await gobi.listBrainUpdates({ limit: 10, cursor: pagination.nextCursor });
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
70
95
|
### Chat (login required)
|
|
71
96
|
|
|
72
97
|
`getSessions`, `loadMessages`, and `sendMessage` require the visitor to be logged in. `getSessions` returns an empty array when not logged in — **but also when logged in with no prior sessions**. Don't use it as a definitive auth check.
|
|
@@ -94,7 +119,7 @@ const { messages, hasMore, nextCursor } = await gobi.loadMessages('sess_abc', {
|
|
|
94
119
|
// sendMessage(sessionId, text, options, onDelta) → Promise<{ content }>
|
|
95
120
|
//
|
|
96
121
|
// options.context tells the AI what the user is looking at:
|
|
97
|
-
// { filePath?: string }
|
|
122
|
+
// { brainUpdateId?: number, brainUpdateTitle?: string, filePath?: string }
|
|
98
123
|
|
|
99
124
|
let reply = '';
|
|
100
125
|
await gobi.sendMessage(sessionId, 'Hello', (delta) => {
|
|
@@ -103,6 +128,10 @@ await gobi.sendMessage(sessionId, 'Hello', (delta) => {
|
|
|
103
128
|
});
|
|
104
129
|
|
|
105
130
|
// With context
|
|
131
|
+
await gobi.sendMessage(sessionId, 'Tell me more', {
|
|
132
|
+
context: { brainUpdateId: 42, brainUpdateTitle: 'New insights' }
|
|
133
|
+
}, (delta) => { reply += delta; renderReply(reply); });
|
|
134
|
+
|
|
106
135
|
await gobi.sendMessage(sessionId, 'Explain this', {
|
|
107
136
|
context: { filePath: 'notes/research.md' }
|
|
108
137
|
}, (delta) => { reply += delta; renderReply(reply); });
|
|
@@ -115,7 +144,7 @@ sessionId = crypto.randomUUID();
|
|
|
115
144
|
|
|
116
145
|
## Rendering Markdown
|
|
117
146
|
|
|
118
|
-
|
|
147
|
+
Brain update `content` and any markdown read via `readFile` may contain Obsidian-style wiki embeds (`![[path|width]]`). Resolve them before passing to a renderer.
|
|
119
148
|
|
|
120
149
|
The examples below use [marked](https://cdn.jsdelivr.net/npm/marked/marked.min.js) — include it in your `<head>`:
|
|
121
150
|
|
|
@@ -133,7 +162,7 @@ function resolveWikiImages(md) {
|
|
|
133
162
|
});
|
|
134
163
|
}
|
|
135
164
|
|
|
136
|
-
const html = marked.parse(resolveWikiImages(
|
|
165
|
+
const html = marked.parse(resolveWikiImages(update.content));
|
|
137
166
|
```
|
|
138
167
|
|
|
139
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:
|
|
@@ -146,7 +175,7 @@ renderer.link = (href, title, text) =>
|
|
|
146
175
|
marked.setOptions({ renderer });
|
|
147
176
|
```
|
|
148
177
|
|
|
149
|
-
**Plain-text previews.** For
|
|
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.
|
|
150
179
|
|
|
151
180
|
---
|
|
152
181
|
|
|
@@ -173,9 +202,58 @@ Centralize colors and spacing in CSS custom properties so restyling is a one-lin
|
|
|
173
202
|
|
|
174
203
|
Pair with Google Fonts (e.g. Space Grotesk for headings, IBM Plex Mono for meta, Inter for body) via CDN `<link>`.
|
|
175
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
|
+
|
|
176
254
|
### Full-screen overlay
|
|
177
255
|
|
|
178
|
-
Useful for expanding any small component into a focused view:
|
|
256
|
+
Useful for expanding the K-Graph (or any small component) into a focused view:
|
|
179
257
|
|
|
180
258
|
```js
|
|
181
259
|
function openOverlay(renderInto) {
|
|
@@ -194,12 +272,27 @@ function openOverlay(renderInto) {
|
|
|
194
272
|
|
|
195
273
|
Always restore `body.overflow` on close, and always remove the `keydown` listener.
|
|
196
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
|
+
|
|
197
290
|
### Chat suggestion chips
|
|
198
291
|
|
|
199
292
|
Empty chat looks dead. Show clickable prompt chips until the first message is sent:
|
|
200
293
|
|
|
201
294
|
```js
|
|
202
|
-
const prompts = ['What is this
|
|
295
|
+
const prompts = ['What is this brain about?', 'Summarize the latest update', 'What topics come up most?'];
|
|
203
296
|
chips.innerHTML = prompts.map(p => `<button class="chip">${escapeHtml(p)}</button>`).join('');
|
|
204
297
|
chips.querySelectorAll('.chip').forEach((btn, i) => {
|
|
205
298
|
btn.onclick = () => { input.value = prompts[i]; chips.remove(); input.focus(); };
|
|
@@ -224,7 +317,7 @@ Single breakpoint at `768px` is enough for most homepages:
|
|
|
224
317
|
|
|
225
318
|
```css
|
|
226
319
|
@media (max-width: 768px) {
|
|
227
|
-
.hero-grid { grid-template-columns: 1fr; }
|
|
320
|
+
.hero-grid, .updates-grid { grid-template-columns: 1fr; }
|
|
228
321
|
.hero-content { flex-direction: column; }
|
|
229
322
|
.btn { width: 100%; }
|
|
230
323
|
}
|
|
@@ -243,9 +336,9 @@ Single breakpoint at `768px` is enough for most homepages:
|
|
|
243
336
|
<style>
|
|
244
337
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
245
338
|
body { font-family: system-ui, sans-serif; max-width: 720px; margin: 0 auto; padding: 24px; }
|
|
246
|
-
.
|
|
247
|
-
.
|
|
248
|
-
.
|
|
339
|
+
.update { margin-bottom: 32px; }
|
|
340
|
+
.update h2 { margin-bottom: 8px; }
|
|
341
|
+
.update img { border-radius: 8px; }
|
|
249
342
|
#chat { margin-top: 40px; border-top: 1px solid #ddd; padding-top: 24px; }
|
|
250
343
|
.message { margin-bottom: 12px; padding: 8px 12px; border-radius: 8px; }
|
|
251
344
|
.message[data-role="human"] { background: #e8f0fe; }
|
|
@@ -258,10 +351,7 @@ Single breakpoint at `768px` is enough for most homepages:
|
|
|
258
351
|
</style>
|
|
259
352
|
</head>
|
|
260
353
|
<body>
|
|
261
|
-
<div
|
|
262
|
-
<h1 id="title"></h1>
|
|
263
|
-
<p id="description"></p>
|
|
264
|
-
</div>
|
|
354
|
+
<div id="updates"></div>
|
|
265
355
|
<div id="chat">
|
|
266
356
|
<div id="messages"></div>
|
|
267
357
|
<div id="chat-input">
|
|
@@ -271,9 +361,7 @@ Single breakpoint at `768px` is enough for most homepages:
|
|
|
271
361
|
</div>
|
|
272
362
|
|
|
273
363
|
<script>
|
|
274
|
-
document.title = gobi.vault.title || '
|
|
275
|
-
document.getElementById('title').textContent = gobi.vault.title || '';
|
|
276
|
-
document.getElementById('description').textContent = gobi.vault.description || '';
|
|
364
|
+
document.title = gobi.vault.title || 'Brain';
|
|
277
365
|
|
|
278
366
|
// ── Helpers ──────────────────────────────────────
|
|
279
367
|
|
|
@@ -301,6 +389,23 @@ Single breakpoint at `768px` is enough for most homepages:
|
|
|
301
389
|
`https://gobispace.com/login?redirect_uri=${encodeURIComponent(window.location.href)}`;
|
|
302
390
|
}
|
|
303
391
|
|
|
392
|
+
// ── Brain updates ────────────────────────────────
|
|
393
|
+
|
|
394
|
+
async function loadUpdates() {
|
|
395
|
+
try {
|
|
396
|
+
const { data: updates } = await gobi.listBrainUpdates({ limit: 5 });
|
|
397
|
+
const el = document.getElementById('updates');
|
|
398
|
+
for (const u of updates) {
|
|
399
|
+
const div = document.createElement('div');
|
|
400
|
+
div.className = 'update';
|
|
401
|
+
div.innerHTML = `<h2>${escapeHtml(u.title)}</h2>${marked.parse(resolveWikiImages(u.content))}`;
|
|
402
|
+
el.appendChild(div);
|
|
403
|
+
}
|
|
404
|
+
} catch (err) {
|
|
405
|
+
console.error('Failed to load brain updates:', err);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
304
409
|
// ── Chat ─────────────────────────────────────────
|
|
305
410
|
|
|
306
411
|
let sessionId = null;
|
|
@@ -361,6 +466,7 @@ Single breakpoint at `768px` is enough for most homepages:
|
|
|
361
466
|
|
|
362
467
|
// ─────────────────────────────────────────────────
|
|
363
468
|
|
|
469
|
+
loadUpdates();
|
|
364
470
|
initChat();
|
|
365
471
|
</script>
|
|
366
472
|
</body>
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gobi-space
|
|
3
3
|
description: >-
|
|
4
|
-
Gobi space commands for community interaction:
|
|
5
|
-
replies, browse
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
spaces. Space and member administration is web-UI only.
|
|
4
|
+
Gobi space commands for community interaction: list/create/edit/delete
|
|
5
|
+
threads and replies, browse topics and topic threads, explore what's
|
|
6
|
+
happening in a space. Use when the user wants to read, write, or manage
|
|
7
|
+
threads and replies in their Gobi community spaces.
|
|
9
8
|
allowed-tools: Bash(gobi:*)
|
|
10
9
|
metadata:
|
|
11
10
|
author: gobi-ai
|
|
12
|
-
version: "0.
|
|
11
|
+
version: "0.8.0"
|
|
13
12
|
---
|
|
14
13
|
|
|
15
14
|
# gobi-space
|
|
16
15
|
|
|
17
|
-
Gobi space commands for community interaction (v0.
|
|
16
|
+
Gobi space commands for community interaction (v0.8.0).
|
|
18
17
|
|
|
19
18
|
Requires gobi-cli installed and authenticated. See gobi-core skill for setup.
|
|
20
19
|
|
|
@@ -41,43 +40,19 @@ For programmatic/agent usage, always pass `--json` as a **global** option (befor
|
|
|
41
40
|
gobi --json space list-threads
|
|
42
41
|
```
|
|
43
42
|
|
|
44
|
-
> Space and member administration (creating spaces, inviting/approving members, joining/leaving) is web-UI only and not available in the CLI.
|
|
45
|
-
|
|
46
43
|
## Available Commands
|
|
47
44
|
|
|
48
|
-
### Space details
|
|
49
|
-
- `gobi space get` — Get details for a space.
|
|
50
|
-
|
|
51
|
-
### Topics
|
|
52
45
|
- `gobi space list-topics` — List topics in a space, ordered by most recent content linkage.
|
|
53
46
|
- `gobi space list-topic-threads` — List threads tagged with a topic in a space (cursor-paginated).
|
|
54
|
-
|
|
55
|
-
### Feed & lineage
|
|
56
|
-
- `gobi space messages` — List the unified message feed (threads and replies, newest first).
|
|
57
|
-
- `gobi space ancestors` — Show the ancestor lineage of a thread or reply (root → immediate parent).
|
|
58
|
-
|
|
59
|
-
### Threads
|
|
60
47
|
- `gobi space get-thread` — Get a thread and its replies (paginated).
|
|
61
48
|
- `gobi space list-threads` — List threads in a space (paginated).
|
|
62
49
|
- `gobi space create-thread` — Create a thread in a space.
|
|
63
50
|
- `gobi space edit-thread` — Edit a thread. You must be the author.
|
|
64
51
|
- `gobi space delete-thread` — Delete a thread. You must be the author.
|
|
65
|
-
|
|
66
|
-
### Replies
|
|
67
52
|
- `gobi space create-reply` — Create a reply to a thread in a space.
|
|
68
53
|
- `gobi space edit-reply` — Edit a reply. You must be the author.
|
|
69
54
|
- `gobi space delete-reply` — Delete a reply. You must be the author.
|
|
70
55
|
|
|
71
|
-
### Global thread space
|
|
72
|
-
The global thread space has no slug and is visible across all spaces.
|
|
73
|
-
|
|
74
|
-
- `gobi global messages` — List the global unified message feed (newest first).
|
|
75
|
-
- `gobi global get-thread` — Get a global thread and its direct replies.
|
|
76
|
-
- `gobi global ancestors` — Show the ancestor lineage of a global thread or reply.
|
|
77
|
-
- `gobi global create-thread` — Create a thread in the global space.
|
|
78
|
-
- `gobi global reply` — Reply to a thread in the global space.
|
|
79
|
-
|
|
80
56
|
## Reference Documentation
|
|
81
57
|
|
|
82
58
|
- [gobi space](references/space.md)
|
|
83
|
-
- [gobi global](references/global.md)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
```
|
|
4
4
|
Usage: gobi space [options] [command]
|
|
5
5
|
|
|
6
|
-
Space commands
|
|
6
|
+
Space commands (threads, replies).
|
|
7
7
|
|
|
8
8
|
Options:
|
|
9
9
|
--space-slug <slug> Space slug (overrides .gobi/settings.yaml)
|
|
@@ -11,12 +11,9 @@ Options:
|
|
|
11
11
|
|
|
12
12
|
Commands:
|
|
13
13
|
list List spaces you are a member of.
|
|
14
|
-
get [spaceSlug] Get details for a space. Pass a slug or omit to use the current space (from .gobi/settings.yaml or --space-slug).
|
|
15
14
|
warp [spaceSlug] Select the active space. Pass a slug to warp directly, or omit for interactive selection.
|
|
16
15
|
list-topics [options] List topics in a space, ordered by most recent content linkage.
|
|
17
16
|
list-topic-threads [options] <topicSlug> List threads tagged with a topic in a space (cursor-paginated).
|
|
18
|
-
messages [options] List the unified message feed (threads and replies, newest first) in a space.
|
|
19
|
-
ancestors <threadId> Show the ancestor lineage of a thread or reply (root → immediate parent).
|
|
20
17
|
get-thread [options] <threadId> Get a thread and its replies (paginated).
|
|
21
18
|
list-threads [options] List threads in a space (paginated).
|
|
22
19
|
create-thread [options] Create a thread in a space.
|
|
@@ -28,17 +25,6 @@ Commands:
|
|
|
28
25
|
help [command] display help for command
|
|
29
26
|
```
|
|
30
27
|
|
|
31
|
-
## get
|
|
32
|
-
|
|
33
|
-
```
|
|
34
|
-
Usage: gobi space get [options] [spaceSlug]
|
|
35
|
-
|
|
36
|
-
Get details for a space. Pass a slug or omit to use the current space (from .gobi/settings.yaml or --space-slug).
|
|
37
|
-
|
|
38
|
-
Options:
|
|
39
|
-
-h, --help display help for command
|
|
40
|
-
```
|
|
41
|
-
|
|
42
28
|
## list-topics
|
|
43
29
|
|
|
44
30
|
```
|
|
@@ -64,30 +50,6 @@ Options:
|
|
|
64
50
|
-h, --help display help for command
|
|
65
51
|
```
|
|
66
52
|
|
|
67
|
-
## messages
|
|
68
|
-
|
|
69
|
-
```
|
|
70
|
-
Usage: gobi space messages [options]
|
|
71
|
-
|
|
72
|
-
List the unified message feed (threads and replies, newest first) in a space.
|
|
73
|
-
|
|
74
|
-
Options:
|
|
75
|
-
--limit <number> Items per page (default: "20")
|
|
76
|
-
--cursor <string> Pagination cursor from previous response
|
|
77
|
-
-h, --help display help for command
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
## ancestors
|
|
81
|
-
|
|
82
|
-
```
|
|
83
|
-
Usage: gobi space ancestors [options] <threadId>
|
|
84
|
-
|
|
85
|
-
Show the ancestor lineage of a thread or reply (root → immediate parent).
|
|
86
|
-
|
|
87
|
-
Options:
|
|
88
|
-
-h, --help display help for command
|
|
89
|
-
```
|
|
90
|
-
|
|
91
53
|
## get-thread
|
|
92
54
|
|
|
93
55
|
```
|
package/dist/commands/global.js
DELETED
|
@@ -1,203 +0,0 @@
|
|
|
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
|
-
}
|