@gobi-ai/cli 0.7.2 → 0.8.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 +4 -3
- package/.claude-plugin/plugin.json +3 -2
- package/dist/commands/media.js +379 -0
- package/dist/main.js +2 -0
- package/package.json +1 -1
- package/skills/{gobi → gobi-cli}/SKILL.md +27 -3
- package/skills/gobi-cli/references/media.md +215 -0
- package/skills/gobi-homepage/SKILL.md +316 -0
- /package/skills/{gobi → gobi-cli}/SKILL.template.md +0 -0
- /package/skills/{gobi → gobi-cli}/references/auth.md +0 -0
- /package/skills/{gobi → gobi-cli}/references/brain.md +0 -0
- /package/skills/{gobi → gobi-cli}/references/init.md +0 -0
- /package/skills/{gobi → gobi-cli}/references/sense.md +0 -0
- /package/skills/{gobi → gobi-cli}/references/session.md +0 -0
- /package/skills/{gobi → gobi-cli}/references/space.md +0 -0
- /package/skills/{gobi → gobi-cli}/references/sync.md +0 -0
- /package/skills/{gobi → gobi-cli}/references/update.md +0 -0
- /package/skills/{gobi → gobi-cli}/scripts/generate-docs.ts +0 -0
|
@@ -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.
|
|
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.
|
|
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-cli",
|
|
20
|
+
"./skills/gobi-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.
|
|
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-cli",
|
|
11
|
+
"./skills/gobi-homepage"
|
|
11
12
|
],
|
|
12
13
|
"commands": "./commands"
|
|
13
14
|
}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { apiGet, apiPost } from "../client.js";
|
|
2
|
+
import { BASE_URL, POLL_MAX_DURATION_MS } from "../constants.js";
|
|
3
|
+
import { getValidToken } from "../auth/manager.js";
|
|
4
|
+
import { ApiError } from "../errors.js";
|
|
5
|
+
import { isJsonMode, jsonOut, unwrapResp } from "./utils.js";
|
|
6
|
+
// ── Polling helper ──
|
|
7
|
+
async function pollStatus(path, terminalStates, intervalMs = 3000) {
|
|
8
|
+
const start = Date.now();
|
|
9
|
+
while (Date.now() - start < POLL_MAX_DURATION_MS) {
|
|
10
|
+
const resp = (await apiGet(path));
|
|
11
|
+
const data = unwrapResp(resp);
|
|
12
|
+
const status = data.status || "";
|
|
13
|
+
if (terminalStates.includes(status))
|
|
14
|
+
return data;
|
|
15
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
16
|
+
}
|
|
17
|
+
throw new Error(`Polling timed out after ${POLL_MAX_DURATION_MS / 1000}s`);
|
|
18
|
+
}
|
|
19
|
+
export function registerMediaCommand(program) {
|
|
20
|
+
const media = program
|
|
21
|
+
.command("media")
|
|
22
|
+
.description("Media generation commands (videos, images).");
|
|
23
|
+
// ════════════════════════════════════════════════════════════════════
|
|
24
|
+
// Upload
|
|
25
|
+
// ════════════════════════════════════════════════════════════════════
|
|
26
|
+
media
|
|
27
|
+
.command("upload-init")
|
|
28
|
+
.description("Get a presigned upload URL for a media file.")
|
|
29
|
+
.requiredOption("--file-name <fileName>", "Name of the file to upload")
|
|
30
|
+
.requiredOption("--content-type <contentType>", "MIME type (e.g. image/png, video/mp4)")
|
|
31
|
+
.option("--file-size <fileSize>", "File size in bytes")
|
|
32
|
+
.action(async (opts) => {
|
|
33
|
+
const body = {
|
|
34
|
+
fileName: opts.fileName,
|
|
35
|
+
contentType: opts.contentType,
|
|
36
|
+
};
|
|
37
|
+
if (opts.fileSize)
|
|
38
|
+
body.fileSize = parseInt(opts.fileSize, 10);
|
|
39
|
+
const resp = (await apiPost("/media-gen/media/initialize", body));
|
|
40
|
+
const data = unwrapResp(resp);
|
|
41
|
+
if (isJsonMode(media)) {
|
|
42
|
+
jsonOut(data);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
console.log(`Upload initialized!\n` +
|
|
46
|
+
` Media ID: ${data.mediaId}\n` +
|
|
47
|
+
` Upload URL: ${data.uploadUrl}\n\n` +
|
|
48
|
+
`PUT your file to the upload URL, then run:\n` +
|
|
49
|
+
` gobi media upload-finalize --media-id ${data.mediaId}`);
|
|
50
|
+
});
|
|
51
|
+
media
|
|
52
|
+
.command("upload-finalize")
|
|
53
|
+
.description("Confirm that a media upload is complete.")
|
|
54
|
+
.requiredOption("--media-id <mediaId>", "Media ID from upload-init")
|
|
55
|
+
.action(async (opts) => {
|
|
56
|
+
const resp = (await apiPost("/media-gen/media/finalize", {
|
|
57
|
+
mediaId: opts.mediaId,
|
|
58
|
+
}));
|
|
59
|
+
const data = unwrapResp(resp);
|
|
60
|
+
if (isJsonMode(media)) {
|
|
61
|
+
jsonOut(data);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
console.log(`Upload finalized for media ${opts.mediaId}.`);
|
|
65
|
+
});
|
|
66
|
+
// ════════════════════════════════════════════════════════════════════
|
|
67
|
+
// Avatars & Voices
|
|
68
|
+
// ════════════════════════════════════════════════════════════════════
|
|
69
|
+
media
|
|
70
|
+
.command("avatars")
|
|
71
|
+
.description("List available avatars.")
|
|
72
|
+
.action(async () => {
|
|
73
|
+
const resp = (await apiGet("/media-gen/avatars"));
|
|
74
|
+
const data = unwrapResp(resp);
|
|
75
|
+
if (isJsonMode(media)) {
|
|
76
|
+
jsonOut(data);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
80
|
+
console.log("No avatars available.");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
console.log("Available avatars:");
|
|
84
|
+
for (const a of data) {
|
|
85
|
+
console.log(` - ${a.id || a.avatarId}: ${a.name || "(unnamed)"}`);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
media
|
|
89
|
+
.command("voices")
|
|
90
|
+
.description("List available voices.")
|
|
91
|
+
.action(async () => {
|
|
92
|
+
const resp = (await apiGet("/media-gen/voices"));
|
|
93
|
+
const data = unwrapResp(resp);
|
|
94
|
+
if (isJsonMode(media)) {
|
|
95
|
+
jsonOut(data);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
99
|
+
console.log("No voices available.");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
console.log("Available voices:");
|
|
103
|
+
for (const v of data) {
|
|
104
|
+
console.log(` - ${v.id || v.voiceId}: ${v.name || "(unnamed)"}`);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
// ════════════════════════════════════════════════════════════════════
|
|
108
|
+
// Videos
|
|
109
|
+
// ════════════════════════════════════════════════════════════════════
|
|
110
|
+
media
|
|
111
|
+
.command("video-create")
|
|
112
|
+
.description("Create an avatar video generation job.")
|
|
113
|
+
.requiredOption("--name <name>", "Name for the video")
|
|
114
|
+
.requiredOption("--avatar-id <avatarId>", "Avatar to use")
|
|
115
|
+
.requiredOption("--voice-id <voiceId>", "Voice to use")
|
|
116
|
+
.requiredOption("--script <script>", "Script for the avatar to read")
|
|
117
|
+
.option("--background-media-id <backgroundMediaId>", "Background media ID (from upload)")
|
|
118
|
+
.option("--wait", "Poll until generation completes")
|
|
119
|
+
.action(async (opts) => {
|
|
120
|
+
const body = {
|
|
121
|
+
name: opts.name,
|
|
122
|
+
avatarId: opts.avatarId,
|
|
123
|
+
voiceId: opts.voiceId,
|
|
124
|
+
script: opts.script,
|
|
125
|
+
};
|
|
126
|
+
if (opts.backgroundMediaId)
|
|
127
|
+
body.backgroundMediaId = opts.backgroundMediaId;
|
|
128
|
+
const resp = (await apiPost("/media-gen/videos", body));
|
|
129
|
+
let data = unwrapResp(resp);
|
|
130
|
+
const videoId = data.id || data.videoId;
|
|
131
|
+
if (opts.wait && videoId) {
|
|
132
|
+
console.log(`Video ${videoId} queued — polling for completion…`);
|
|
133
|
+
data = await pollStatus(`/media-gen/videos/${videoId}/status`, ["inference_complete", "inference_failed"]);
|
|
134
|
+
}
|
|
135
|
+
if (isJsonMode(media)) {
|
|
136
|
+
jsonOut(data);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const status = data.status || "queued";
|
|
140
|
+
console.log(`Video created!\n` +
|
|
141
|
+
` ID: ${videoId}\n` +
|
|
142
|
+
` Status: ${status}`);
|
|
143
|
+
if (status === "inference_complete") {
|
|
144
|
+
console.log(` Download: gobi media video-download ${videoId}`);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
media
|
|
148
|
+
.command("video-list")
|
|
149
|
+
.description("List all videos.")
|
|
150
|
+
.action(async () => {
|
|
151
|
+
const resp = (await apiGet("/media-gen/videos"));
|
|
152
|
+
const data = unwrapResp(resp);
|
|
153
|
+
if (isJsonMode(media)) {
|
|
154
|
+
jsonOut(data);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
158
|
+
console.log("No videos found.");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
console.log("Videos:");
|
|
162
|
+
for (const v of data) {
|
|
163
|
+
console.log(` - [${v.id}] status: ${v.status || "unknown"}, created: ${v.createdAt || "?"}`);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
media
|
|
167
|
+
.command("video-get <id>")
|
|
168
|
+
.description("Get video metadata.")
|
|
169
|
+
.action(async (id) => {
|
|
170
|
+
const resp = (await apiGet(`/media-gen/videos/${id}`));
|
|
171
|
+
const data = unwrapResp(resp);
|
|
172
|
+
if (isJsonMode(media)) {
|
|
173
|
+
jsonOut(data);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
console.log(`Video ${id}:`);
|
|
177
|
+
for (const [k, v] of Object.entries(data)) {
|
|
178
|
+
console.log(` ${k}: ${typeof v === "object" ? JSON.stringify(v) : v}`);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
media
|
|
182
|
+
.command("video-status <id>")
|
|
183
|
+
.description("Poll video generation status.")
|
|
184
|
+
.option("--wait", "Poll until a terminal state is reached")
|
|
185
|
+
.action(async (id, opts) => {
|
|
186
|
+
if (opts.wait) {
|
|
187
|
+
const data = await pollStatus(`/media-gen/videos/${id}/status`, ["inference_complete", "inference_failed"]);
|
|
188
|
+
if (isJsonMode(media)) {
|
|
189
|
+
jsonOut(data);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
console.log(`Video ${id} — status: ${data.status}`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const resp = (await apiGet(`/media-gen/videos/${id}/status`));
|
|
196
|
+
const data = unwrapResp(resp);
|
|
197
|
+
if (isJsonMode(media)) {
|
|
198
|
+
jsonOut(data);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
console.log(`Video ${id} — status: ${data.status || "unknown"}`);
|
|
202
|
+
});
|
|
203
|
+
media
|
|
204
|
+
.command("video-download <id>")
|
|
205
|
+
.description("Get the download URL for a completed video.")
|
|
206
|
+
.action(async (id) => {
|
|
207
|
+
const token = await getValidToken();
|
|
208
|
+
const url = `${BASE_URL}/media-gen/videos/${id}/download`;
|
|
209
|
+
const res = await fetch(url, {
|
|
210
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
211
|
+
redirect: "manual",
|
|
212
|
+
});
|
|
213
|
+
// If the server redirects, extract the Location header
|
|
214
|
+
if (res.status >= 300 && res.status < 400) {
|
|
215
|
+
const location = res.headers.get("location") || "";
|
|
216
|
+
if (isJsonMode(media)) {
|
|
217
|
+
jsonOut({ downloadUrl: location });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
console.log(`Download URL for video ${id}:\n ${location}`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (!res.ok) {
|
|
224
|
+
const text = (await res.text()) || "(no body)";
|
|
225
|
+
throw new ApiError(res.status, `/media-gen/videos/${id}/download`, text);
|
|
226
|
+
}
|
|
227
|
+
// If it returns JSON instead of a redirect
|
|
228
|
+
const resp = (await res.json());
|
|
229
|
+
const data = unwrapResp(resp);
|
|
230
|
+
if (isJsonMode(media)) {
|
|
231
|
+
jsonOut(data);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
console.log(`Download URL for video ${id}:\n ${data.url || data.downloadUrl || JSON.stringify(data)}`);
|
|
235
|
+
});
|
|
236
|
+
// ════════════════════════════════════════════════════════════════════
|
|
237
|
+
// Images
|
|
238
|
+
// ════════════════════════════════════════════════════════════════════
|
|
239
|
+
media
|
|
240
|
+
.command("image-generate")
|
|
241
|
+
.description("Generate an image from a text prompt. Types: image (default), thumbnail (YouTube-optimized), asset (logo/product). Aspect ratios: 1:1, 16:9, 9:16, 4:3, 3:4")
|
|
242
|
+
.requiredOption("--prompt <prompt>", "Text prompt for image generation")
|
|
243
|
+
.requiredOption("--name <name>", "Name for the generated image")
|
|
244
|
+
.option("--type <type>", "Generation type: image (default), thumbnail (YouTube-optimized), asset (logo/product)")
|
|
245
|
+
.option("--aspect-ratio <aspectRatio>", "Aspect ratio (1:1, 16:9, 9:16, 4:3, 3:4)")
|
|
246
|
+
.option("--negative-prompt <negativePrompt>", "Negative prompt")
|
|
247
|
+
.option("--seed <seed>", "Random seed for reproducibility")
|
|
248
|
+
.option("--reference-media-id <referenceMediaId>", "Reference image media ID")
|
|
249
|
+
.option("--wait", "Poll until generation completes")
|
|
250
|
+
.action(async (opts) => {
|
|
251
|
+
const body = {
|
|
252
|
+
prompt: opts.prompt,
|
|
253
|
+
name: opts.name,
|
|
254
|
+
};
|
|
255
|
+
if (opts.type)
|
|
256
|
+
body.type = opts.type;
|
|
257
|
+
if (opts.aspectRatio)
|
|
258
|
+
body.aspectRatio = opts.aspectRatio;
|
|
259
|
+
if (opts.negativePrompt)
|
|
260
|
+
body.negativePrompt = opts.negativePrompt;
|
|
261
|
+
if (opts.seed)
|
|
262
|
+
body.seed = parseInt(opts.seed, 10);
|
|
263
|
+
if (opts.referenceMediaId)
|
|
264
|
+
body.referenceMediaId = opts.referenceMediaId;
|
|
265
|
+
const resp = (await apiPost("/media-gen/images/generate", body));
|
|
266
|
+
let data = unwrapResp(resp);
|
|
267
|
+
const jobId = data.jobId || data.id;
|
|
268
|
+
if (opts.wait && jobId) {
|
|
269
|
+
console.log(`Image job ${jobId} queued — polling for completion…`);
|
|
270
|
+
data = await pollStatus(`/media-gen/images/${jobId}`, [
|
|
271
|
+
"completed",
|
|
272
|
+
"failed",
|
|
273
|
+
"inference_complete",
|
|
274
|
+
"inference_failed",
|
|
275
|
+
]);
|
|
276
|
+
}
|
|
277
|
+
if (isJsonMode(media)) {
|
|
278
|
+
jsonOut(data);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
console.log(`Image generation started!\n` +
|
|
282
|
+
` Job ID: ${jobId}\n` +
|
|
283
|
+
` Status: ${data.status || "queued"}\n` +
|
|
284
|
+
` Check: gobi media image-status ${jobId}`);
|
|
285
|
+
});
|
|
286
|
+
media
|
|
287
|
+
.command("image-edit")
|
|
288
|
+
.description("Edit an existing image with a prompt (image-to-image).")
|
|
289
|
+
.requiredOption("--media-id <mediaId>", "Source image media ID")
|
|
290
|
+
.requiredOption("--prompt <prompt>", "Edit instruction")
|
|
291
|
+
.requiredOption("--name <name>", "Name for the edited image")
|
|
292
|
+
.option("--wait", "Poll until generation completes")
|
|
293
|
+
.action(async (opts) => {
|
|
294
|
+
const resp = (await apiPost("/media-gen/images/edit", {
|
|
295
|
+
mediaId: opts.mediaId,
|
|
296
|
+
prompt: opts.prompt,
|
|
297
|
+
name: opts.name,
|
|
298
|
+
}));
|
|
299
|
+
let data = unwrapResp(resp);
|
|
300
|
+
const jobId = data.jobId || data.id;
|
|
301
|
+
if (opts.wait && jobId) {
|
|
302
|
+
console.log(`Image edit job ${jobId} — polling for completion…`);
|
|
303
|
+
data = await pollStatus(`/media-gen/images/${jobId}`, [
|
|
304
|
+
"completed",
|
|
305
|
+
"failed",
|
|
306
|
+
"inference_complete",
|
|
307
|
+
"inference_failed",
|
|
308
|
+
]);
|
|
309
|
+
}
|
|
310
|
+
if (isJsonMode(media)) {
|
|
311
|
+
jsonOut(data);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
console.log(`Image edit started!\n` +
|
|
315
|
+
` Job ID: ${jobId}\n` +
|
|
316
|
+
` Status: ${data.status || "queued"}`);
|
|
317
|
+
});
|
|
318
|
+
media
|
|
319
|
+
.command("image-inpaint")
|
|
320
|
+
.description("Inpaint an image region using a mask.")
|
|
321
|
+
.requiredOption("--media-id <mediaId>", "Source image media ID")
|
|
322
|
+
.requiredOption("--mask-media-id <maskMediaId>", "Mask image media ID")
|
|
323
|
+
.requiredOption("--prompt <prompt>", "Inpainting prompt")
|
|
324
|
+
.requiredOption("--name <name>", "Name for the inpainted image")
|
|
325
|
+
.option("--wait", "Poll until generation completes")
|
|
326
|
+
.action(async (opts) => {
|
|
327
|
+
const resp = (await apiPost("/media-gen/images/inpaint", {
|
|
328
|
+
mediaId: opts.mediaId,
|
|
329
|
+
maskMediaId: opts.maskMediaId,
|
|
330
|
+
prompt: opts.prompt,
|
|
331
|
+
name: opts.name,
|
|
332
|
+
}));
|
|
333
|
+
let data = unwrapResp(resp);
|
|
334
|
+
const jobId = data.jobId || data.id;
|
|
335
|
+
if (opts.wait && jobId) {
|
|
336
|
+
console.log(`Inpaint job ${jobId} — polling for completion…`);
|
|
337
|
+
data = await pollStatus(`/media-gen/images/${jobId}`, [
|
|
338
|
+
"completed",
|
|
339
|
+
"failed",
|
|
340
|
+
"inference_complete",
|
|
341
|
+
"inference_failed",
|
|
342
|
+
]);
|
|
343
|
+
}
|
|
344
|
+
if (isJsonMode(media)) {
|
|
345
|
+
jsonOut(data);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
console.log(`Inpainting started!\n` +
|
|
349
|
+
` Job ID: ${jobId}\n` +
|
|
350
|
+
` Status: ${data.status || "queued"}`);
|
|
351
|
+
});
|
|
352
|
+
media
|
|
353
|
+
.command("image-status <jobId>")
|
|
354
|
+
.description("Check image generation job status.")
|
|
355
|
+
.option("--wait", "Poll until a terminal state is reached")
|
|
356
|
+
.action(async (jobId, opts) => {
|
|
357
|
+
if (opts.wait) {
|
|
358
|
+
const data = await pollStatus(`/media-gen/images/${jobId}`, [
|
|
359
|
+
"completed",
|
|
360
|
+
"failed",
|
|
361
|
+
"inference_complete",
|
|
362
|
+
"inference_failed",
|
|
363
|
+
]);
|
|
364
|
+
if (isJsonMode(media)) {
|
|
365
|
+
jsonOut(data);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
console.log(`Image job ${jobId} — status: ${data.status}`);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const resp = (await apiGet(`/media-gen/images/${jobId}`));
|
|
372
|
+
const data = unwrapResp(resp);
|
|
373
|
+
if (isJsonMode(media)) {
|
|
374
|
+
jsonOut(data);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
console.log(`Image job ${jobId} — status: ${data.status || "unknown"}`);
|
|
378
|
+
});
|
|
379
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -10,6 +10,7 @@ import { registerSessionsCommand } from "./commands/sessions.js";
|
|
|
10
10
|
import { registerSenseCommand } from "./commands/sense.js";
|
|
11
11
|
import { registerSyncCommand } from "./commands/sync.js";
|
|
12
12
|
import { registerUpdateCommand } from "./commands/update.js";
|
|
13
|
+
import { registerMediaCommand } from "./commands/media.js";
|
|
13
14
|
const require = createRequire(import.meta.url);
|
|
14
15
|
const { version } = require("../package.json");
|
|
15
16
|
const SKIP_BANNER_COMMANDS = new Set(["auth", "init", "update"]);
|
|
@@ -36,6 +37,7 @@ export async function cli() {
|
|
|
36
37
|
registerSenseCommand(program);
|
|
37
38
|
registerSyncCommand(program);
|
|
38
39
|
registerUpdateCommand(program);
|
|
40
|
+
registerMediaCommand(program);
|
|
39
41
|
// Propagate helpWidth to all subcommands
|
|
40
42
|
const helpWidth = process.stdout.columns || 200;
|
|
41
43
|
for (const cmd of program.commands) {
|
package/package.json
CHANGED
|
@@ -6,16 +6,16 @@ description: >-
|
|
|
6
6
|
the outside world — checking what's happening, reading and writing threads,
|
|
7
7
|
and collaborating with others.
|
|
8
8
|
Use when the user wants to interact with Gobi spaces, vaults, brains, threads,
|
|
9
|
-
sessions, or
|
|
9
|
+
sessions, brain updates, or media generation (images, videos, thumbnails).
|
|
10
10
|
allowed-tools: Bash(gobi:*)
|
|
11
11
|
metadata:
|
|
12
12
|
author: gobi-ai
|
|
13
|
-
version: "0.7.
|
|
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.
|
|
18
|
+
A CLI client for the Gobi collaborative knowledge platform (v0.7.3).
|
|
19
19
|
|
|
20
20
|
## Prerequisites
|
|
21
21
|
|
|
@@ -100,6 +100,14 @@ gobi auth status
|
|
|
100
100
|
|
|
101
101
|
`gobi brain` commands manage your vault's brain: search across all spaces, ask brains questions, and publish/unpublish your BRAIN.md. Public brains are accessible at `https://gobispace.com/@{vaultSlug}`.
|
|
102
102
|
|
|
103
|
+
## Gobi Media — Image & Video Generation
|
|
104
|
+
|
|
105
|
+
`gobi media` commands generate images, thumbnails, assets, and avatar videos using AI. All generation is async: create a job, poll for status (or use `--wait`), then retrieve the result.
|
|
106
|
+
|
|
107
|
+
- **Images**: Generate from text prompts, edit existing images, or inpaint with masks. Supports types: `image` (default), `thumbnail` (YouTube-optimized), `asset` (logo/product).
|
|
108
|
+
- **Videos**: Create avatar videos with script narration. Choose an avatar and voice, provide a script, and generate a talking-head video.
|
|
109
|
+
- **Upload**: Upload custom media files (backgrounds, references, masks) via presigned S3 URLs for use in generation.
|
|
110
|
+
|
|
103
111
|
## Gobi Session — Conversations
|
|
104
112
|
|
|
105
113
|
`gobi session` commands manage your conversations: list, read, and reply to sessions.
|
|
@@ -169,6 +177,20 @@ Note: `--space-slug` is not available on other `brain` subcommands or on `sessio
|
|
|
169
177
|
- `gobi session get` — Get a session and its messages (paginated).
|
|
170
178
|
- `gobi session list` — List all sessions you are part of, sorted by most recent activity.
|
|
171
179
|
- `gobi session reply` — Send a human reply to a session you are a member of.
|
|
180
|
+
- `gobi media` — Media generation commands (images, videos).
|
|
181
|
+
- `gobi media upload-init` — Get a presigned upload URL for a media file. Requires `--file-name`, `--content-type`, `--file-size`.
|
|
182
|
+
- `gobi media upload-finalize` — Confirm that a media upload is complete.
|
|
183
|
+
- `gobi media avatars` — List available avatars.
|
|
184
|
+
- `gobi media voices` — List available voices.
|
|
185
|
+
- `gobi media video-create` — Create an avatar video generation job. Requires `--name`, `--avatar-id`, `--voice-id`, `--script`.
|
|
186
|
+
- `gobi media video-list` — List all videos.
|
|
187
|
+
- `gobi media video-get <id>` — Get video metadata.
|
|
188
|
+
- `gobi media video-status <id>` — Poll video generation status. Use `--wait` to block until complete.
|
|
189
|
+
- `gobi media video-download <id>` — Get the download URL for a completed video.
|
|
190
|
+
- `gobi media image-generate` — Generate an image from a text prompt. Requires `--prompt`, `--name`. Optional: `--type` (image|thumbnail|asset), `--aspect-ratio`, `--negative-prompt`, `--seed`, `--reference-media-id`. Use `--wait` to block until complete.
|
|
191
|
+
- `gobi media image-edit` — Edit an existing image with a prompt. Requires `--media-id`, `--prompt`, `--name`.
|
|
192
|
+
- `gobi media image-inpaint` — Inpaint an image region using a mask. Requires `--media-id`, `--mask-media-id`, `--prompt`, `--name`.
|
|
193
|
+
- `gobi media image-status <jobId>` — Check image generation job status. Use `--wait` to block until complete.
|
|
172
194
|
- `gobi sense` — Sense commands (activities, transcriptions).
|
|
173
195
|
- `gobi sense activities` — Fetch activity records within a time range.
|
|
174
196
|
- `gobi sense transcriptions` — Fetch transcription records within a time range.
|
|
@@ -182,6 +204,7 @@ Note: `--space-slug` is not available on other `brain` subcommands or on `sessio
|
|
|
182
204
|
- [gobi space](references/space.md)
|
|
183
205
|
- [gobi brain](references/brain.md)
|
|
184
206
|
- [gobi session](references/session.md)
|
|
207
|
+
- [gobi media](references/media.md)
|
|
185
208
|
- [gobi sense](references/sense.md)
|
|
186
209
|
- [gobi sync](references/sync.md)
|
|
187
210
|
- [gobi update](references/update.md)
|
|
@@ -196,6 +219,7 @@ gobi auth --help
|
|
|
196
219
|
gobi space --help
|
|
197
220
|
gobi brain --help
|
|
198
221
|
gobi session --help
|
|
222
|
+
gobi media --help
|
|
199
223
|
gobi sense --help
|
|
200
224
|
gobi sync --help
|
|
201
225
|
```
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# gobi media
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
Usage: gobi media [options] [command]
|
|
5
|
+
|
|
6
|
+
Media generation commands (videos, images).
|
|
7
|
+
|
|
8
|
+
Options:
|
|
9
|
+
-h, --help display help for command
|
|
10
|
+
|
|
11
|
+
Commands:
|
|
12
|
+
upload-init [options] Get a presigned upload URL for a media file.
|
|
13
|
+
upload-finalize [options] Confirm that a media upload is complete.
|
|
14
|
+
avatars List available avatars.
|
|
15
|
+
voices List available voices.
|
|
16
|
+
video-create [options] Create an avatar video generation job.
|
|
17
|
+
video-list List all videos.
|
|
18
|
+
video-get <id> Get video metadata.
|
|
19
|
+
video-status [options] <id> Poll video generation status.
|
|
20
|
+
video-download <id> Get the download URL for a completed video.
|
|
21
|
+
image-generate [options] Generate an image from a text prompt. Types: image (default), thumbnail (YouTube-optimized), asset (logo/product). Aspect ratios: 1:1, 16:9, 9:16, 4:3, 3:4
|
|
22
|
+
image-edit [options] Edit an existing image with a prompt (image-to-image).
|
|
23
|
+
image-inpaint [options] Inpaint an image region using a mask.
|
|
24
|
+
image-status [options] <jobId> Check image generation job status.
|
|
25
|
+
help [command] display help for command
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Async Workflow
|
|
29
|
+
|
|
30
|
+
Video and image generation are async:
|
|
31
|
+
1. Call the create/generate endpoint — receive a job ID
|
|
32
|
+
2. Poll with `--wait` or manually check status until terminal state
|
|
33
|
+
3. Retrieve or download the result
|
|
34
|
+
|
|
35
|
+
## Media Upload
|
|
36
|
+
|
|
37
|
+
Upload media files (images, videos) for use as backgrounds, references, or masks:
|
|
38
|
+
|
|
39
|
+
1. `gobi media upload-init` — get a presigned S3 upload URL
|
|
40
|
+
2. PUT your file to the returned `uploadUrl`
|
|
41
|
+
3. `gobi media upload-finalize` — confirm the upload
|
|
42
|
+
|
|
43
|
+
The returned `mediaId` can then be used with `--background-media-id`, `--reference-media-id`, `--media-id`, or `--mask-media-id`.
|
|
44
|
+
|
|
45
|
+
## upload-init
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
Usage: gobi media upload-init [options]
|
|
49
|
+
|
|
50
|
+
Get a presigned upload URL for a media file.
|
|
51
|
+
|
|
52
|
+
Options:
|
|
53
|
+
--file-name <fileName> Name of the file to upload
|
|
54
|
+
--content-type <contentType> MIME type (e.g. image/png, video/mp4)
|
|
55
|
+
--file-size <fileSize> File size in bytes
|
|
56
|
+
-h, --help display help for command
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## upload-finalize
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
Usage: gobi media upload-finalize [options]
|
|
63
|
+
|
|
64
|
+
Confirm that a media upload is complete.
|
|
65
|
+
|
|
66
|
+
Options:
|
|
67
|
+
--media-id <mediaId> Media ID from upload-init
|
|
68
|
+
-h, --help display help for command
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## avatars
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
Usage: gobi media avatars [options]
|
|
75
|
+
|
|
76
|
+
List available avatars.
|
|
77
|
+
|
|
78
|
+
Options:
|
|
79
|
+
-h, --help display help for command
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## voices
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
Usage: gobi media voices [options]
|
|
86
|
+
|
|
87
|
+
List available voices.
|
|
88
|
+
|
|
89
|
+
Options:
|
|
90
|
+
-h, --help display help for command
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## video-create
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
Usage: gobi media video-create [options]
|
|
97
|
+
|
|
98
|
+
Create an avatar video generation job.
|
|
99
|
+
|
|
100
|
+
Options:
|
|
101
|
+
--name <name> Name for the video
|
|
102
|
+
--avatar-id <avatarId> Avatar to use
|
|
103
|
+
--voice-id <voiceId> Voice to use
|
|
104
|
+
--script <script> Script for the avatar to read
|
|
105
|
+
--background-media-id <backgroundMediaId> Background media ID (from upload)
|
|
106
|
+
--wait Poll until generation completes
|
|
107
|
+
-h, --help display help for command
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## video-list
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
Usage: gobi media video-list [options]
|
|
114
|
+
|
|
115
|
+
List all videos.
|
|
116
|
+
|
|
117
|
+
Options:
|
|
118
|
+
-h, --help display help for command
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## video-get
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
Usage: gobi media video-get [options] <id>
|
|
125
|
+
|
|
126
|
+
Get video metadata.
|
|
127
|
+
|
|
128
|
+
Options:
|
|
129
|
+
-h, --help display help for command
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## video-status
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
Usage: gobi media video-status [options] <id>
|
|
136
|
+
|
|
137
|
+
Poll video generation status.
|
|
138
|
+
|
|
139
|
+
Options:
|
|
140
|
+
--wait Poll until a terminal state is reached
|
|
141
|
+
-h, --help display help for command
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## video-download
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
Usage: gobi media video-download [options] <id>
|
|
148
|
+
|
|
149
|
+
Get the download URL for a completed video.
|
|
150
|
+
|
|
151
|
+
Options:
|
|
152
|
+
-h, --help display help for command
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## image-generate
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
Usage: gobi media image-generate [options]
|
|
159
|
+
|
|
160
|
+
Generate an image from a text prompt. Types: image (default), thumbnail (YouTube-optimized), asset (logo/product). Aspect ratios: 1:1, 16:9, 9:16, 4:3, 3:4
|
|
161
|
+
|
|
162
|
+
Options:
|
|
163
|
+
--prompt <prompt> Text prompt for image generation
|
|
164
|
+
--name <name> Name for the generated image
|
|
165
|
+
--type <type> Generation type: image (default), thumbnail (YouTube-optimized), asset (logo/product)
|
|
166
|
+
--aspect-ratio <aspectRatio> Aspect ratio (1:1, 16:9, 9:16, 4:3, 3:4)
|
|
167
|
+
--negative-prompt <negativePrompt> Negative prompt
|
|
168
|
+
--seed <seed> Random seed for reproducibility
|
|
169
|
+
--reference-media-id <referenceMediaId> Reference image media ID
|
|
170
|
+
--wait Poll until generation completes
|
|
171
|
+
-h, --help display help for command
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## image-edit
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
Usage: gobi media image-edit [options]
|
|
178
|
+
|
|
179
|
+
Edit an existing image with a prompt (image-to-image).
|
|
180
|
+
|
|
181
|
+
Options:
|
|
182
|
+
--media-id <mediaId> Source image media ID
|
|
183
|
+
--prompt <prompt> Edit instruction
|
|
184
|
+
--name <name> Name for the edited image
|
|
185
|
+
--wait Poll until generation completes
|
|
186
|
+
-h, --help display help for command
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## image-inpaint
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
Usage: gobi media image-inpaint [options]
|
|
193
|
+
|
|
194
|
+
Inpaint an image region using a mask.
|
|
195
|
+
|
|
196
|
+
Options:
|
|
197
|
+
--media-id <mediaId> Source image media ID
|
|
198
|
+
--mask-media-id <maskMediaId> Mask image media ID
|
|
199
|
+
--prompt <prompt> Inpainting prompt
|
|
200
|
+
--name <name> Name for the inpainted image
|
|
201
|
+
--wait Poll until generation completes
|
|
202
|
+
-h, --help display help for command
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## image-status
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
Usage: gobi media image-status [options] <jobId>
|
|
209
|
+
|
|
210
|
+
Check image generation job status.
|
|
211
|
+
|
|
212
|
+
Options:
|
|
213
|
+
--wait Poll until a terminal state is reached
|
|
214
|
+
-h, --help display help for command
|
|
215
|
+
```
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gobi-homepage
|
|
3
|
+
description: >-
|
|
4
|
+
Developer reference for building Gobi Homepages — 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.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Gobi Homepage Developer Guide
|
|
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, brain updates, and chat.
|
|
12
|
+
|
|
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
|
+
|
|
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 `homepage` in BRAIN.md (homepage property):
|
|
24
|
+
- `homepage: "[[app/home.html]]"` — Gobi sidebars visible alongside the homepage
|
|
25
|
+
- `homepage: "[[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
|
+
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|