@wji.ai/cli 0.1.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 ADDED
@@ -0,0 +1,242 @@
1
+ # wji-cli
2
+
3
+ Command line interface for the WJI AI platform (`wji-backend`).
4
+
5
+ Covers auth, workspaces, the fsnode tree (ideas / datasets / files), asset
6
+ uploads, image / video / audio generation, model discovery, agents, prompt
7
+ presets and your profile.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install -g @wji.ai/cli # or: pnpm add -g @wji.ai/cli / yarn global add @wji.ai/cli
13
+ wji --version
14
+ ```
15
+
16
+ Requires Node 20+ (uses built-in `fetch`, `FormData` and `Blob`).
17
+
18
+ From source:
19
+
20
+ ```bash
21
+ git clone https://github.com/ultra-instinct-ai/wji-cli.git && cd wji-cli
22
+ pnpm install && pnpm build
23
+ pnpm link --global # puts `wji` on your PATH
24
+ ```
25
+
26
+ ## Quick start
27
+
28
+ ```bash
29
+ wji auth login --email you@example.com
30
+ wji ws list
31
+ wji ws use <workspaceId> # remembered for later commands
32
+ wji models list --feature image # pass the MODEL column to -m
33
+ wji gen image -m nano-banana -p "a rainy alley at dusk" --ratio 16:9 --wait
34
+ ```
35
+
36
+ Against a local backend, point at the API port (the Nuxt dev server on `:3000`
37
+ is *not* the API):
38
+
39
+ ```bash
40
+ wji config set endpoint http://localhost:3002
41
+ ```
42
+
43
+ ## Authentication
44
+
45
+ `wji auth login` stores a JWT pair in `~/.wji/config.json` (mode `0600`) and
46
+ refreshes it automatically when the access token expires.
47
+
48
+ Each refresh rotates the refresh token, and the server revokes **every**
49
+ session if a used refresh token is presented again. Parallel `wji` processes
50
+ therefore share one refresh through a lock file beside the config
51
+ (`.refresh-<profile>.lock`) and re-read the config before refreshing. `wji auth
52
+ logout` revokes the refresh token server-side; an issued access token stays
53
+ valid until it expires.
54
+
55
+ For automation, prefer a long-lived API key:
56
+
57
+ ```bash
58
+ wji auth key create "ci" --save # secret is printed once
59
+ wji auth key list
60
+ wji auth key revoke <id>
61
+ ```
62
+
63
+ Credential precedence:
64
+
65
+ ```
66
+ --token > $WJI_TOKEN / $WJI_API_KEY > profile.apiKey > profile.accessToken
67
+ ```
68
+
69
+ `sk_…` keys are never refreshed; a JWT is.
70
+
71
+ ## Profiles, endpoints and orgs
72
+
73
+ A profile bundles an endpoint, credentials, an org context and a default
74
+ workspace. The default endpoint is `https://api.wji.ai`.
75
+
76
+ ```bash
77
+ wji config use staging
78
+ wji config set endpoint http://localhost:3002
79
+ wji config profiles
80
+ wji --profile staging ws list # one-off override
81
+ ```
82
+
83
+ Organization context is the `X-Org-Id` header, not a separate login:
84
+
85
+ ```bash
86
+ wji org list
87
+ wji org use <orgId>
88
+ wji org personal # back to your personal space
89
+ wji --no-org ws list # one-off personal call
90
+ ```
91
+
92
+ ## Commands
93
+
94
+ | Group | What it does |
95
+ | --- | --- |
96
+ | `wji auth` | login / signup / token / whoami / refresh / logout, `auth key` for API keys |
97
+ | `wji config` | profiles, endpoint, defaults |
98
+ | `wji org` | organization context, members |
99
+ | `wji ws` | workspaces, apps, members, quota, stats, templates |
100
+ | `wji fs` | fsnode tree: ls, tree, get, mkdir, touch, set, describe, rm, sort, batch |
101
+ | `wji dataset` | dataset nodes (character / location / prop / …) and dataset records |
102
+ | `wji asset` | upload, import from URL, search, status, download, summary |
103
+ | `wji canvas` | Infinite Canvas: linked canvas + selection, nodes, references, generate nodes, submit |
104
+ | `wji gen` | `image` / `video` / `audio` generation plus `cost` estimation |
105
+ | `wji history` | generation history: search, poll, bookmark, download |
106
+ | `wji models` | list models and their form parameters |
107
+ | `wji agent` | browse and manage AI agents |
108
+ | `wji preset` | prompt presets: yours, public, workspace; fork and copy |
109
+ | `wji skill` | agent skills: system list, push/pull SKILL.md bundles, import zip, update, rm |
110
+ | `wji profile` | account profile, password, wallet, login history |
111
+
112
+ Run `wji <group> --help` for the full option list.
113
+
114
+ ## Conventions
115
+
116
+ - **`--json`** on any command prints the raw API response and nothing else, so
117
+ everything composes with `jq`.
118
+ - **`@file` and `-`** work anywhere a long text or JSON value is expected:
119
+ `-p @prompt.txt`, `--content @node.json`, `-p -` to read stdin.
120
+ - **`--set key=value`** sends arbitrary fields; dotted keys nest
121
+ (`--set values.quality=high`), and `true` / `false` / `null` / numbers /
122
+ JSON literals are parsed.
123
+ - **References** are `assetId[:label[:weight]]`, e.g.
124
+ `--ref 65f0…:hero:0.6`.
125
+ - **Destructive commands** confirm interactively; `--yes` skips, and they refuse
126
+ to run unconfirmed on a non-TTY.
127
+ - Server-side filters are only offered where the API actually implements them.
128
+ Agent `--name` / `--category` / `--tag` are exact matches, and `wji asset
129
+ list` filters only by `--store`; for anything else, pipe `--json` into `jq`.
130
+
131
+ ## Generation
132
+
133
+ Generation is asynchronous. By default `wji gen` writes a *history* entry you
134
+ can poll:
135
+
136
+ ```bash
137
+ wji gen video -m wan-3.0 -p "slow pan across the rooftop" -d 5 \
138
+ --start-image <assetId> --wait --timeout 1200
139
+
140
+ wji history get <historyId> --wait
141
+ wji history download <historyId> -o ./out
142
+ ```
143
+
144
+ Check the price first with `wji gen cost -t video -m <model> -d 5`.
145
+
146
+ Model-specific parameters come from the model's own form schema — see
147
+ `wji models get <model>` — and are passed with `--set` or `--values`.
148
+
149
+ `-m` takes the value in the `MODEL` column, which is the model's `nickname`.
150
+ That is the unique key; plain names collide across providers.
151
+
152
+ ## Canvas
153
+
154
+ Open a canvas in the browser and press the **link** button in its top bar. The
155
+ open workspace, canvas and selected nodes are then available to the CLI for
156
+ 5 minutes (renewed while the tab is open), and `wji canvas` defaults to that
157
+ canvas:
158
+
159
+ ```bash
160
+ wji canvas status # what is linked and selected
161
+ wji canvas add video --from <img1> <img2> -m seedance-2.0-fast-ref -r 16:9 -d 8 \
162
+ -p "@image1 paddles across the lake while @image2 waits on the shore"
163
+ wji canvas set <node> --resolution 720p --set seed=42
164
+ wji canvas gen <node> --yes --wait # spends credits
165
+ ```
166
+
167
+ Changes appear live in the open canvas. Pass `--canvas <id>` to work on a
168
+ canvas that is not linked (`wji canvas list`).
169
+
170
+ ## Skills
171
+
172
+ Skills are SKILL.md bundles (markdown only — scripts don't persist server-side).
173
+ System skills are built-in; user skills are yours to create and attach to agents.
174
+
175
+ ```bash
176
+ wji skill system # built-in skills (image, document, …)
177
+ wji skill push ./my-skill # zip a local folder (needs SKILL.md at root) and upload
178
+ wji skill list
179
+ wji skill pull <id> ./my-skill # write the bundle back to disk
180
+ wji skill cat <id> # print its SKILL.md
181
+ wji skill rm <id>
182
+ ```
183
+
184
+ Attach skills to an agent, and use agent version history:
185
+
186
+ ```bash
187
+ wji agent skill-add <agentId> system:image user:<skillId>
188
+ wji agent skills <agentId>
189
+ wji agent versions <agentId> # every edit auto-snapshots a version
190
+ wji agent restore <agentId> <versionId>
191
+ ```
192
+
193
+ Skill refs are `system:<key>` or `user:<skillId>` (a bare id is treated as `user:`).
194
+
195
+ ## Server limits worth knowing
196
+
197
+ | Area | Limit |
198
+ | --- | --- |
199
+ | Generation submissions | image 30 / video 10 / audio 20 per minute, per user or key |
200
+ | Generation queue | queued + running jobs: FREE 3 / VIP 10 / SVIP 25 → HTTP 429 |
201
+ | Pagination | `--limit` ≤100; `-1` returns at most 500 except on opt-in endpoints |
202
+ | Batches | `asset status` is sent 100 ids at a time; `asset summary` ≤100 ids |
203
+ | Skills | zip ≤5MB, ≤200 files, ≤2MB per `.md`, ≤20MB total, 50 per account |
204
+ | Creation caps | 50 workspaces, 3 organizations per account |
205
+ | Remote import | private / internal hosts are refused, including via redirect |
206
+
207
+ Rejected requests (429 / 400) are refused before any credit is charged.
208
+
209
+ ## Exit codes
210
+
211
+ | Code | Meaning |
212
+ | --- | --- |
213
+ | `0` | success |
214
+ | `1` | error (validation, network, API 4xx/5xx) |
215
+ | `77` | authentication or permission failure (401 / 403) |
216
+
217
+ ## Environment variables
218
+
219
+ | Variable | Effect |
220
+ | --- | --- |
221
+ | `WJI_ENDPOINT` | API base URL |
222
+ | `WJI_TOKEN` / `WJI_API_KEY` | credential for this process |
223
+ | `WJI_PROFILE` | profile to use |
224
+ | `WJI_ORG_ID` | organization context |
225
+ | `WJI_WORKSPACE` | default workspace |
226
+ | `WJI_LOCALE` | response language (`cht`, `chs`, `en`) |
227
+ | `WJI_CONFIG_FILE` / `WJI_CONFIG_DIR` | relocate the config |
228
+ | `NO_COLOR` | disable colour |
229
+
230
+ ## Claude Code skill
231
+
232
+ `skill/wji/` is an Agent Skill that teaches Claude Code to drive this CLI:
233
+
234
+ The npm package ships it. After a global install:
235
+
236
+ ```bash
237
+ mkdir -p ~/.claude/skills
238
+ cp -r "$(npm root -g)/@wji.ai/cli/skill/wji" ~/.claude/skills/wji
239
+ ```
240
+
241
+ From a source checkout: `cp -r skill/wji ~/.claude/skills/wji`. Re-copy after
242
+ upgrading the CLI so the skill matches the commands.
@@ -0,0 +1,274 @@
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
+ export function agentCommand(globals) {
19
+ const agent = new Command('agent').description('Manage AI agents');
20
+ agent
21
+ .command('list', { isDefault: true })
22
+ .description('Browse the public agent marketplace')
23
+ .option('-n, --name <name>', 'exact agent name')
24
+ .option('-c, --category <name>', 'exact category')
25
+ .option('-t, --tag <tag>', 'agents carrying this tag')
26
+ .option('--include-unlisted', 'also show unlisted agents')
27
+ .option('--page <n>', 'page number', '1')
28
+ .option('--limit <n>', 'page size', '20')
29
+ .action(async (opts) => {
30
+ const client = new WjiClient(globals());
31
+ const result = await client.get('/ai/agent/search', {
32
+ name: opts.name,
33
+ category: opts.category,
34
+ tags: opts.tag,
35
+ includeUnlisted: opts.includeUnlisted ? 'true' : undefined,
36
+ page: Number(opts.page),
37
+ limit: Number(opts.limit),
38
+ });
39
+ render(result, (r) => printAgentTable(r));
40
+ });
41
+ agent
42
+ .command('mine')
43
+ .description('List agents you created')
44
+ .option('-n, --name <name>', 'exact agent name')
45
+ .option('-c, --category <name>', 'exact category')
46
+ .option('--visibility <v>', 'public | private | unlist')
47
+ .option('--page <n>', 'page number', '1')
48
+ .option('--limit <n>', 'page size', '20')
49
+ .action(async (opts) => {
50
+ const client = new WjiClient(globals());
51
+ const result = await client.get('/ai/agent/search/my', {
52
+ name: opts.name,
53
+ category: opts.category,
54
+ visibility: opts.visibility,
55
+ page: Number(opts.page),
56
+ limit: Number(opts.limit),
57
+ });
58
+ render(result, (r) => printAgentTable(r));
59
+ });
60
+ agent
61
+ .command('get <id>')
62
+ .description('Show an agent (instructions included when you own it)')
63
+ .action(async (id) => {
64
+ const client = new WjiClient(globals());
65
+ const a = await client.get(`/ai/agent/list/${id}`);
66
+ render(a, () => detail([
67
+ ['id', a.id],
68
+ ['name', a.name],
69
+ ['description', a.description],
70
+ ['model', a.model],
71
+ ['visibility', a.visibility],
72
+ ['category', a.category],
73
+ ['tags', (a.tags || []).join(', ')],
74
+ ['greeting', a.greeting],
75
+ ['creator', a.creator?.name || a.creator?.id || a.creator],
76
+ ['organization', a.organization?.id || a.organization || 'personal'],
77
+ ['skills', (a.skills || []).map(skillRefLabel).join(', ')],
78
+ ['instructions', a.instructions],
79
+ ]));
80
+ });
81
+ agent
82
+ .command('create <name>')
83
+ .description('Create an agent')
84
+ .requiredOption('-d, --description <text>', 'short description')
85
+ .option('-i, --instructions <text>', 'system prompt (@file / - supported)')
86
+ .option('-m, --model <name>', 'backing model')
87
+ .option('--visibility <v>', 'private | unlist | public', 'private')
88
+ .option('-c, --category <name>', 'category')
89
+ .option('-t, --tag <tag...>', 'tags')
90
+ .option('--greeting <text>', 'opening message')
91
+ .option('--skill <ref...>', 'attach skills: system:<key> or user:<skillId>')
92
+ .option('--set <key=value...>', 'arbitrary field')
93
+ .action(async (name, opts) => {
94
+ const client = new WjiClient(globals());
95
+ const created = await client.post('/ai/agent/create', {
96
+ name,
97
+ description: opts.description,
98
+ instructions: opts.instructions ? await readValue(opts.instructions) : undefined,
99
+ model: opts.model,
100
+ visibility: opts.visibility,
101
+ category: opts.category,
102
+ tags: opts.tag,
103
+ greeting: opts.greeting,
104
+ skills: opts.skill?.length ? opts.skill.map(parseSkillRef) : undefined,
105
+ ...parseKeyValues(opts.set),
106
+ });
107
+ render(created, (a) => success(`Created agent ${a.name} (${a.id})`));
108
+ });
109
+ agent
110
+ .command('update <id>')
111
+ .description('Update an agent')
112
+ .option('-n, --name <name>', 'new name')
113
+ .option('-d, --description <text>', 'new description')
114
+ .option('-i, --instructions <text>', 'new system prompt (@file / - supported)')
115
+ .option('-m, --model <name>', 'new model')
116
+ .option('--visibility <v>', 'private | unlist | public')
117
+ .option('-c, --category <name>', 'category')
118
+ .option('-t, --tag <tag...>', 'replace tags')
119
+ .option('--greeting <text>', 'opening message')
120
+ .option('--set <key=value...>', 'arbitrary field')
121
+ .action(async (id, opts) => {
122
+ const client = new WjiClient(globals());
123
+ const updated = await client.put(`/ai/agent/list/${id}`, {
124
+ name: opts.name,
125
+ description: opts.description,
126
+ instructions: opts.instructions ? await readValue(opts.instructions) : undefined,
127
+ model: opts.model,
128
+ visibility: opts.visibility,
129
+ category: opts.category,
130
+ tags: opts.tag,
131
+ greeting: opts.greeting,
132
+ ...parseKeyValues(opts.set),
133
+ });
134
+ render(updated, () => success(`Updated ${id}`));
135
+ });
136
+ agent
137
+ .command('rm <id>')
138
+ .description('Delete an agent')
139
+ .option('-y, --yes', 'skip confirmation')
140
+ .action(async (id, opts) => {
141
+ const client = new WjiClient(globals());
142
+ const a = await client.get(`/ai/agent/list/${id}`).catch(() => ({ name: id }));
143
+ if (!(await confirm(`Delete agent "${a.name}" (${id})?`, opts.yes)))
144
+ return info('Aborted');
145
+ const result = await client.delete(`/ai/agent/list/${id}`);
146
+ render(result, () => success(`Deleted ${id}`));
147
+ });
148
+ agent
149
+ .command('files <id>')
150
+ .description('List files attached to an agent')
151
+ .action(async (id) => {
152
+ const client = new WjiClient(globals());
153
+ const files = await client.get(`/ai/agent/list/${id}/files`);
154
+ render(files, (rows) => table(rows || [], [
155
+ { header: 'id', value: (f) => f.id },
156
+ { header: 'name', value: (f) => f.name },
157
+ { header: 'type', value: (f) => f.type },
158
+ ]));
159
+ });
160
+ agent
161
+ .command('categories')
162
+ .description('List agent categories')
163
+ .option('--mine', 'only categories used by your agents')
164
+ .action(async (opts) => {
165
+ const client = new WjiClient(globals());
166
+ const list = await client.get(opts.mine ? '/ai/agent/categories/my' : '/ai/agent/categories');
167
+ render(list, (rows) => rows.filter(Boolean).forEach((c) => process.stdout.write(c + '\n')));
168
+ });
169
+ agent
170
+ .command('tags')
171
+ .description('List agent tags')
172
+ .option('--mine', 'only tags used by your agents')
173
+ .action(async (opts) => {
174
+ const client = new WjiClient(globals());
175
+ const list = await client.get(opts.mine ? '/ai/agent/tags/my' : '/ai/agent/tags');
176
+ render(list, (rows) => rows.filter(Boolean).forEach((t) => process.stdout.write(t + '\n')));
177
+ });
178
+ agent
179
+ .command('recommend')
180
+ .description('Show recommended agents')
181
+ .action(async () => {
182
+ const client = new WjiClient(globals());
183
+ const rows = await client.get('/ai/agent/recommend');
184
+ render(rows, (list) => table(list || [], [
185
+ { header: 'id', value: (a) => a.id },
186
+ { header: 'name', value: (a) => a.name },
187
+ { header: 'title', value: (a) => truncate(a.recommendTitle, 40) },
188
+ ]));
189
+ });
190
+ agent
191
+ .command('skills <id>')
192
+ .description('Show the skills attached to an agent')
193
+ .action(async (id) => {
194
+ const client = new WjiClient(globals());
195
+ const a = await client.get(`/ai/agent/list/${id}`);
196
+ render(a.skills || [], (rows) => {
197
+ if (!rows.length)
198
+ return info(color.dim('(no skills attached)'));
199
+ table(rows, [
200
+ { header: 'kind', value: (r) => r.kind },
201
+ { header: 'ref', value: (r) => (r.kind === 'system' ? r.key : r.id) },
202
+ { header: 'name', value: (r) => r.name },
203
+ ]);
204
+ });
205
+ });
206
+ agent
207
+ .command('skill-add <id> <ref...>')
208
+ .description('Attach skills to an agent (refs: system:<key> or user:<skillId>)')
209
+ .action(async (id, refs) => {
210
+ const client = new WjiClient(globals());
211
+ const a = await client.get(`/ai/agent/list/${id}`);
212
+ const existing = (a.skills || []);
213
+ const seen = new Set(existing.map(skillRefLabel));
214
+ const added = [];
215
+ for (const token of refs) {
216
+ const ref = parseSkillRef(token);
217
+ if (seen.has(skillRefLabel(ref)))
218
+ continue;
219
+ existing.push(ref);
220
+ seen.add(skillRefLabel(ref));
221
+ added.push(skillRefLabel(ref));
222
+ }
223
+ const updated = await client.put(`/ai/agent/list/${id}`, { skills: existing });
224
+ render(updated, () => success(added.length ? `Attached ${added.join(', ')}` : 'No new skills to attach'));
225
+ });
226
+ agent
227
+ .command('skill-remove <id> <ref...>')
228
+ .description('Detach skills from 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 drop = new Set(refs.map((t) => skillRefLabel(parseSkillRef(t))));
233
+ const kept = (a.skills || []).filter((r) => !drop.has(skillRefLabel(r)));
234
+ const updated = await client.put(`/ai/agent/list/${id}`, { skills: kept });
235
+ render(updated, () => success(`Removed ${refs.length} skill ref(s)`));
236
+ });
237
+ agent
238
+ .command('versions <id>')
239
+ .description('List an agent\'s version history')
240
+ .action(async (id) => {
241
+ const client = new WjiClient(globals());
242
+ const versions = await client.get(`/ai/agent/list/${id}/versions`);
243
+ render(versions, (rows) => table(rows || [], [
244
+ { header: 'versionId', value: (v) => v.id },
245
+ { header: 'v', value: (v) => v.version },
246
+ { header: 'name', value: (v) => truncate(v.snapshot?.name, 24) },
247
+ { header: 'model', value: (v) => v.snapshot?.model },
248
+ { header: 'comment', value: (v) => truncate(v.comment, 30) },
249
+ { header: 'when', value: (v) => relativeTime(v.createdAt) },
250
+ ]));
251
+ });
252
+ agent
253
+ .command('restore <id> <versionId>')
254
+ .description('Restore an agent to a previous version')
255
+ .action(async (id, versionId) => {
256
+ const client = new WjiClient(globals());
257
+ const result = await client.post(`/ai/agent/list/${id}/versions/${versionId}/restore`);
258
+ render(result, () => success(`Restored ${id} to version ${versionId}`));
259
+ });
260
+ return agent;
261
+ }
262
+ function printAgentTable(result) {
263
+ const rows = result?.data || result || [];
264
+ table(rows, [
265
+ { header: 'id', value: (a) => a.id },
266
+ { header: 'name', value: (a) => truncate(a.name, 28) },
267
+ { header: 'model', value: (a) => a.model },
268
+ { header: 'visibility', value: (a) => a.visibility },
269
+ { header: 'category', value: (a) => a.category },
270
+ { header: 'updated', value: (a) => relativeTime(a.updatedAt) },
271
+ ]);
272
+ if (result?.pagination)
273
+ info(color.dim(`${result.pagination.total} total`));
274
+ }