@gobi-ai/cli 0.7.1 → 0.7.3

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,19 +4,20 @@
4
4
  "name": "gobi-ai"
5
5
  },
6
6
  "description": "Claude Code plugin for the Gobi collaborative knowledge platform CLI",
7
- "version": "0.7.0",
7
+ "version": "0.7.3",
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.",
12
- "version": "0.7.0",
12
+ "version": "0.7.3",
13
13
  "author": {
14
14
  "name": "gobi-ai"
15
15
  },
16
16
  "homepage": "https://github.com/gobi-ai/gobi-cli",
17
17
  "source": "./",
18
18
  "skills": [
19
- "./skills/gobi"
19
+ "./skills/gobi",
20
+ "./skills/gobi-dev-homepage"
20
21
  ],
21
22
  "commands": "./commands"
22
23
  }
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "gobi",
3
3
  "description": "Manage the Gobi collaborative knowledge platform from the command line",
4
- "version": "0.7.0",
4
+ "version": "0.7.3",
5
5
  "author": {
6
6
  "name": "gobi-ai"
7
7
  },
8
8
  "homepage": "https://github.com/gobi-ai/gobi-cli",
9
9
  "skills": [
10
- "./skills/gobi"
10
+ "./skills/gobi",
11
+ "./skills/gobi-dev-homepage"
11
12
  ],
12
13
  "commands": "./commands"
13
14
  }
@@ -422,9 +422,9 @@ export async function runSync(opts) {
422
422
  for (const cachedPath of Object.keys(state.hashCache)) {
423
423
  if (!localPathSet.has(cachedPath) && !existsSync(join(vaultDir, cachedPath))) {
424
424
  // File was in our cache but no longer on disk — deleted offline.
425
- // In download-only mode, skip the DELETE: the server's client_deleted_paths
426
- // mechanism will re-download the file instead (server-side download_only path).
427
- if (opts.downloadOnly)
425
+ // In download-only or upload-only mode, skip the DELETE:
426
+ // upload-only should never remove files from the server.
427
+ if (opts.downloadOnly || opts.uploadOnly)
428
428
  continue;
429
429
  if (!opts.jsonMode)
430
430
  console.log(` Deleting remote (offline deletion): ${cachedPath}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobi-ai/cli",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "CLI client for the Gobi collaborative knowledge platform",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -10,12 +10,12 @@ description: >-
10
10
  allowed-tools: Bash(gobi:*)
11
11
  metadata:
12
12
  author: gobi-ai
13
- version: "0.7.0"
13
+ version: "0.7.3"
14
14
  ---
15
15
 
16
16
  # gobi-cli
17
17
 
18
- A CLI client for the Gobi collaborative knowledge platform (v0.7.0).
18
+ A CLI client for the Gobi collaborative knowledge platform (v0.7.3).
19
19
 
20
20
  ## Prerequisites
21
21
 
@@ -0,0 +1,316 @@
1
+ ---
2
+ name: gobi-homepage
3
+ description: >-
4
+ Developer reference for building Gobi Applets — custom HTML pages hosted on
5
+ webdrive and served as a vault's public homepage at gobispace.com/@{vaultSlug}.
6
+ Use when a developer wants to build or modify a vault homepage applet.
7
+ ---
8
+
9
+ # Gobi Applet Developer Guide
10
+
11
+ A **Gobi Applet** 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 applet access to vault data, files, brain updates, and chat.
12
+
13
+ > **Sandbox:** The applet 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
+
15
+ ---
16
+
17
+ ## Setup
18
+
19
+ 1. Create an HTML file in the vault (e.g. `app/home.html`) and upload:
20
+ ```bash
21
+ gobi sync
22
+ ```
23
+ 2. Set `homepagePath` in vault settings:
24
+ - `app/home.html` — Gobi sidebars visible alongside the applet
25
+ - `app/home.html?nav=false` — full-screen, no Gobi chrome
26
+
27
+ ---
28
+
29
+ ## window.gobi Reference
30
+
31
+ `gobi.vault` is **synchronous** — available at the top of any `<script>`, no `DOMContentLoaded` needed. All other methods return `Promise`.
32
+
33
+ ### gobi.vault
34
+
35
+ ```js
36
+ const { vaultId, title, description, thumbnailPath, tags,
37
+ ownerName, ownerProfilePictureUrl, webdriveUrl } = gobi.vault;
38
+
39
+ // Profile picture: https://d16t3dioqz0xo9.cloudfront.net/{thumbnailPath}@{W}x{H}.webp
40
+ ```
41
+
42
+ ### Error Handling
43
+
44
+ All async `gobi.*` methods throw on failure. Wrap calls in `try/catch` to handle errors gracefully.
45
+
46
+ ```js
47
+ try {
48
+ const text = await gobi.readFile('data/config.json');
49
+ } catch (err) {
50
+ console.error('gobi API error:', err.message);
51
+ // err.message contains a human-readable description of what went wrong
52
+ }
53
+ ```
54
+
55
+ ### Files
56
+
57
+ ```js
58
+ const text = await gobi.readFile('data/config.json'); // → string (throws if not found)
59
+ const items = await gobi.listFiles('images'); // → [{ name, type: 'file'|'folder' }]
60
+ const exists = await gobi.fileExists('README.md'); // → boolean
61
+
62
+ // Direct URL for any vault file (public, no auth required).
63
+ // Encode each segment separately — encodeURIComponent(path) would encode slashes and break the URL.
64
+ function getFileUrl(path) {
65
+ const enc = path.split('/').map(encodeURIComponent).join('/');
66
+ return `${gobi.vault.webdriveUrl}/api/v1/file/raw/${gobi.vault.vaultId}/${enc}`;
67
+ }
68
+ ```
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
+
95
+ ### Chat (login required)
96
+
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.
98
+
99
+ ```js
100
+ // Redirect to login, returning here after. Use window.top — the applet is inside an iframe.
101
+ function redirectToLogin() {
102
+ window.top.location.href =
103
+ `https://gobispace.com/login?redirect_uri=${encodeURIComponent(window.location.href)}`;
104
+ }
105
+
106
+ // Session list — newest first
107
+ const { data: sessions, pagination } = await gobi.getSessions({ limit: 20, cursor: null });
108
+ // sessions[i] → { sessionId: 'sess_abc', messageCount: 12, lastMessageAt: '2025-03-30T...' }
109
+
110
+ // Message history
111
+ const { messages, hasMore, nextCursor } = await gobi.loadMessages('sess_abc', { limit: 20, cursor: null });
112
+ // messages[i] → { id, role: 'human'|'assistant', content, createdAt }
113
+
114
+ // Send a message and stream the response.
115
+ // New session: pass crypto.randomUUID() — the backend creates it lazily on first message sent.
116
+ //
117
+ // Signatures:
118
+ // sendMessage(sessionId, text, onDelta) → Promise<{ content }>
119
+ // sendMessage(sessionId, text, options, onDelta) → Promise<{ content }>
120
+ //
121
+ // options.context tells the AI what the user is looking at:
122
+ // { brainUpdateId?: number, brainUpdateTitle?: string, filePath?: string }
123
+
124
+ let reply = '';
125
+ await gobi.sendMessage(sessionId, 'Hello', (delta) => {
126
+ reply += delta;
127
+ renderReply(reply);
128
+ });
129
+
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
+
135
+ await gobi.sendMessage(sessionId, 'Explain this', {
136
+ context: { filePath: 'notes/research.md' }
137
+ }, (delta) => { reply += delta; renderReply(reply); });
138
+
139
+ // Start a fresh session
140
+ sessionId = crypto.randomUUID();
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Rendering Markdown
146
+
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.
148
+
149
+ The examples below use [marked](https://cdn.jsdelivr.net/npm/marked/marked.min.js) — include it in your `<head>`:
150
+
151
+ ```html
152
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
153
+ ```
154
+
155
+ ```js
156
+ // Define once, reuses getFileUrl from the Files section.
157
+ function resolveWikiImages(md) {
158
+ return md.replace(/!\[\[([^\]|]+)(?:\|(\d+))?\]\]/g, (_, p, w) => {
159
+ const url = getFileUrl(p.trim());
160
+ return w ? `<img src="${url}" width="${w}" style="max-width:100%">`
161
+ : `<img src="${url}" style="max-width:100%">`;
162
+ });
163
+ }
164
+
165
+ const html = marked.parse(resolveWikiImages(update.content));
166
+ ```
167
+
168
+ ---
169
+
170
+ ## Complete Example
171
+
172
+ ```html
173
+ <!DOCTYPE html>
174
+ <html>
175
+ <head>
176
+ <meta charset="UTF-8">
177
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
178
+ <style>
179
+ * { box-sizing: border-box; margin: 0; padding: 0; }
180
+ body { font-family: system-ui, sans-serif; max-width: 720px; margin: 0 auto; padding: 24px; }
181
+ .update { margin-bottom: 32px; }
182
+ .update h2 { margin-bottom: 8px; }
183
+ .update img { border-radius: 8px; }
184
+ #chat { margin-top: 40px; border-top: 1px solid #ddd; padding-top: 24px; }
185
+ .message { margin-bottom: 12px; padding: 8px 12px; border-radius: 8px; }
186
+ .message[data-role="human"] { background: #e8f0fe; }
187
+ .message[data-role="assistant"] { background: #f1f3f4; }
188
+ #chat-input { display: flex; gap: 8px; margin-top: 16px; }
189
+ #chat-input input { flex: 1; padding: 8px; border: 1px solid #ccc; border-radius: 6px; }
190
+ #chat-input button { padding: 8px 16px; border: none; border-radius: 6px; background: #1a73e8; color: #fff; cursor: pointer; }
191
+ .login-prompt { text-align: center; color: #666; }
192
+ .login-prompt a { color: #1a73e8; cursor: pointer; text-decoration: underline; }
193
+ </style>
194
+ </head>
195
+ <body>
196
+ <div id="updates"></div>
197
+ <div id="chat">
198
+ <div id="messages"></div>
199
+ <div id="chat-input">
200
+ <input id="input" type="text" placeholder="Ask a question…" />
201
+ <button onclick="onSend()">Send</button>
202
+ </div>
203
+ </div>
204
+
205
+ <script>
206
+ document.title = gobi.vault.title || 'Brain';
207
+
208
+ // ── Helpers ──────────────────────────────────────
209
+
210
+ function getFileUrl(path) {
211
+ const enc = path.split('/').map(encodeURIComponent).join('/');
212
+ return `${gobi.vault.webdriveUrl}/api/v1/file/raw/${gobi.vault.vaultId}/${enc}`;
213
+ }
214
+
215
+ function resolveWikiImages(md) {
216
+ return md.replace(/!\[\[([^\]|]+)(?:\|(\d+))?\]\]/g, (_, p, w) => {
217
+ const url = getFileUrl(p.trim());
218
+ return w ? `<img src="${url}" width="${w}" style="max-width:100%">`
219
+ : `<img src="${url}" style="max-width:100%">`;
220
+ });
221
+ }
222
+
223
+ function escapeHtml(s) {
224
+ const el = document.createElement('div');
225
+ el.textContent = s;
226
+ return el.innerHTML;
227
+ }
228
+
229
+ function redirectToLogin() {
230
+ window.top.location.href =
231
+ `https://gobispace.com/login?redirect_uri=${encodeURIComponent(window.location.href)}`;
232
+ }
233
+
234
+ // ── Brain updates ────────────────────────────────
235
+
236
+ async function loadUpdates() {
237
+ try {
238
+ const { data: updates } = await gobi.listBrainUpdates({ limit: 5 });
239
+ const el = document.getElementById('updates');
240
+ for (const u of updates) {
241
+ const div = document.createElement('div');
242
+ div.className = 'update';
243
+ div.innerHTML = `<h2>${escapeHtml(u.title)}</h2>${marked.parse(resolveWikiImages(u.content))}`;
244
+ el.appendChild(div);
245
+ }
246
+ } catch (err) {
247
+ console.error('Failed to load brain updates:', err);
248
+ }
249
+ }
250
+
251
+ // ── Chat ─────────────────────────────────────────
252
+
253
+ let sessionId = null;
254
+ const messagesEl = document.getElementById('messages');
255
+
256
+ function renderMessage(role, html) {
257
+ const div = document.createElement('div');
258
+ div.className = 'message';
259
+ div.dataset.role = role;
260
+ div.innerHTML = html;
261
+ messagesEl.appendChild(div);
262
+ return div;
263
+ }
264
+
265
+ async function initChat() {
266
+ try {
267
+ const { data: sessions } = await gobi.getSessions({ limit: 1 });
268
+ if (!sessions.length) {
269
+ messagesEl.innerHTML =
270
+ '<p class="login-prompt">No chat sessions yet. <a onclick="redirectToLogin()">Log in</a> to start chatting.</p>';
271
+ return;
272
+ }
273
+ sessionId = sessions[0].sessionId;
274
+ const { messages } = await gobi.loadMessages(sessionId, { limit: 20 });
275
+ for (const m of messages) {
276
+ renderMessage(m.role, m.role === 'assistant' ? marked.parse(m.content) : escapeHtml(m.content));
277
+ }
278
+ } catch (err) {
279
+ console.error('Failed to init chat:', err);
280
+ }
281
+ }
282
+
283
+ async function onSend() {
284
+ const input = document.getElementById('input');
285
+ const text = input.value.trim();
286
+ if (!text) return;
287
+ input.value = '';
288
+
289
+ if (!sessionId) sessionId = crypto.randomUUID();
290
+ renderMessage('human', escapeHtml(text));
291
+
292
+ const replyEl = renderMessage('assistant', '…');
293
+ let reply = '';
294
+ try {
295
+ await gobi.sendMessage(sessionId, text, (delta) => {
296
+ reply += delta;
297
+ replyEl.innerHTML = marked.parse(reply);
298
+ });
299
+ } catch (err) {
300
+ replyEl.textContent = 'Error: ' + err.message;
301
+ }
302
+ }
303
+
304
+ // Submit on Enter
305
+ document.getElementById('input').addEventListener('keydown', (e) => {
306
+ if (e.key === 'Enter') onSend();
307
+ });
308
+
309
+ // ─────────────────────────────────────────────────
310
+
311
+ loadUpdates();
312
+ initChat();
313
+ </script>
314
+ </body>
315
+ </html>
316
+ ```