@wji.ai/cli 0.7.0 → 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.
Files changed (49) hide show
  1. package/README.md +4 -1
  2. package/dist/index.js +7162 -76
  3. package/package.json +6 -9
  4. package/skill/short-drama/SKILL.md +22 -10
  5. package/skill/short-drama/reference/bgm-sfx.md +124 -0
  6. package/skill/short-drama/reference/canvas-recipes.md +19 -2
  7. package/skill/short-drama/reference/film-mixing-guideline.md +447 -0
  8. package/skill/short-drama/reference/mix-over-video.md +89 -0
  9. package/skill/short-drama/reference/operations-cli.md +1 -1
  10. package/skill/short-drama/reference/schema.md +4 -2
  11. package/skill/short-drama/reference/stage-characters.md +2 -2
  12. package/skill/short-drama/reference/stage-storyboard.md +29 -6
  13. package/skill/short-drama/reference/workspace-layout.md +2 -2
  14. package/skill/short-drama/scripts/selftest.mjs +54 -9
  15. package/skill/short-drama/scripts/short-drama.mjs +150 -12
  16. package/dist/commands/agent.js +0 -294
  17. package/dist/commands/asset.js +0 -241
  18. package/dist/commands/auth.js +0 -216
  19. package/dist/commands/backreel-enquiry.js +0 -171
  20. package/dist/commands/backreel-episode.js +0 -243
  21. package/dist/commands/backreel-fund.js +0 -299
  22. package/dist/commands/backreel.js +0 -443
  23. package/dist/commands/canvas.js +0 -1706
  24. package/dist/commands/class.js +0 -395
  25. package/dist/commands/config.js +0 -113
  26. package/dist/commands/crew.js +0 -205
  27. package/dist/commands/fsnode.js +0 -288
  28. package/dist/commands/gen.js +0 -356
  29. package/dist/commands/model.js +0 -136
  30. package/dist/commands/npc-album.js +0 -201
  31. package/dist/commands/npc-live.js +0 -316
  32. package/dist/commands/npc.js +0 -437
  33. package/dist/commands/org.js +0 -85
  34. package/dist/commands/preset.js +0 -176
  35. package/dist/commands/profile.js +0 -129
  36. package/dist/commands/skill.js +0 -272
  37. package/dist/commands/workspace.js +0 -470
  38. package/dist/core/archive.js +0 -95
  39. package/dist/core/backreel.js +0 -23
  40. package/dist/core/client.js +0 -269
  41. package/dist/core/config.js +0 -95
  42. package/dist/core/crew.js +0 -546
  43. package/dist/core/image.js +0 -106
  44. package/dist/core/members.js +0 -54
  45. package/dist/core/npc.js +0 -108
  46. package/dist/core/output.js +0 -174
  47. package/dist/core/prompt.js +0 -118
  48. package/dist/core/vod.js +0 -149
  49. package/dist/core/zip.js +0 -87
@@ -1,294 +0,0 @@
1
- import { Command } from 'commander';
2
- import { WjiClient } from '../core/client.js';
3
- import { confirm, parseKeyValues, readValue } from '../core/prompt.js';
4
- import { color, detail, info, relativeTime, render, success, table, truncate } from '../core/output.js';
5
- /** Parse a skill ref token: `system:<key>` or `user:<id>` (bare = user id). */
6
- function parseSkillRef(token) {
7
- const [kindRaw, rest] = token.includes(':') ? token.split(/:(.+)/) : ['user', token];
8
- const kind = kindRaw === 'system' ? 'system' : 'user';
9
- if (!rest)
10
- throw new Error(`Invalid skill ref "${token}" — use system:<key> or user:<id>`);
11
- return kind === 'system' ? { kind, key: rest } : { kind, id: rest };
12
- }
13
- function skillRefLabel(ref) {
14
- if (!ref?.kind)
15
- return String(ref);
16
- return ref.kind === 'system' ? `system:${ref.key}` : `user:${ref.id}`;
17
- }
18
- /** Backend DEFAULT_MAX_STEPS / MAX_STEPS_LIMIT (src/ai/_/constant.ts). */
19
- const DEFAULT_MAX_STEPS = 40;
20
- const MAX_STEPS_LIMIT = 200;
21
- /** `--max-steps`: an integer in range, or "default" to clear back to the site default. */
22
- function parseMaxSteps(value) {
23
- if (value === undefined)
24
- return undefined;
25
- if (['default', 'none', 'null'].includes(value.trim().toLowerCase()))
26
- return null;
27
- const n = Number(value);
28
- if (!Number.isInteger(n) || n < 1 || n > MAX_STEPS_LIMIT) {
29
- throw new Error(`--max-steps must be an integer from 1 to ${MAX_STEPS_LIMIT} (or "default")`);
30
- }
31
- return n;
32
- }
33
- export function agentCommand(globals) {
34
- const agent = new Command('agent').description('Manage AI agents');
35
- agent
36
- .command('list', { isDefault: true })
37
- .description('Browse the public agent marketplace')
38
- .option('-n, --name <name>', 'exact agent name')
39
- .option('-c, --category <name>', 'exact category')
40
- .option('-t, --tag <tag>', 'agents carrying this tag')
41
- .option('--include-unlisted', 'also show unlisted agents')
42
- .option('--page <n>', 'page number', '1')
43
- .option('--limit <n>', 'page size', '20')
44
- .action(async (opts) => {
45
- const client = new WjiClient(globals());
46
- const result = await client.get('/ai/agent/search', {
47
- name: opts.name,
48
- category: opts.category,
49
- tags: opts.tag,
50
- includeUnlisted: opts.includeUnlisted ? 'true' : undefined,
51
- page: Number(opts.page),
52
- limit: Number(opts.limit),
53
- });
54
- render(result, (r) => printAgentTable(r));
55
- });
56
- agent
57
- .command('mine')
58
- .description('List agents you created')
59
- .option('-n, --name <name>', 'exact agent name')
60
- .option('-c, --category <name>', 'exact category')
61
- .option('--visibility <v>', 'public | private | unlist')
62
- .option('--page <n>', 'page number', '1')
63
- .option('--limit <n>', 'page size', '20')
64
- .action(async (opts) => {
65
- const client = new WjiClient(globals());
66
- const result = await client.get('/ai/agent/search/my', {
67
- name: opts.name,
68
- category: opts.category,
69
- visibility: opts.visibility,
70
- page: Number(opts.page),
71
- limit: Number(opts.limit),
72
- });
73
- render(result, (r) => printAgentTable(r));
74
- });
75
- agent
76
- .command('get <id>')
77
- .description('Show an agent (private agents: creator only; instructions only when you own it)')
78
- .action(async (id) => {
79
- const client = new WjiClient(globals());
80
- const a = await client.get(`/ai/agent/list/${id}`);
81
- render(a, () => detail([
82
- ['id', a.id],
83
- ['name', a.name],
84
- ['description', a.description],
85
- ['model', a.model],
86
- ['visibility', a.visibility],
87
- ['category', a.category],
88
- ['tags', (a.tags || []).join(', ')],
89
- ['greeting', a.greeting],
90
- ['maxSteps', a.maxSteps ?? color.dim(`default (${DEFAULT_MAX_STEPS})`)],
91
- ['creator', a.creator?.name || a.creator?.id || a.creator],
92
- ['organization', a.organization?.id || a.organization || 'personal'],
93
- ['skills', (a.skills || []).map(skillRefLabel).join(', ')],
94
- ['instructions', a.instructions],
95
- ]));
96
- });
97
- agent
98
- .command('create <name>')
99
- .description('Create an agent (needs the agent-create permission on your account)')
100
- .requiredOption('-d, --description <text>', 'short description')
101
- .option('-i, --instructions <text>', 'system prompt (@file / - supported)')
102
- .option('-m, --model <name>', 'backing model')
103
- .option('--visibility <v>', 'private | unlist | public', 'private')
104
- .option('-c, --category <name>', 'category')
105
- .option('-t, --tag <tag...>', 'tags')
106
- .option('--greeting <text>', 'opening message')
107
- .option('--skill <ref...>', 'attach skills: system:<key> or user:<skillId>')
108
- .option('--max-steps <n>', `model calls allowed per chat turn, 1-${MAX_STEPS_LIMIT} (default ${DEFAULT_MAX_STEPS})`)
109
- .option('--set <key=value...>', 'arbitrary field (admin-only fields are dropped by the server)')
110
- .action(async (name, opts) => {
111
- const client = new WjiClient(globals());
112
- const created = await client.post('/ai/agent/create', {
113
- name,
114
- description: opts.description,
115
- instructions: opts.instructions ? await readValue(opts.instructions) : undefined,
116
- model: opts.model,
117
- visibility: opts.visibility,
118
- category: opts.category,
119
- tags: opts.tag,
120
- greeting: opts.greeting,
121
- skills: opts.skill?.length ? opts.skill.map(parseSkillRef) : undefined,
122
- maxSteps: parseMaxSteps(opts.maxSteps),
123
- ...parseKeyValues(opts.set),
124
- });
125
- render(created, (a) => success(`Created agent ${a.name} (${a.id})`));
126
- });
127
- agent
128
- .command('update <id>')
129
- .description('Update an agent (creator only)')
130
- .option('-n, --name <name>', 'new name')
131
- .option('-d, --description <text>', 'new description')
132
- .option('-i, --instructions <text>', 'new system prompt (@file / - supported)')
133
- .option('-m, --model <name>', 'new model')
134
- .option('--visibility <v>', 'private | unlist | public')
135
- .option('-c, --category <name>', 'category')
136
- .option('-t, --tag <tag...>', 'replace tags')
137
- .option('--greeting <text>', 'opening message')
138
- .option('--max-steps <n>', `model calls allowed per chat turn, 1-${MAX_STEPS_LIMIT}; "default" clears it`)
139
- .option('--set <key=value...>', 'arbitrary field (admin-only fields are dropped by the server)')
140
- .action(async (id, opts) => {
141
- const client = new WjiClient(globals());
142
- const updated = await client.put(`/ai/agent/list/${id}`, {
143
- name: opts.name,
144
- description: opts.description,
145
- instructions: opts.instructions ? await readValue(opts.instructions) : undefined,
146
- model: opts.model,
147
- visibility: opts.visibility,
148
- category: opts.category,
149
- tags: opts.tag,
150
- greeting: opts.greeting,
151
- maxSteps: parseMaxSteps(opts.maxSteps),
152
- ...parseKeyValues(opts.set),
153
- });
154
- render(updated, () => success(`Updated ${id}`));
155
- });
156
- agent
157
- .command('rm <id>')
158
- .description('Delete an agent')
159
- .option('-y, --yes', 'skip confirmation')
160
- .action(async (id, opts) => {
161
- const client = new WjiClient(globals());
162
- const a = await client.get(`/ai/agent/list/${id}`).catch(() => ({ name: id }));
163
- if (!(await confirm(`Delete agent "${a.name}" (${id})?`, opts.yes)))
164
- return info('Aborted');
165
- const result = await client.delete(`/ai/agent/list/${id}`);
166
- render(result, () => success(`Deleted ${id}`));
167
- });
168
- agent
169
- .command('files <id>')
170
- .description('List files attached to an agent (creator only)')
171
- .action(async (id) => {
172
- const client = new WjiClient(globals());
173
- const files = await client.get(`/ai/agent/list/${id}/files`);
174
- render(files, (rows) => table(rows || [], [
175
- { header: 'id', value: (f) => f.id },
176
- { header: 'name', value: (f) => f.name },
177
- { header: 'type', value: (f) => f.type },
178
- ]));
179
- });
180
- agent
181
- .command('categories')
182
- .description('List agent categories')
183
- .option('--mine', 'only categories used by your agents')
184
- .action(async (opts) => {
185
- const client = new WjiClient(globals());
186
- const list = await client.get(opts.mine ? '/ai/agent/categories/my' : '/ai/agent/categories');
187
- render(list, (rows) => rows.filter(Boolean).forEach((c) => process.stdout.write(c + '\n')));
188
- });
189
- agent
190
- .command('tags')
191
- .description('List agent tags')
192
- .option('--mine', 'only tags used by your agents')
193
- .action(async (opts) => {
194
- const client = new WjiClient(globals());
195
- const list = await client.get(opts.mine ? '/ai/agent/tags/my' : '/ai/agent/tags');
196
- render(list, (rows) => rows.filter(Boolean).forEach((t) => process.stdout.write(t + '\n')));
197
- });
198
- agent
199
- .command('recommend')
200
- .description('Show recommended agents')
201
- .action(async () => {
202
- const client = new WjiClient(globals());
203
- const rows = await client.get('/ai/agent/recommend');
204
- render(rows, (list) => table(list || [], [
205
- { header: 'id', value: (a) => a.id },
206
- { header: 'name', value: (a) => a.name },
207
- { header: 'title', value: (a) => truncate(a.recommendTitle, 40) },
208
- ]));
209
- });
210
- agent
211
- .command('skills <id>')
212
- .description('Show the skills attached to an agent')
213
- .action(async (id) => {
214
- const client = new WjiClient(globals());
215
- const a = await client.get(`/ai/agent/list/${id}`);
216
- render(a.skills || [], (rows) => {
217
- if (!rows.length)
218
- return info(color.dim('(no skills attached)'));
219
- table(rows, [
220
- { header: 'kind', value: (r) => r.kind },
221
- { header: 'ref', value: (r) => (r.kind === 'system' ? r.key : r.id) },
222
- { header: 'name', value: (r) => r.name },
223
- ]);
224
- });
225
- });
226
- agent
227
- .command('skill-add <id> <ref...>')
228
- .description('Attach skills to an agent (refs: system:<key> or user:<skillId>)')
229
- .action(async (id, refs) => {
230
- const client = new WjiClient(globals());
231
- const a = await client.get(`/ai/agent/list/${id}`);
232
- const existing = (a.skills || []);
233
- const seen = new Set(existing.map(skillRefLabel));
234
- const added = [];
235
- for (const token of refs) {
236
- const ref = parseSkillRef(token);
237
- if (seen.has(skillRefLabel(ref)))
238
- continue;
239
- existing.push(ref);
240
- seen.add(skillRefLabel(ref));
241
- added.push(skillRefLabel(ref));
242
- }
243
- const updated = await client.put(`/ai/agent/list/${id}`, { skills: existing });
244
- render(updated, () => success(added.length ? `Attached ${added.join(', ')}` : 'No new skills to attach'));
245
- });
246
- agent
247
- .command('skill-remove <id> <ref...>')
248
- .description('Detach skills from an agent (refs: system:<key> or user:<skillId>)')
249
- .action(async (id, refs) => {
250
- const client = new WjiClient(globals());
251
- const a = await client.get(`/ai/agent/list/${id}`);
252
- const drop = new Set(refs.map((t) => skillRefLabel(parseSkillRef(t))));
253
- const kept = (a.skills || []).filter((r) => !drop.has(skillRefLabel(r)));
254
- const updated = await client.put(`/ai/agent/list/${id}`, { skills: kept });
255
- render(updated, () => success(`Removed ${refs.length} skill ref(s)`));
256
- });
257
- agent
258
- .command('versions <id>')
259
- .description('List an agent\'s version history (creator only — snapshots hold instructions)')
260
- .action(async (id) => {
261
- const client = new WjiClient(globals());
262
- const versions = await client.get(`/ai/agent/list/${id}/versions`);
263
- render(versions, (rows) => table(rows || [], [
264
- { header: 'versionId', value: (v) => v.id },
265
- { header: 'v', value: (v) => v.version },
266
- { header: 'name', value: (v) => truncate(v.snapshot?.name, 24) },
267
- { header: 'model', value: (v) => v.snapshot?.model },
268
- { header: 'comment', value: (v) => truncate(v.comment, 30) },
269
- { header: 'when', value: (v) => relativeTime(v.createdAt) },
270
- ]));
271
- });
272
- agent
273
- .command('restore <id> <versionId>')
274
- .description('Restore an agent to a previous version (creator only)')
275
- .action(async (id, versionId) => {
276
- const client = new WjiClient(globals());
277
- const result = await client.post(`/ai/agent/list/${id}/versions/${versionId}/restore`);
278
- render(result, () => success(`Restored ${id} to version ${versionId}`));
279
- });
280
- return agent;
281
- }
282
- function printAgentTable(result) {
283
- const rows = result?.data || result || [];
284
- table(rows, [
285
- { header: 'id', value: (a) => a.id },
286
- { header: 'name', value: (a) => truncate(a.name, 28) },
287
- { header: 'model', value: (a) => a.model },
288
- { header: 'visibility', value: (a) => a.visibility },
289
- { header: 'category', value: (a) => a.category },
290
- { header: 'updated', value: (a) => relativeTime(a.updatedAt) },
291
- ]);
292
- if (result?.pagination)
293
- info(color.dim(`${result.pagination.total} total`));
294
- }
@@ -1,241 +0,0 @@
1
- import { createWriteStream } from 'node:fs';
2
- import { stat } from 'node:fs/promises';
3
- import { basename, join } from 'node:path';
4
- import { Readable } from 'node:stream';
5
- import { pipeline } from 'node:stream/promises';
6
- import { Command } from 'commander';
7
- import { ASSET_BATCH_MAX_IDS, WjiClient, isObjectId } from '../core/client.js';
8
- import { confirm } from '../core/prompt.js';
9
- import { color, detail, info, isJsonMode, printJson, relativeTime, render, success, table, truncate, warn } from '../core/output.js';
10
- const STATE = { 0: 'pending', 1: 'ready', [-1]: 'cancelled' };
11
- /** The server rejects the whole batch if any id is malformed — fail fast and say which. */
12
- function assertAssetIds(ids) {
13
- const bad = ids.filter((id) => !isObjectId(id));
14
- if (bad.length)
15
- throw new Error(`Not asset ids (expected 24-char ObjectIds): ${bad.join(', ')}`);
16
- }
17
- function humanSize(bytes) {
18
- if (bytes == null)
19
- return null;
20
- const units = ['B', 'KB', 'MB', 'GB'];
21
- let value = bytes;
22
- let unit = 0;
23
- while (value >= 1024 && unit < units.length - 1) {
24
- value /= 1024;
25
- unit++;
26
- }
27
- return `${value.toFixed(unit === 0 ? 0 : 1)}${units[unit]}`;
28
- }
29
- export function assetCommand(globals) {
30
- const asset = new Command('asset').description('Upload and manage assets');
31
- asset
32
- .command('upload <file...>')
33
- .description('Upload one or more local files')
34
- .option('-w, --workspace <id>', 'attach to a workspace')
35
- .option('--store <id>', 'vector store id')
36
- .option('--vectorize', 'index the file for retrieval')
37
- .option('--width <n>', 'source width hint')
38
- .option('--height <n>', 'source height hint')
39
- .option('--duration <n>', 'media duration hint in seconds')
40
- .action(async (files, opts) => {
41
- const client = new WjiClient(globals());
42
- const workspace = client.workspace(opts.workspace);
43
- const uploaded = [];
44
- for (const file of files) {
45
- const size = (await stat(file)).size;
46
- info(color.dim(`Uploading ${basename(file)} (${humanSize(size)})…`));
47
- const result = await client.upload('/assets/v2/upload', file, {
48
- workspace,
49
- store: opts.store,
50
- vectorize: opts.vectorize ? 'true' : undefined,
51
- width: opts.width,
52
- height: opts.height,
53
- duration: opts.duration,
54
- });
55
- uploaded.push(result);
56
- if (!isJsonMode())
57
- success(`${basename(file)} → ${result.id}`);
58
- }
59
- if (isJsonMode())
60
- render(files.length === 1 ? uploaded[0] : uploaded, () => { });
61
- });
62
- asset
63
- .command('import <url>')
64
- .description('Import an asset from a URL')
65
- .option('-w, --workspace <id>', 'attach to a workspace')
66
- .option('-n, --name <name>', 'asset name')
67
- .option('--type <type>', 'asset type override')
68
- .option('--raw', 'use /assets/import-from-url instead of create-from-url')
69
- .action(async (url, opts) => {
70
- const client = new WjiClient(globals());
71
- const created = opts.raw
72
- ? await client.post('/assets/import-from-url', { url })
73
- : await client.post('/assets/create-from-url', {
74
- url,
75
- workspace: client.workspace(opts.workspace),
76
- name: opts.name,
77
- type: opts.type,
78
- });
79
- render(created, (a) => success(`Imported ${a.id}`));
80
- });
81
- asset
82
- .command('list')
83
- .description('Search your assets')
84
- .option('--store <id>', 'filter by vector store')
85
- .option('--page <n>', 'page number', '1')
86
- .option('--limit <n>', 'page size', '20')
87
- .action(async (opts) => {
88
- const client = new WjiClient(globals());
89
- const result = await client.get('/assets/search', {
90
- store: opts.store,
91
- page: Number(opts.page),
92
- limit: Number(opts.limit),
93
- });
94
- render(result, (r) => {
95
- table(r.data || [], [
96
- { header: 'id', value: (a) => a.id },
97
- { header: 'type', value: (a) => a.type },
98
- { header: 'name', value: (a) => truncate(a.name, 30) },
99
- { header: 'size', value: (a) => humanSize(a.size) },
100
- { header: 'state', value: (a) => STATE[a.status] ?? a.status },
101
- { header: 'created', value: (a) => relativeTime(a.createdAt) },
102
- ]);
103
- info(color.dim(`${r.pagination?.total ?? 0} total`));
104
- });
105
- });
106
- asset
107
- .command('get <id>')
108
- .description('Show one asset')
109
- .action(async (id) => {
110
- const client = new WjiClient(globals());
111
- const a = await client.get(`/assets/list/${id}`);
112
- render(a, () => printAsset(a));
113
- });
114
- asset
115
- .command('status <ids...>')
116
- .description(`Check processing state of several assets (sent ${ASSET_BATCH_MAX_IDS} per request)`)
117
- .action(async (ids) => {
118
- assertAssetIds(ids);
119
- const unique = [...new Set(ids)];
120
- const client = new WjiClient(globals());
121
- const rows = await client.checkAssetStatus(unique);
122
- render(rows, (list) => {
123
- table(list || [], [
124
- { header: 'id', value: (a) => a.id },
125
- { header: 'state', value: (a) => STATE[a.status] ?? a.status },
126
- { header: 'error', value: (a) => truncate(a.error?.message || a.error, 40) },
127
- { header: 'url', value: (a) => truncate(a.url, 50) },
128
- ]);
129
- // The server silently drops ids you may not read instead of erroring.
130
- const missing = unique.length - (list?.length ?? 0);
131
- if (missing > 0) {
132
- warn(`${missing} id(s) not returned: not found, or neither yours nor in a workspace you belong to`);
133
- }
134
- });
135
- });
136
- asset
137
- .command('update <id>')
138
- .description('Rename or retag an asset')
139
- .option('-n, --name <name>', 'new name')
140
- .option('--tags <tag...>', 'replace tags')
141
- .action(async (id, opts) => {
142
- const client = new WjiClient(globals());
143
- const updated = await client.patch(`/assets/list/${id}`, {
144
- name: opts.name,
145
- tags: opts.tags,
146
- });
147
- render(updated, () => success(`Updated ${id}`));
148
- });
149
- asset
150
- .command('rm <id>')
151
- .description('Delete an asset')
152
- .option('-y, --yes', 'skip confirmation')
153
- .action(async (id, opts) => {
154
- if (!(await confirm(`Delete asset ${id}?`, opts.yes)))
155
- return info('Aborted');
156
- const client = new WjiClient(globals());
157
- const result = await client.delete(`/assets/list/${id}`);
158
- render(result, () => success(`Deleted ${id}`));
159
- });
160
- asset
161
- .command('download <ids...>')
162
- .description('Download assets to disk (a directory keeps each asset name plus its file extension)')
163
- .option('-o, --out <path>', 'output file (one id) or directory', '.')
164
- .action(async (ids, opts) => {
165
- const client = new WjiClient(globals());
166
- const isDir = await stat(opts.out).then((s) => s.isDirectory()).catch(() => false);
167
- if (ids.length > 1 && !isDir)
168
- throw new Error('With several ids, --out must be an existing directory');
169
- const saved = [];
170
- for (const id of ids) {
171
- const a = await client.get(`/assets/list/${id}`);
172
- if (!a?.url)
173
- throw new Error(`Asset ${id} has no URL yet (state: ${STATE[a?.status] ?? a?.status})`);
174
- const res = await fetch(a.url);
175
- if (!res.ok || !res.body)
176
- throw new Error(`Download failed: ${res.status} ${res.statusText}`);
177
- let target = opts.out;
178
- if (isDir) {
179
- // Asset names are titles ("boat"), not file names: keep the extension so
180
- // image viewers and agents can open the file.
181
- const base = String(a.name || id).replace(/[\\/]/g, '_');
182
- target = join(opts.out, /\.[a-z0-9]{2,5}$/i.test(base) ? base : `${base}${extensionOf(a.url)}`);
183
- }
184
- await pipeline(Readable.fromWeb(res.body), createWriteStream(target));
185
- saved.push({ id, path: target });
186
- if (!isJsonMode())
187
- success(`Saved ${target}`);
188
- }
189
- if (isJsonMode())
190
- printJson(ids.length === 1 ? saved[0] : saved);
191
- });
192
- asset
193
- .command('resolve <url>')
194
- .description('Find an existing asset by its URL')
195
- .option('-w, --workspace <id>', 'workspace scope')
196
- .action(async (url, opts) => {
197
- const client = new WjiClient(globals());
198
- const a = await client.get('/assets/resolve-by-url', {
199
- url,
200
- workspace: client.workspace(opts.workspace),
201
- });
202
- render(a, (found) => (found ? printAsset(found) : info('No matching asset')));
203
- });
204
- asset
205
- .command('summary <id...>')
206
- .description(`Generate AI summaries for your own assets (at most ${ASSET_BATCH_MAX_IDS} per call)`)
207
- .action(async (ids) => {
208
- assertAssetIds(ids);
209
- // Not batched automatically: every id is a separate vectorize + LLM job.
210
- if (ids.length > ASSET_BATCH_MAX_IDS) {
211
- throw new Error(`asset summary takes at most ${ASSET_BATCH_MAX_IDS} ids per call`);
212
- }
213
- const client = new WjiClient(globals());
214
- const result = ids.length === 1
215
- ? await client.post(`/assets/list/${ids[0]}/summary`)
216
- : await client.post('/assets/summary/batch', { ids });
217
- render(result, (r) => detail(Object.entries(r || {})));
218
- });
219
- return asset;
220
- }
221
- function extensionOf(url) {
222
- const match = /\.([a-z0-9]{2,5})(?:\?|$)/i.exec(url);
223
- return match ? `.${match[1]}` : '';
224
- }
225
- function printAsset(a) {
226
- detail([
227
- ['id', a.id],
228
- ['name', a.name],
229
- ['type', a.type],
230
- ['mimetype', a.mimetype],
231
- ['state', STATE[a.status] ?? a.status],
232
- ['size', humanSize(a.size)],
233
- ['aspectRatio', a.aspectRatio],
234
- ['duration', a.duration],
235
- ['url', a.url],
236
- ['thumbnail', a.thumbnail],
237
- ['tags', (a.tags || []).join(', ')],
238
- ['error', a.error?.message || a.error],
239
- ['createdAt', a.createdAt],
240
- ]);
241
- }