@gobi-ai/cli 0.9.5 → 0.9.7

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.4",
7
+ "version": "0.9.7",
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.4",
12
+ "version": "0.9.7",
13
13
  "author": {
14
14
  "name": "gobi-ai"
15
15
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gobi",
3
3
  "description": "Manage the Gobi collaborative knowledge platform from the command line",
4
- "version": "0.9.4",
4
+ "version": "0.9.7",
5
5
  "author": {
6
6
  "name": "gobi-ai"
7
7
  },
@@ -116,15 +116,18 @@ export function registerMediaCommand(program) {
116
116
  media
117
117
  .command("video-create")
118
118
  .description("Create an avatar video generation job.")
119
- .requiredOption("--name <name>", "Name for the video")
119
+ .option("--name <name>", "Name for the video (auto-generated if omitted)")
120
120
  .requiredOption("--avatar-id <avatarId>", "Avatar to use")
121
121
  .requiredOption("--voice-id <voiceId>", "Voice to use")
122
122
  .requiredOption("--script <script>", "Script for the avatar to read")
123
123
  .option("--background-media-id <backgroundMediaId>", "Background media ID (from upload)")
124
124
  .option("--wait", "Poll until generation completes")
125
+ .option("-o, --output <path>", "Download video to this path when done (implies --wait)")
125
126
  .action(async (opts) => {
127
+ const shouldWait = opts.wait || !!opts.output;
128
+ const autoName = opts.name || `video-${new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)}`;
126
129
  const body = {
127
- name: opts.name,
130
+ name: autoName,
128
131
  avatarId: opts.avatarId,
129
132
  voiceId: opts.voiceId,
130
133
  script: opts.script,
@@ -134,10 +137,56 @@ export function registerMediaCommand(program) {
134
137
  const resp = (await apiPost("/media-gen/videos", body));
135
138
  let data = unwrapResp(resp);
136
139
  const videoId = data.id || data.videoId;
137
- if (opts.wait && videoId) {
140
+ if (shouldWait && videoId) {
138
141
  console.log(`Video ${videoId} queued — polling for completion…`);
139
142
  data = await pollStatus(`/media-gen/videos/${videoId}/status`, ["inference_complete", "inference_failed"]);
140
143
  }
144
+ // Download video to file if -o specified
145
+ if (opts.output && videoId && data.status === "inference_complete") {
146
+ const token = await getValidToken();
147
+ const dlUrl = `${BASE_URL}/media-gen/videos/${videoId}/download`;
148
+ const dlRes = await fetch(dlUrl, {
149
+ headers: { Authorization: `Bearer ${token}` },
150
+ redirect: "follow",
151
+ });
152
+ if (dlRes.ok) {
153
+ const { writeFile, mkdir } = await import("fs/promises");
154
+ const { dirname } = await import("path");
155
+ const buffer = Buffer.from(await dlRes.arrayBuffer());
156
+ await mkdir(dirname(opts.output), { recursive: true });
157
+ await writeFile(opts.output, buffer);
158
+ const contentType = dlRes.headers.get("content-type") || "video/mp4";
159
+ if (isJsonMode(media)) {
160
+ jsonOut({ ...data, filename: opts.output, contentType, size: buffer.length });
161
+ return;
162
+ }
163
+ console.log(`Video saved to ${opts.output} (${buffer.length} bytes)`);
164
+ return;
165
+ }
166
+ // If direct download fails, try getting the URL and fetching that
167
+ const dlRes2 = await fetch(dlUrl, {
168
+ headers: { Authorization: `Bearer ${token}` },
169
+ redirect: "manual",
170
+ });
171
+ const location = dlRes2.headers.get("location");
172
+ if (location) {
173
+ const videoRes = await fetch(location);
174
+ if (videoRes.ok) {
175
+ const { writeFile, mkdir } = await import("fs/promises");
176
+ const { dirname } = await import("path");
177
+ const buffer = Buffer.from(await videoRes.arrayBuffer());
178
+ await mkdir(dirname(opts.output), { recursive: true });
179
+ await writeFile(opts.output, buffer);
180
+ const contentType = videoRes.headers.get("content-type") || "video/mp4";
181
+ if (isJsonMode(media)) {
182
+ jsonOut({ ...data, filename: opts.output, contentType, size: buffer.length });
183
+ return;
184
+ }
185
+ console.log(`Video saved to ${opts.output} (${buffer.length} bytes)`);
186
+ return;
187
+ }
188
+ }
189
+ }
141
190
  if (isJsonMode(media)) {
142
191
  jsonOut(data);
143
192
  return;
@@ -188,9 +237,57 @@ export function registerMediaCommand(program) {
188
237
  .command("video-status <id>")
189
238
  .description("Poll video generation status.")
190
239
  .option("--wait", "Poll until a terminal state is reached")
240
+ .option("-o, --output <path>", "Download video to this path when complete (implies --wait)")
191
241
  .action(async (id, opts) => {
192
- if (opts.wait) {
242
+ const shouldWait = opts.wait || !!opts.output;
243
+ if (shouldWait) {
193
244
  const data = await pollStatus(`/media-gen/videos/${id}/status`, ["inference_complete", "inference_failed"]);
245
+ // Download if -o specified and completed
246
+ if (opts.output && data.status === "inference_complete") {
247
+ const token = await getValidToken();
248
+ const dlUrl = `${BASE_URL}/media-gen/videos/${id}/download`;
249
+ const dlRes = await fetch(dlUrl, {
250
+ headers: { Authorization: `Bearer ${token}` },
251
+ redirect: "follow",
252
+ });
253
+ if (dlRes.ok) {
254
+ const { writeFile, mkdir } = await import("fs/promises");
255
+ const { dirname } = await import("path");
256
+ const buffer = Buffer.from(await dlRes.arrayBuffer());
257
+ await mkdir(dirname(opts.output), { recursive: true });
258
+ await writeFile(opts.output, buffer);
259
+ const contentType = dlRes.headers.get("content-type") || "video/mp4";
260
+ if (isJsonMode(media)) {
261
+ jsonOut({ ...data, filename: opts.output, contentType, size: buffer.length });
262
+ return;
263
+ }
264
+ console.log(`Video ${id} — ${data.status}\nSaved to ${opts.output} (${buffer.length} bytes)`);
265
+ return;
266
+ }
267
+ // Try manual redirect
268
+ const dlRes2 = await fetch(dlUrl, {
269
+ headers: { Authorization: `Bearer ${token}` },
270
+ redirect: "manual",
271
+ });
272
+ const location = dlRes2.headers.get("location");
273
+ if (location) {
274
+ const videoRes = await fetch(location);
275
+ if (videoRes.ok) {
276
+ const { writeFile, mkdir } = await import("fs/promises");
277
+ const { dirname } = await import("path");
278
+ const buffer = Buffer.from(await videoRes.arrayBuffer());
279
+ await mkdir(dirname(opts.output), { recursive: true });
280
+ await writeFile(opts.output, buffer);
281
+ const contentType = videoRes.headers.get("content-type") || "video/mp4";
282
+ if (isJsonMode(media)) {
283
+ jsonOut({ ...data, filename: opts.output, contentType, size: buffer.length });
284
+ return;
285
+ }
286
+ console.log(`Video ${id} — ${data.status}\nSaved to ${opts.output} (${buffer.length} bytes)`);
287
+ return;
288
+ }
289
+ }
290
+ }
194
291
  if (isJsonMode(media)) {
195
292
  jsonOut(data);
196
293
  return;
@@ -208,10 +305,57 @@ export function registerMediaCommand(program) {
208
305
  });
209
306
  media
210
307
  .command("video-download <id>")
211
- .description("Get the download URL for a completed video.")
212
- .action(async (id) => {
308
+ .description("Download a completed video (or get its URL).")
309
+ .option("-o, --output <path>", "Save video to this file path")
310
+ .action(async (id, opts) => {
213
311
  const token = await getValidToken();
214
312
  const url = `${BASE_URL}/media-gen/videos/${id}/download`;
313
+ // If -o specified, download directly to file
314
+ if (opts.output) {
315
+ const res = await fetch(url, {
316
+ headers: { Authorization: `Bearer ${token}` },
317
+ redirect: "follow",
318
+ });
319
+ if (res.ok) {
320
+ const { writeFile, mkdir } = await import("fs/promises");
321
+ const { dirname } = await import("path");
322
+ const buffer = Buffer.from(await res.arrayBuffer());
323
+ await mkdir(dirname(opts.output), { recursive: true });
324
+ await writeFile(opts.output, buffer);
325
+ const contentType = res.headers.get("content-type") || "video/mp4";
326
+ if (isJsonMode(media)) {
327
+ jsonOut({ filename: opts.output, contentType, size: buffer.length });
328
+ return;
329
+ }
330
+ console.log(`Video saved to ${opts.output} (${buffer.length} bytes)`);
331
+ return;
332
+ }
333
+ // If direct follow didn't work, try manual redirect
334
+ const res2 = await fetch(url, {
335
+ headers: { Authorization: `Bearer ${token}` },
336
+ redirect: "manual",
337
+ });
338
+ const location = res2.headers.get("location");
339
+ if (location) {
340
+ const videoRes = await fetch(location);
341
+ if (videoRes.ok) {
342
+ const { writeFile, mkdir } = await import("fs/promises");
343
+ const { dirname } = await import("path");
344
+ const buffer = Buffer.from(await videoRes.arrayBuffer());
345
+ await mkdir(dirname(opts.output), { recursive: true });
346
+ await writeFile(opts.output, buffer);
347
+ const contentType = videoRes.headers.get("content-type") || "video/mp4";
348
+ if (isJsonMode(media)) {
349
+ jsonOut({ filename: opts.output, contentType, size: buffer.length });
350
+ return;
351
+ }
352
+ console.log(`Video saved to ${opts.output} (${buffer.length} bytes)`);
353
+ return;
354
+ }
355
+ }
356
+ throw new ApiError(res.status, `/media-gen/videos/${id}/download`, "Failed to download video");
357
+ }
358
+ // No -o: just return the URL (existing behavior)
215
359
  const res = await fetch(url, {
216
360
  headers: { Authorization: `Bearer ${token}` },
217
361
  redirect: "manual",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobi-ai/cli",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
4
4
  "description": "CLI client for the Gobi collaborative knowledge platform",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -52,6 +52,40 @@ Do NOT use markdown image syntax `![](...)` or `gobi://` URLs. Always use `![[me
52
52
  - `image-download` takes a **positional** jobId (NOT `--job-id`): `gobi media image-download <jobId>`
53
53
  - The `jobId` (or `id`) field is what you pass to `image-download` / `image-status` — NOT `mediaId`.
54
54
 
55
+ ## Typical Workflow (Video Generation)
56
+
57
+ Single command — create and download in one step:
58
+
59
+ ```bash
60
+ gobi --json media video-create --avatar-id "<AVATAR_ID>" --voice-id "<VOICE_ID>" --script "<SCRIPT>" -o media/<NAME>.mp4
61
+ ```
62
+
63
+ `--name` is optional (auto-generated if omitted). Replace `<NAME>` with a short descriptive slug. Use `gobi media avatars` and `gobi media voices` to list available IDs.
64
+
65
+ The `-o` flag implies `--wait` and downloads the video when done.
66
+
67
+ **IMPORTANT: Avatars are pre-built system avatars ONLY.** You CANNOT create custom avatars from uploaded images. The `gobi media avatars` list is the complete set of available avatars. Do NOT attempt to upload an image and use its mediaId as an avatarId — it will fail.
68
+
69
+ To use a custom image (e.g. a generated image) as the **background** of a video, upload it first via `upload-init` / `upload-finalize`, then pass the mediaId as `--background-media-id`:
70
+
71
+ ```bash
72
+ # 1. Upload custom image as background
73
+ gobi --json media upload-init --file-name "bg.png" --content-type "image/png" --file-size <SIZE>
74
+ curl -T "media/bg.png" -H "Content-Type: image/png" "<UPLOAD_URL>"
75
+ gobi --json media upload-finalize --media-id "<MEDIA_ID>"
76
+
77
+ # 2. Create video with pre-built avatar + custom background
78
+ gobi --json media video-create --avatar-id "<AVATAR_ID>" --voice-id "<VOICE_ID>" --script "<SCRIPT>" --background-media-id "<MEDIA_ID>" -o media/<NAME>.mp4
79
+ ```
80
+
81
+ **IMPORTANT: After downloading, show the video using Obsidian wiki-link syntax EXACTLY like this:**
82
+
83
+ ```
84
+ ![[media/<NAME>.mp4]]
85
+ ```
86
+
87
+ Do NOT use markdown image/link syntax `![](...)` or `gobi://` URLs. Always use `![[media/<NAME>.mp4]]`.
88
+
55
89
  ## Available Commands
56
90
 
57
91
  ### Upload
@@ -70,7 +104,7 @@ Do NOT use markdown image syntax `![](...)` or `gobi://` URLs. Always use `![[me
70
104
  - `gobi media video-list` — List all videos.
71
105
  - `gobi media video-get` — Get video metadata.
72
106
  - `gobi media video-status` — Poll video generation status.
73
- - `gobi media video-download` — Get the download URL for a completed video.
107
+ - `gobi media video-download` — Download a completed video (`-o` to save to file).
74
108
 
75
109
  ### Images
76
110
 
@@ -17,7 +17,7 @@ Commands:
17
17
  video-list List all videos.
18
18
  video-get <id> Get video metadata.
19
19
  video-status [options] <id> Poll video generation status.
20
- video-download <id> Get the download URL for a completed video.
20
+ video-download [options] <id> Download a completed video (or get its URL).
21
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
22
  image-edit [options] Edit an existing image with a prompt (image-to-image).
23
23
  image-inpaint [options] Inpaint an image region using a mask.
@@ -82,12 +82,13 @@ Usage: gobi media video-create [options]
82
82
  Create an avatar video generation job.
83
83
 
84
84
  Options:
85
- --name <name> Name for the video
85
+ --name <name> Name for the video (auto-generated if omitted)
86
86
  --avatar-id <avatarId> Avatar to use
87
87
  --voice-id <voiceId> Voice to use
88
88
  --script <script> Script for the avatar to read
89
89
  --background-media-id <backgroundMediaId> Background media ID (from upload)
90
90
  --wait Poll until generation completes
91
+ -o, --output <path> Download video to this path when done (implies --wait)
91
92
  -h, --help display help for command
92
93
  ```
93
94
 
@@ -121,8 +122,9 @@ Usage: gobi media video-status [options] <id>
121
122
  Poll video generation status.
122
123
 
123
124
  Options:
124
- --wait Poll until a terminal state is reached
125
- -h, --help display help for command
125
+ --wait Poll until a terminal state is reached
126
+ -o, --output <path> Download video to this path when complete (implies --wait)
127
+ -h, --help display help for command
126
128
  ```
127
129
 
128
130
  ## video-download
@@ -130,10 +132,11 @@ Options:
130
132
  ```
131
133
  Usage: gobi media video-download [options] <id>
132
134
 
133
- Get the download URL for a completed video.
135
+ Download a completed video (or get its URL).
134
136
 
135
137
  Options:
136
- -h, --help display help for command
138
+ -o, --output <path> Save video to this file path
139
+ -h, --help display help for command
137
140
  ```
138
141
 
139
142
  ## image-generate