@wji.ai/cli 0.5.0 → 0.6.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/README.md +22 -2
- package/dist/commands/canvas.js +52 -5
- package/dist/commands/fsnode.js +13 -6
- package/dist/commands/gen.js +3 -2
- package/dist/commands/model.js +4 -2
- package/dist/commands/workspace.js +6 -3
- package/dist/core/output.js +29 -0
- package/dist/index.js +5 -2
- package/package.json +1 -1
- package/skill/ads/SKILL.md +176 -0
- package/skill/ads/reference/ai-consistency.md +109 -0
- package/skill/ads/reference/brief-template.md +103 -0
- package/skill/ads/reference/compliance.md +66 -0
- package/skill/ads/reference/formats-image.md +108 -0
- package/skill/ads/reference/formats-video.md +121 -0
- package/skill/ads/reference/models.md +95 -0
- package/skill/ads/reference/product-photography.md +119 -0
- package/skill/ads/reference/styles.md +70 -0
- package/skill/short-drama/SKILL.md +4 -0
- package/skill/short-drama/reference/canvas-recipes.md +18 -0
- package/skill/short-drama/reference/operations-cli.md +1 -0
- package/skill/short-drama/reference/operations-wji.md +27 -4
- package/skill/short-drama/reference/stage-storyboard.md +6 -0
- package/skill/social-media/SKILL.md +152 -0
- package/skill/social-media/reference/content-craft.md +124 -0
- package/skill/social-media/reference/plan-template.md +171 -0
- package/skill/social-media/reference/platforms-china.md +94 -0
- package/skill/social-media/reference/platforms-global.md +122 -0
- package/skill/social-media/reference/ratio-map.md +100 -0
- package/skill/social-media/reference/repurpose.md +119 -0
- package/skill/wji/SKILL.md +30 -2
- package/skill/wji/reference/commands.md +27 -12
package/README.md
CHANGED
|
@@ -117,8 +117,12 @@ Run `wji <group> --help` for the full option list.
|
|
|
117
117
|
|
|
118
118
|
## Conventions
|
|
119
119
|
|
|
120
|
-
- **`--json`** on any command prints
|
|
121
|
-
|
|
120
|
+
- **`--json`** on any command prints JSON and nothing else, so everything
|
|
121
|
+
composes with `jq`. List and get commands print the **`standard`** level by
|
|
122
|
+
default (ids, names, status, media urls, long text clipped to 200 chars,
|
|
123
|
+
`content` summarised with `truncated: true`). Add **`--detail full`** (or set
|
|
124
|
+
`WJI_DETAIL=full`) for the raw API response, `--detail brief` for id lists.
|
|
125
|
+
Human table output is never affected.
|
|
122
126
|
- **`@file` and `-`** work anywhere a long text or JSON value is expected:
|
|
123
127
|
`-p @prompt.txt`, `--content @node.json`, `-p -` to read stdin.
|
|
124
128
|
- **`--set key=value`** sends arbitrary fields; dotted keys nest
|
|
@@ -244,6 +248,7 @@ Skill refs are `system:<key>` or `user:<skillId>` (a bare id is treated as `user
|
|
|
244
248
|
| Generation submissions | image 30 / video 10 / audio 20 per minute, per user or key |
|
|
245
249
|
| Generation queue | queued + running jobs: FREE 3 / VIP 10 / SVIP 25 → HTTP 429 |
|
|
246
250
|
| Pagination | `--limit` ≤100; `-1` returns at most 500 except on opt-in endpoints |
|
|
251
|
+
| Detail levels | `?detail=brief\|standard\|full` on workspace, fsnode, model and history reads; the API defaults to `full`, the CLI's `--json` to `standard` |
|
|
247
252
|
| Batches | `asset status` is sent 100 ids at a time; `asset summary` ≤100 ids |
|
|
248
253
|
| Skills | zip ≤5MB, ≤200 files, ≤2MB per `.md`, ≤20MB total, 50 per account |
|
|
249
254
|
| Creation caps | 50 workspaces, 3 organizations per account |
|
|
@@ -290,6 +295,21 @@ upgrading the CLI so the skill matches the commands.
|
|
|
290
295
|
pipeline (outline → characters → scenes/props → script → storyboard) whose
|
|
291
296
|
stages are checked by `skill/short-drama/scripts/short-drama.mjs validate`,
|
|
292
297
|
with the data stored as workspace datasets and the work laid out on canvases.
|
|
298
|
+
|
|
299
|
+
`skill/ads/` and `skill/social-media/` are two more: `ads` makes commercials
|
|
300
|
+
and ad creative (TVC / bumper / social ad formats and durations, product
|
|
301
|
+
photography and studio lighting, visual styles, product identity across
|
|
302
|
+
generations, banner sizes, AI-disclosure compliance); `social-media` holds
|
|
303
|
+
every platform's image / video specs (Instagram, TikTok, YouTube, Facebook, X,
|
|
304
|
+
LinkedIn, Pinterest, Threads, Snapchat, 小红书, 抖音, 快手, 视频号, B站, 微博,
|
|
305
|
+
公众号) and the repurposing recipes — an episode into 9:16 teasers and posters,
|
|
306
|
+
an ad master into every placement. Install them the same way:
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
cp -r "$(npm root -g)/@wji.ai/cli/skill/short-drama" ~/.claude/skills/short-drama
|
|
310
|
+
cp -r "$(npm root -g)/@wji.ai/cli/skill/ads" ~/.claude/skills/ads
|
|
311
|
+
cp -r "$(npm root -g)/@wji.ai/cli/skill/social-media" ~/.claude/skills/social-media
|
|
312
|
+
```
|
|
293
313
|
Install it the same way (`cp -r skill/short-drama ~/.claude/skills/short-drama`)
|
|
294
314
|
next to `wji`; `node skill/short-drama/scripts/selftest.mjs` runs its self-test.
|
|
295
315
|
|
package/dist/commands/canvas.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { WjiClient } from '../core/client.js';
|
|
3
3
|
import { confirm, parseKeyValues, readValue } from '../core/prompt.js';
|
|
4
|
-
import { color, detail, info, isJsonMode, printJson, relativeTime, render, success, table, truncate, warn } from '../core/output.js';
|
|
4
|
+
import { clipText, color, detail, detailLevel, info, isJsonMode, printJson, relativeTime, render, success, table, truncate, warn } from '../core/output.js';
|
|
5
5
|
import { pollHistory, reportHistory, resolveAssets } from './gen.js';
|
|
6
6
|
import { loadMembers, memberLabel, resolveMember } from '../core/members.js';
|
|
7
7
|
import { fetchModels } from './model.js';
|
|
@@ -315,6 +315,53 @@ export function describe(canvas, node) {
|
|
|
315
315
|
outgoing: canvas.edges.filter((e) => e.fromNodeId === node.id).map((e) => e.toNodeId),
|
|
316
316
|
};
|
|
317
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* The canvas commands always load the full canvas (edits and generation need
|
|
320
|
+
* every field), so --detail is applied here, to the JSON they print. Mirrors
|
|
321
|
+
* the backend's canvas_list_nodes levels.
|
|
322
|
+
*/
|
|
323
|
+
export function trimDescribed(row, level = detailLevel()) {
|
|
324
|
+
if (!level || level === 'full')
|
|
325
|
+
return row;
|
|
326
|
+
if (level === 'brief') {
|
|
327
|
+
return compactRow({
|
|
328
|
+
id: row.id,
|
|
329
|
+
kind: row.kind,
|
|
330
|
+
name: row.name,
|
|
331
|
+
tag: row.tag,
|
|
332
|
+
mode: row.mode,
|
|
333
|
+
status: row.status,
|
|
334
|
+
assetId: row.assetId,
|
|
335
|
+
position: row.position,
|
|
336
|
+
references: row.references.map((r) => ({ nodeId: r.nodeId, label: r.label })),
|
|
337
|
+
outgoing: row.outgoing,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
const g = row.generate;
|
|
341
|
+
return compactRow({
|
|
342
|
+
...row,
|
|
343
|
+
text: row.text === undefined ? undefined : clipText(row.text) ?? '',
|
|
344
|
+
error: typeof row.error === 'string' ? clipText(row.error) : row.error,
|
|
345
|
+
generate: g
|
|
346
|
+
? compactRow({
|
|
347
|
+
prompt: clipText(g.prompt),
|
|
348
|
+
model: g.model,
|
|
349
|
+
ratio: g.ratio,
|
|
350
|
+
resolution: g.resolution,
|
|
351
|
+
duration: g.duration,
|
|
352
|
+
historyId: g.historyId,
|
|
353
|
+
pendingAssetId: g.pendingAssetId,
|
|
354
|
+
})
|
|
355
|
+
: undefined,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
function compactRow(value) {
|
|
359
|
+
const out = {};
|
|
360
|
+
for (const key of Object.keys(value))
|
|
361
|
+
if (value[key] !== undefined)
|
|
362
|
+
out[key] = value[key];
|
|
363
|
+
return out;
|
|
364
|
+
}
|
|
318
365
|
// ── models ──────────────────────────────────────────────────────────────────
|
|
319
366
|
/** An audio node is fed by either an `audio` or a `tts` model — the backend tags voice models `tts`. */
|
|
320
367
|
const KIND_FEATURES = { audio: ['audio', 'tts'] };
|
|
@@ -1022,7 +1069,7 @@ export function canvasCommand(globals) {
|
|
|
1022
1069
|
.filter((id) => loaded.byId.has(id))
|
|
1023
1070
|
.map((id) => describe(loaded, loaded.byId.get(id)));
|
|
1024
1071
|
}
|
|
1025
|
-
const out = { ...res, state: { ...state, canvasName }, selected };
|
|
1072
|
+
const out = { ...res, state: { ...state, canvasName }, selected: selected.map((n) => trimDescribed(n)) };
|
|
1026
1073
|
render(out, () => {
|
|
1027
1074
|
const seconds = Math.max(0, Math.round((new Date(state.expiresAt).getTime() - Date.now()) / 1000));
|
|
1028
1075
|
detail([
|
|
@@ -1249,7 +1296,7 @@ export function canvasCommand(globals) {
|
|
|
1249
1296
|
.filter((n) => !nameRe || nameRe.test(n.name))
|
|
1250
1297
|
.map((n) => describe(cv, n))
|
|
1251
1298
|
.filter((n) => (colors ? !!n.tag && colors.has(n.tag.color) : opts.untagged ? !n.tag : true));
|
|
1252
|
-
render({ canvas: { id: cv.id, name: cv.name, workspaceId: cv.workspaceId }, nodes: rows }, () => {
|
|
1299
|
+
render({ canvas: { id: cv.id, name: cv.name, workspaceId: cv.workspaceId }, nodes: rows.map((n) => trimDescribed(n)) }, () => {
|
|
1253
1300
|
info(color.dim(`${cv.name} (${cv.id}) · ${rows.length} node(s) · ${cv.edges.length} edge(s)`));
|
|
1254
1301
|
nodeTable(rows);
|
|
1255
1302
|
});
|
|
@@ -1283,13 +1330,13 @@ export function canvasCommand(globals) {
|
|
|
1283
1330
|
});
|
|
1284
1331
|
canvas
|
|
1285
1332
|
.command('get <nodeId>')
|
|
1286
|
-
.description('One node
|
|
1333
|
+
.description('One node with its references (--detail full for the whole generate / text)')
|
|
1287
1334
|
.option('-c, --canvas <id>', 'canvas id (defaults to the linked canvas)')
|
|
1288
1335
|
.action(async (id, opts) => {
|
|
1289
1336
|
const c = client();
|
|
1290
1337
|
const cv = await loadCanvas(c, await resolveCanvasId(c, opts.canvas));
|
|
1291
1338
|
const n = describe(cv, requireNode(cv, id));
|
|
1292
|
-
render(n, () => printNode(n));
|
|
1339
|
+
render(trimDescribed(n), () => printNode(n));
|
|
1293
1340
|
});
|
|
1294
1341
|
canvas
|
|
1295
1342
|
.command('edges')
|
package/dist/commands/fsnode.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { WjiClient } from '../core/client.js';
|
|
3
3
|
import { confirm, parseKeyValues, readJson, readValue } from '../core/prompt.js';
|
|
4
|
-
import { color, detail, info, relativeTime, render, success, table, truncate } from '../core/output.js';
|
|
4
|
+
import { color, detail, detailLevel, info, relativeTime, render, success, table, truncate } from '../core/output.js';
|
|
5
5
|
import { printTree } from './workspace.js';
|
|
6
6
|
/** Nested plain objects merge; arrays and scalars are replaced. */
|
|
7
7
|
function deepMerge(base, patch) {
|
|
@@ -22,7 +22,10 @@ export function fsCommand(globals) {
|
|
|
22
22
|
.action(async (key, opts) => {
|
|
23
23
|
const client = new WjiClient(globals());
|
|
24
24
|
const path = key ? `/fsnode/root/${key}` : '/fsnode/root';
|
|
25
|
-
const node = await client.get(path,
|
|
25
|
+
const node = await client.get(path, {
|
|
26
|
+
depth: opts.depth ? Number(opts.depth) : undefined,
|
|
27
|
+
detail: detailLevel(),
|
|
28
|
+
});
|
|
26
29
|
render(node, (n) => (opts.depth ? printTree(n) : printNode(n)));
|
|
27
30
|
});
|
|
28
31
|
fs.command('ls <id>')
|
|
@@ -34,6 +37,7 @@ export function fsCommand(globals) {
|
|
|
34
37
|
const result = await client.get(`/fsnode/list/${id}/children`, {
|
|
35
38
|
limit: opts.limit ? Number(opts.limit) : undefined,
|
|
36
39
|
scope: opts.scope,
|
|
40
|
+
detail: detailLevel(),
|
|
37
41
|
});
|
|
38
42
|
const rows = Array.isArray(result) ? result : result?.data || [];
|
|
39
43
|
render(result, () => table(rows, [
|
|
@@ -50,15 +54,18 @@ export function fsCommand(globals) {
|
|
|
50
54
|
.option('-d, --depth <n>', 'levels to expand')
|
|
51
55
|
.action(async (id, opts) => {
|
|
52
56
|
const client = new WjiClient(globals());
|
|
53
|
-
const node = await client.get(`/fsnode/list/${id}/tree`,
|
|
57
|
+
const node = await client.get(`/fsnode/list/${id}/tree`, {
|
|
58
|
+
depth: opts.depth ? Number(opts.depth) : undefined,
|
|
59
|
+
detail: detailLevel(),
|
|
60
|
+
});
|
|
54
61
|
render(node, (n) => printTree(n));
|
|
55
62
|
});
|
|
56
63
|
fs.command('get <id>')
|
|
57
|
-
.description('Show one node (comma-separate ids for a batch)')
|
|
64
|
+
.description('Show one node (comma-separate ids for a batch; --detail full for the whole content)')
|
|
58
65
|
.option('--scope <scope>', 'server-side scope filter')
|
|
59
66
|
.action(async (id, opts) => {
|
|
60
67
|
const client = new WjiClient(globals());
|
|
61
|
-
const node = await client.get(`/fsnode/list/${id}`, { scope: opts.scope });
|
|
68
|
+
const node = await client.get(`/fsnode/list/${id}`, { scope: opts.scope, detail: detailLevel() });
|
|
62
69
|
render(node, (n) => (Array.isArray(n) ? n.forEach(printNode) : printNode(n)));
|
|
63
70
|
});
|
|
64
71
|
fs.command('path <id>')
|
|
@@ -232,7 +239,7 @@ export function datasetCommand(globals) {
|
|
|
232
239
|
.description('List dataset nodes in a folder')
|
|
233
240
|
.action(async (folderId) => {
|
|
234
241
|
const client = new WjiClient(globals());
|
|
235
|
-
const result = await client.get(`/fsnode/list/${folderId}/children
|
|
242
|
+
const result = await client.get(`/fsnode/list/${folderId}/children`, { detail: detailLevel() });
|
|
236
243
|
const rows = (Array.isArray(result) ? result : result?.data || []).filter((n) => n.mimetype === 'workspace/dataset' || n.type === 'file');
|
|
237
244
|
render(rows, () => table(rows, [
|
|
238
245
|
{ header: 'id', value: (n) => n.id },
|
package/dist/commands/gen.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { WjiClient } from '../core/client.js';
|
|
3
3
|
import { confirm, parseKeyValues, readJson, readValue } from '../core/prompt.js';
|
|
4
|
-
import { color, detail, info, isJsonMode, printJson, render, success, table, truncate, warn } from '../core/output.js';
|
|
4
|
+
import { color, detail, detailLevel, info, isJsonMode, printJson, render, success, table, truncate, warn } from '../core/output.js';
|
|
5
5
|
const READY = 1;
|
|
6
6
|
const CANCELLED = -1;
|
|
7
7
|
/** `--ref abc123`, `--ref abc123:hero`, `--ref abc123:hero:0.6` */
|
|
@@ -254,6 +254,7 @@ export function historyCommand(globals) {
|
|
|
254
254
|
bookmarked: opts.bookmarked ? true : undefined,
|
|
255
255
|
page: Number(opts.page),
|
|
256
256
|
limit: Number(opts.limit),
|
|
257
|
+
detail: detailLevel(),
|
|
257
258
|
});
|
|
258
259
|
render(result, (r) => {
|
|
259
260
|
table(r.data || [], [
|
|
@@ -284,7 +285,7 @@ export function historyCommand(globals) {
|
|
|
284
285
|
}));
|
|
285
286
|
}
|
|
286
287
|
else {
|
|
287
|
-
history = await client.get(`/ai/assets/history/list/${id}
|
|
288
|
+
history = await client.get(`/ai/assets/history/list/${id}`, { detail: detailLevel() });
|
|
288
289
|
assets = await resolveAssets(client, history);
|
|
289
290
|
}
|
|
290
291
|
render({ ...history, images: assets }, () => reportHistory(history, assets));
|
package/dist/commands/model.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { WjiClient } from '../core/client.js';
|
|
3
|
-
import { color, detail, info, render, table, truncate, warn } from '../core/output.js';
|
|
3
|
+
import { color, detail, detailLevel, info, render, table, truncate, warn } from '../core/output.js';
|
|
4
4
|
const FEATURES = [
|
|
5
5
|
'text',
|
|
6
6
|
'image',
|
|
@@ -24,6 +24,7 @@ export async function fetchModels(client, opts) {
|
|
|
24
24
|
const features = opts.features?.length ? opts.features : FEATURES;
|
|
25
25
|
const list = await client.get(`/ai/model/list/${features.join(',')}`, {
|
|
26
26
|
workspaceId: client.workspace(opts.workspace),
|
|
27
|
+
detail: opts.detail,
|
|
27
28
|
});
|
|
28
29
|
return Array.isArray(list) ? list : list?.data || [];
|
|
29
30
|
}
|
|
@@ -41,6 +42,7 @@ export function modelCommand(globals) {
|
|
|
41
42
|
let rows = await fetchModels(client, {
|
|
42
43
|
features: opts.feature,
|
|
43
44
|
workspace: opts.workspace,
|
|
45
|
+
detail: detailLevel(),
|
|
44
46
|
});
|
|
45
47
|
if (!opts.all)
|
|
46
48
|
rows = rows.filter((m) => m.enabled !== false && !m.hidden);
|
|
@@ -70,7 +72,7 @@ export function modelCommand(globals) {
|
|
|
70
72
|
.option('-w, --workspace <id>', 'resolve pricing/access for a workspace')
|
|
71
73
|
.action(async (name, opts) => {
|
|
72
74
|
const client = new WjiClient(globals());
|
|
73
|
-
const rows = await fetchModels(client, { workspace: opts.workspace });
|
|
75
|
+
const rows = await fetchModels(client, { workspace: opts.workspace, detail: detailLevel() });
|
|
74
76
|
// `name` is not unique across providers; `nickname` is, so prefer it.
|
|
75
77
|
const matches = rows.filter((m) => m.nickname === name || m.name === name);
|
|
76
78
|
if (!matches.length)
|
|
@@ -2,7 +2,7 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { WjiClient } from '../core/client.js';
|
|
3
3
|
import { updateProfile } from '../core/config.js';
|
|
4
4
|
import { confirm, parseKeyValues } from '../core/prompt.js';
|
|
5
|
-
import { color, detail, info, relativeTime, render, success, table, truncate } from '../core/output.js';
|
|
5
|
+
import { color, detail, detailLevel, info, relativeTime, render, success, table, truncate } from '../core/output.js';
|
|
6
6
|
import { loadMembers, memberLabel, resolveMember } from '../core/members.js';
|
|
7
7
|
/** App tokens accepted by `--app`; mirrors WorkspaceApplication on the backend. */
|
|
8
8
|
const APPS = {
|
|
@@ -54,6 +54,7 @@ export function workspaceCommand(globals) {
|
|
|
54
54
|
scope: opts.scope,
|
|
55
55
|
page: Number(opts.page),
|
|
56
56
|
limit: Number(opts.limit),
|
|
57
|
+
detail: detailLevel(),
|
|
57
58
|
});
|
|
58
59
|
const current = client.workspace();
|
|
59
60
|
render(result, (r) => {
|
|
@@ -77,7 +78,9 @@ export function workspaceCommand(globals) {
|
|
|
77
78
|
.description('Show a workspace (defaults to the selected one)')
|
|
78
79
|
.action(async (id) => {
|
|
79
80
|
const client = new WjiClient(globals());
|
|
80
|
-
const w = await client.get(`/workspace/list/${client.requireWorkspace(id)}
|
|
81
|
+
const w = await client.get(`/workspace/list/${client.requireWorkspace(id)}`, {
|
|
82
|
+
detail: detailLevel(),
|
|
83
|
+
});
|
|
81
84
|
render(w, () => detail([
|
|
82
85
|
['id', w.id],
|
|
83
86
|
['name', w.name],
|
|
@@ -173,7 +176,7 @@ export function workspaceCommand(globals) {
|
|
|
173
176
|
const path = opts.app
|
|
174
177
|
? `/workspace/list/${wsId}/fs/${opts.app}`
|
|
175
178
|
: `/workspace/list/${wsId}/fs`;
|
|
176
|
-
const root = await client.get(path);
|
|
179
|
+
const root = await client.get(path, { detail: detailLevel() });
|
|
177
180
|
render(root, (node) => printTree(node));
|
|
178
181
|
});
|
|
179
182
|
// ── apps ───────────────────────────────────────────────────────────────────
|
package/dist/core/output.js
CHANGED
|
@@ -17,6 +17,35 @@ export function setJsonMode(on) {
|
|
|
17
17
|
export function isJsonMode() {
|
|
18
18
|
return jsonMode;
|
|
19
19
|
}
|
|
20
|
+
const DETAIL_LEVELS = ['brief', 'standard', 'full'];
|
|
21
|
+
/**
|
|
22
|
+
* Global --detail (or WJI_DETAIL). Unset means "standard when printing JSON,
|
|
23
|
+
* nothing (server default = full) for human output", so tables are unchanged.
|
|
24
|
+
*/
|
|
25
|
+
let detailMode;
|
|
26
|
+
export function setDetailMode(raw) {
|
|
27
|
+
if (raw === undefined || raw === '') {
|
|
28
|
+
detailMode = undefined;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const level = String(raw).toLowerCase();
|
|
32
|
+
if (!DETAIL_LEVELS.includes(level)) {
|
|
33
|
+
throw new Error(`--detail must be one of ${DETAIL_LEVELS.join(', ')} (got "${raw}")`);
|
|
34
|
+
}
|
|
35
|
+
detailMode = level;
|
|
36
|
+
}
|
|
37
|
+
export function detailLevel() {
|
|
38
|
+
if (detailMode)
|
|
39
|
+
return detailMode;
|
|
40
|
+
return jsonMode ? 'standard' : undefined;
|
|
41
|
+
}
|
|
42
|
+
/** Clip long text the way the server does at `standard` (200 chars + …). */
|
|
43
|
+
export function clipText(value, max = 200) {
|
|
44
|
+
if (value === undefined || value === null || value === '')
|
|
45
|
+
return undefined;
|
|
46
|
+
const s = String(value);
|
|
47
|
+
return s.length <= max ? s : `${s.slice(0, max)}…`;
|
|
48
|
+
}
|
|
20
49
|
export function printJson(value) {
|
|
21
50
|
process.stdout.write(JSON.stringify(value, null, 2) + '\n');
|
|
22
51
|
}
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { ApiError } from './core/client.js';
|
|
5
|
-
import { color, setJsonMode } from './core/output.js';
|
|
5
|
+
import { color, setDetailMode, setJsonMode } from './core/output.js';
|
|
6
6
|
import { authCommand } from './commands/auth.js';
|
|
7
7
|
import { configCommand } from './commands/config.js';
|
|
8
8
|
import { orgCommand } from './commands/org.js';
|
|
@@ -34,7 +34,8 @@ program
|
|
|
34
34
|
.option('--org <orgId>', 'organization context (X-Org-Id)')
|
|
35
35
|
.option('--no-org', 'force personal context')
|
|
36
36
|
.option('--locale <locale>', 'response language (cht, chs, en)')
|
|
37
|
-
.option('--json', 'print
|
|
37
|
+
.option('--json', 'print JSON instead of tables (standard detail unless --detail is given)')
|
|
38
|
+
.option('--detail <level>', 'how much to return: brief | standard | full (default standard for --json; env WJI_DETAIL)')
|
|
38
39
|
.configureHelp({ sortSubcommands: true });
|
|
39
40
|
/**
|
|
40
41
|
* Global flags live on the root command, but subcommand actions run before
|
|
@@ -51,10 +52,12 @@ const globals = () => {
|
|
|
51
52
|
// commander turns `--no-org` into org === false
|
|
52
53
|
personal: opts.org === false,
|
|
53
54
|
locale: opts.locale,
|
|
55
|
+
detail: opts.detail,
|
|
54
56
|
};
|
|
55
57
|
};
|
|
56
58
|
program.hook('preAction', () => {
|
|
57
59
|
setJsonMode(!!program.opts().json);
|
|
60
|
+
setDetailMode(program.opts().detail ?? process.env.WJI_DETAIL);
|
|
58
61
|
});
|
|
59
62
|
program.addCommand(authCommand(globals));
|
|
60
63
|
program.addCommand(configCommand(globals));
|
package/package.json
CHANGED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ads
|
|
3
|
+
description: Make commercials and ad creative with AI on WJI through the `wji` CLI — video ads (TVC 15 / 30 / 60 s, 6-second bumpers, skippable and non-skippable pre-roll, 9–34 s social ads, product demos, UGC-style, testimonials, explainers) and ad images (product shots / 产品图, key visuals / 主视觉 KV, display banners), with studio lighting and product-photography vocabulary, visual styles by industry, second-by-second ad structure (hook in 3 s, brand by 5 s, CTA card), product identity locked across every generation, deliverables per placement and AI-disclosure compliance. Use whenever a task mentions an ad / 广告 / 廣告, a commercial, TVC, product shot, packshot, banner, key visual, promo / 宣传片 or brand video. Hands per-platform sizes to the social-media skill. Works together with the `wji` skill.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Ads on WJI
|
|
7
|
+
|
|
8
|
+
One brief, one canvas, one master. **You write the brief, the concept and every prompt**; the
|
|
9
|
+
`wji` CLI keeps the brief as a dataset, lays the work out on a canvas and generates the stills
|
|
10
|
+
and clips. Nothing is generated until the brief is complete and the user has said yes to the
|
|
11
|
+
quote (or the message carried `-auto`, see the `wji` skill).
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
brief ──▶ concept & hooks ──▶ key visual / storyboard ──▶ stills ──▶ clips ──▶ master + deliverables
|
|
15
|
+
│ │ │ │ │ │
|
|
16
|
+
brief.md 3–5 hooks, shot list with product- image-to- ratio variants,
|
|
17
|
+
+ story/ framework, timecodes, one locked video, overlays, end card
|
|
18
|
+
dataset CTA & end card master ratio KV + keys 3–8 s each → social-media skill
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`{baseDir}` = this skill. Read `{baseDir}/reference/brief-template.md` before asking the user
|
|
22
|
+
anything and `{baseDir}/reference/formats-video.md` or `formats-image.md` before proposing a format.
|
|
23
|
+
The `wji` skill governs every CLI call (auth, confirmations, spending policy, model choice).
|
|
24
|
+
|
|
25
|
+
| Read | When |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `reference/brief-template.md` | before the first question — the fields, and what a complete brief looks like |
|
|
28
|
+
| `reference/formats-video.md` | choosing a video format, its length and second-by-second structure; cutting the ad into clips |
|
|
29
|
+
| `reference/formats-image.md` | a KV, packshot or banner: IAB / Google / Meta sizes, text limits, generate-then-crop |
|
|
30
|
+
| `reference/product-photography.md` | writing any product still: lighting setups, shot types, backgrounds, lens, grade, the prompt template |
|
|
31
|
+
| `reference/styles.md` | picking the look — 13 styles with signifiers and prompt phrases, by industry |
|
|
32
|
+
| `reference/ai-consistency.md` | before generating: how the product stays the same across stills and clips, what never to generate |
|
|
33
|
+
| `reference/models.md` | which WJI model for which need (ratios, resolutions, references, text in image, transparent background) |
|
|
34
|
+
| `reference/compliance.md` | before delivery: AI-content labels per platform / jurisdiction, misleading-depiction rules |
|
|
35
|
+
|
|
36
|
+
## Operations
|
|
37
|
+
|
|
38
|
+
### Where the brief lives
|
|
39
|
+
|
|
40
|
+
Locally: an `ads/<product>/` folder with `brief.md` (the brief, concept, shot list and
|
|
41
|
+
deliverables — one file, sections in that order) and `prompts/` (one text file per still and
|
|
42
|
+
clip, so `--prompt-file` can send them verbatim). On WJI: the same brief as a dataset in the
|
|
43
|
+
workspace's **story** folder, so the canvas and the chat see it.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
wji auth whoami --json && wji profile wallet # preflight; an ad is a handful of stills + 4–8 clips
|
|
47
|
+
wji ws use <workspaceId> # or wji ws create "<product> ads" -r 16:9
|
|
48
|
+
wji ws app list # the Dataset app id → DS_APP
|
|
49
|
+
wji --json ws fs --app DS_APP | jq -r '.. | objects | select(.content.semanticKey? == "story") | .id' # → STORY
|
|
50
|
+
wji dataset new STORY "AD <product>" -t other -d "<one line>" --json | jq -r .id # → BRIEF
|
|
51
|
+
wji fs set BRIEF --merge content=@ads/<product>/brief.md --merge viewMode=content # the brief text
|
|
52
|
+
wji asset upload ads/<product>/packshot.png # the user's product photo → PACK asset id
|
|
53
|
+
wji models get <model> # confirm ratios and the reference cap before promising a size
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### The canvas
|
|
57
|
+
|
|
58
|
+
One canvas per ad, `AD · <product>`, rows top to bottom:
|
|
59
|
+
|
|
60
|
+
| Row | Nodes | Command |
|
|
61
|
+
| --- | --- | --- |
|
|
62
|
+
| 1 references | the packshots and brand assets | `wji canvas add image -c CV -n "packshot" --asset PACK --at 0,0` (display nodes) |
|
|
63
|
+
| 2 key visuals | one image node per style / hook variant | `wji canvas add image -c CV -n "KV minimal" --from P1 -m <image model> -r 1:1 --resolution 2K --prompt-file prompts/kv-minimal.txt --at 0,400` — the packshot node is always **first** in `--from` (`@image1`) |
|
|
64
|
+
| 3 storyboard stills | one per clip, in order | same, `--from P1 KV1` so light and grade carry over |
|
|
65
|
+
| 4 clips | one video node per still | `wji canvas add video -c CV -n "clip 01" --from S1 P1 -m <video model> -r 16:9 -d 5 --prompt-file prompts/clip-01.txt` |
|
|
66
|
+
| 5 copy | headline / CTA / legal lines | `wji canvas add text -c CV --text-file ads/<product>/overlays.md --plain` beside the clip they belong to — overlays are added in the edit, **never generated into the picture** |
|
|
67
|
+
|
|
68
|
+
`--from` order sets `@imageN`; `wji canvas connect <from> <to>` adds a reference later. Quote
|
|
69
|
+
first — `wji canvas gen <nodeIds…>` prints the cost and refuses — tell the user, then
|
|
70
|
+
`wji canvas gen <nodeIds…> --yes --wait` after the yes. `wji canvas set <node> -p … / -r … / -d …`
|
|
71
|
+
changes a node without rebuilding it; `wji canvas get <node> --json | jq -r .assetId` then
|
|
72
|
+
`wji asset download <asset> -o /tmp/ad` to look at a result before approving it.
|
|
73
|
+
|
|
74
|
+
For a one-off still with no canvas: `wji gen image -m <model> -p "…" -r 1:1 --ref PACK --wait`
|
|
75
|
+
— it still needs the user's yes first unless the message carried `-auto`.
|
|
76
|
+
|
|
77
|
+
Take ids from the command's own JSON (`wji --json canvas add … | jq -r .id`), never by searching
|
|
78
|
+
names. Ratio, resolution and duration are validated against the model — copy the spelling from
|
|
79
|
+
`wji models get`.
|
|
80
|
+
|
|
81
|
+
## Before anything else
|
|
82
|
+
|
|
83
|
+
1. **Read the brief template**, then **ask for what is missing in one message**: product (and a
|
|
84
|
+
packshot or product photo — without one the product cannot be kept consistent), audience,
|
|
85
|
+
objective (awareness / conversion / retargeting), format and length, placements and their
|
|
86
|
+
ratios, style, brand colours (hex), must-show / must-say, CTA, legal lines, whether the ad
|
|
87
|
+
will run where AI content must be labelled. Offer defaults where the user has none
|
|
88
|
+
(`reference/formats-video.md` §"Formats at a glance", `reference/styles.md` §"Choosing by industry"), but
|
|
89
|
+
**do not generate anything without the product reference and the placement**.
|
|
90
|
+
2. **Write `brief.md`** and push it as the `AD <product>` dataset. Everything later reads from it.
|
|
91
|
+
3. **Confirm the master ratio**: 16:9 when the primary placement is YouTube / TV / display,
|
|
92
|
+
9:16 when it is Reels / TikTok / Shorts / 抖音, 1:1 or 4:5 for feed-only. One master; the
|
|
93
|
+
other ratios are made later (stills regenerated or cropped, video regenerated — see
|
|
94
|
+
`reference/formats-image.md` §"Generate, then crop" and the social-media skill).
|
|
95
|
+
4. **Check the wallet and quote** (`wji gen cost`) before every generation; say what the whole
|
|
96
|
+
plan will cost before starting row 2.
|
|
97
|
+
|
|
98
|
+
## Stage 1 — Concept
|
|
99
|
+
|
|
100
|
+
- Pick the framework (`reference/formats-video.md`): AIDA for cold audiences and launches, PAS
|
|
101
|
+
for retargeting, ABCD as the checklist for anything on YouTube. Write **3–5 hooks** (first
|
|
102
|
+
3 seconds), pick one with the user, keep the others as A/B variants.
|
|
103
|
+
- Write the **shot list with timecodes**: hook 0–3 s, product in frame by 3 s, brand by 5 s,
|
|
104
|
+
proof / demo, CTA card in the last 2–5 s. Sound-off first: every beat must read with captions
|
|
105
|
+
and no audio.
|
|
106
|
+
- Cut the ad into **clips of 3–8 s**, each one generation, sized to the video model
|
|
107
|
+
(`reference/models.md`: h3 5–15 s fixed, seedance-2.5 4–30 s auto, wan 2–30 s, veo 4–8 s,
|
|
108
|
+
kling 3–15 s). A 15-s vertical ad is typically 3 clips + end card; a 30-s TVC 5–7 clips.
|
|
109
|
+
- For an **image-only** job (KV, banner set, packshot variants) the concept is the layout: where
|
|
110
|
+
the product sits, where the headline zone stays clear, which sizes are needed
|
|
111
|
+
(`reference/formats-image.md`).
|
|
112
|
+
- Put the concept into `brief.md` (`## Concept`, `## Shot list`, `## Deliverables`) and
|
|
113
|
+
`wji fs set BRIEF --merge content=@ads/<product>/brief.md`.
|
|
114
|
+
|
|
115
|
+
## Stage 2 — Key visual and stills
|
|
116
|
+
|
|
117
|
+
- Build row 1 (packshots, brand refs) and row 2 (one KV per style variant, 2–3 at most). Every
|
|
118
|
+
prompt follows `reference/product-photography.md` §"The prompt template": subject → shot → lighting
|
|
119
|
+
→ background → camera → grade → constraints, and ends with the identity line from
|
|
120
|
+
`reference/ai-consistency.md` ("keep the label, shape and colour of @image1 exactly …").
|
|
121
|
+
- Pick the model by need (`reference/models.md`): legible text in the picture → nano-banana-pro /
|
|
122
|
+
gpt-image-2.5 / seedream-5-pro; transparent background for a logo or cut-out → gpt-image-2.5
|
|
123
|
+
with `--set background=transparent`; 4:5 or 2:3 placements → nano-banana / flux-2-flex /
|
|
124
|
+
seedream-5; a marketing look out of the box → marketing-studio-image. Default
|
|
125
|
+
`--resolution 2K` for a KV, `1K` while exploring.
|
|
126
|
+
- **Never generate** legal text, small print, prices that must be exact, or a real logo — those
|
|
127
|
+
are text nodes and overlays. Headline text may be generated only to preview a layout.
|
|
128
|
+
- Download and look at the KVs, show the user, take the pick, then generate row 3 (storyboard
|
|
129
|
+
stills) `--from P1 KV1` so light direction, grade and lens stay the same across the ad.
|
|
130
|
+
- Quality pass before any clip: hands on the product, label text, logo drift, product proportions,
|
|
131
|
+
impossible physics (`reference/ai-consistency.md` §"Artefact checklist"). Regenerate a still
|
|
132
|
+
rather than trying to fix it in the clip.
|
|
133
|
+
|
|
134
|
+
## Stage 3 — Clips
|
|
135
|
+
|
|
136
|
+
- One video node per approved still, `--from S1 P1`, `-d` from the shot list (whole seconds),
|
|
137
|
+
master `-r`. Prompts describe **motion and camera only** — the look is already in the still;
|
|
138
|
+
repeat the product-identity line.
|
|
139
|
+
- Product in frame for the whole clip unless the shot list says otherwise; no text in the video
|
|
140
|
+
prompt (captions and CTA are overlays).
|
|
141
|
+
- Voice-over and music: `wji canvas add audio -c CV -m seed-audio-1.0 --prompt-file prompts/vo.txt`
|
|
142
|
+
when the user wants a VO bed; the script lines come from the brief's must-say.
|
|
143
|
+
- Quote the whole row, generate after the yes, download and review every clip against the still
|
|
144
|
+
it came from (same product, same light), regenerate the ones that drift.
|
|
145
|
+
|
|
146
|
+
## Stage 4 — Master and deliverables
|
|
147
|
+
|
|
148
|
+
- The master is the clip sequence + overlay text in order; assembling the cut, captions and
|
|
149
|
+
overlays happens in the user's editor (or locally with ffmpeg when it is installed and the user
|
|
150
|
+
asks) — list the timeline explicitly (clip → seconds → overlay text) in `## Deliverables`.
|
|
151
|
+
- **Other ratios**: stills are regenerated at the placement ratio (or cropped when the ratio is
|
|
152
|
+
not native, `reference/formats-image.md`), video is **regenerated** at the placement ratio
|
|
153
|
+
from the same stills — never cropped. When the social-media skill is installed, hand it the
|
|
154
|
+
brief and the canvas; it owns the per-platform spec sheet. Otherwise list the sizes yourself
|
|
155
|
+
from `reference/formats-image.md`.
|
|
156
|
+
- **Compliance** (`reference/compliance.md`): say whether the ad needs an AI-content label on
|
|
157
|
+
each placement, and never depict a result the product cannot deliver.
|
|
158
|
+
|
|
159
|
+
## Reporting
|
|
160
|
+
|
|
161
|
+
After each stage: what is on the canvas (row, node names), what was spent, what the user must
|
|
162
|
+
decide next (pick a KV, approve the shot list, approve a clip). After delivery: the deliverables
|
|
163
|
+
table (file, ratio, duration, overlay text, label needed) and the A/B variants still unbuilt.
|
|
164
|
+
|
|
165
|
+
## Rules specific to this skill
|
|
166
|
+
|
|
167
|
+
- **Ask before every generation**, even one image (the `wji` skill's spending policy is the
|
|
168
|
+
floor; this skill asks for images too, because an ad's stills are its most expensive mistakes).
|
|
169
|
+
- **The packshot is `@image1` in every generation** — stills and clips. No packshot, no
|
|
170
|
+
generation; ask for one or generate a packshot first and have the user approve it.
|
|
171
|
+
- One master ratio per ad; variants are separate rows, never reframes of the master.
|
|
172
|
+
- Text, prices, legal lines and logos are overlays, never generated pixels.
|
|
173
|
+
- Do not invent platform limits or sizes — cite the row in `reference/formats-video.md` /
|
|
174
|
+
`reference/formats-image.md`; rows marked *(verify)* are said as such.
|
|
175
|
+
- All-audiences content only, no real people's likeness without the user's confirmation that they have the
|
|
176
|
+
rights, no competitor products in frame.
|