@wji.ai/cli 0.7.0 → 0.7.1
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 +4 -1
- package/dist/index.js +7162 -76
- package/package.json +6 -9
- package/dist/commands/agent.js +0 -294
- package/dist/commands/asset.js +0 -241
- package/dist/commands/auth.js +0 -216
- package/dist/commands/backreel-enquiry.js +0 -171
- package/dist/commands/backreel-episode.js +0 -243
- package/dist/commands/backreel-fund.js +0 -299
- package/dist/commands/backreel.js +0 -443
- package/dist/commands/canvas.js +0 -1706
- package/dist/commands/class.js +0 -395
- package/dist/commands/config.js +0 -113
- package/dist/commands/crew.js +0 -205
- package/dist/commands/fsnode.js +0 -288
- package/dist/commands/gen.js +0 -356
- package/dist/commands/model.js +0 -136
- package/dist/commands/npc-album.js +0 -201
- package/dist/commands/npc-live.js +0 -316
- package/dist/commands/npc.js +0 -437
- package/dist/commands/org.js +0 -85
- package/dist/commands/preset.js +0 -176
- package/dist/commands/profile.js +0 -129
- package/dist/commands/skill.js +0 -272
- package/dist/commands/workspace.js +0 -470
- package/dist/core/archive.js +0 -95
- package/dist/core/backreel.js +0 -23
- package/dist/core/client.js +0 -269
- package/dist/core/config.js +0 -95
- package/dist/core/crew.js +0 -546
- package/dist/core/image.js +0 -106
- package/dist/core/members.js +0 -54
- package/dist/core/npc.js +0 -108
- package/dist/core/output.js +0 -174
- package/dist/core/prompt.js +0 -118
- package/dist/core/vod.js +0 -149
- package/dist/core/zip.js +0 -87
package/dist/commands/preset.js
DELETED
|
@@ -1,176 +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, render, success, table, truncate } from '../core/output.js';
|
|
5
|
-
const CATEGORIES = ['character', 'storyboard', 'multi_angle', 'composition', 'utility'];
|
|
6
|
-
const SORTS = ['weight', 'popular', 'latest', 'updated'];
|
|
7
|
-
export function presetCommand(globals) {
|
|
8
|
-
const preset = new Command('preset')
|
|
9
|
-
.aliases(['prompt'])
|
|
10
|
-
.description('Manage prompt presets');
|
|
11
|
-
const listOptions = (cmd) => cmd
|
|
12
|
-
.option('-k, --keyword <text>', 'search text')
|
|
13
|
-
.option('-t, --tag <tags>', 'comma-separated tags, all must match')
|
|
14
|
-
.option('-s, --sort <sort>', SORTS.join(' | '))
|
|
15
|
-
.option('--page <n>', 'page number', '1')
|
|
16
|
-
.option('--limit <n>', 'page size', '20');
|
|
17
|
-
const query = (client, opts, workspace) => ({
|
|
18
|
-
keyword: opts.keyword,
|
|
19
|
-
tag: opts.tag,
|
|
20
|
-
sortBy: opts.sort,
|
|
21
|
-
workspaceId: workspace,
|
|
22
|
-
page: Number(opts.page),
|
|
23
|
-
limit: Number(opts.limit),
|
|
24
|
-
});
|
|
25
|
-
listOptions(preset
|
|
26
|
-
.command('list', { isDefault: true })
|
|
27
|
-
.description('Presets you can use here (yours + public + workspace)')
|
|
28
|
-
.option('-w, --workspace <id>', 'include workspace presets')).action(async (opts) => {
|
|
29
|
-
const client = new WjiClient(globals());
|
|
30
|
-
const result = await client.get('/prompt-preset/search/usable', query(client, opts, client.workspace(opts.workspace)));
|
|
31
|
-
render(result, (r) => printPresets(r));
|
|
32
|
-
});
|
|
33
|
-
listOptions(preset.command('mine').description('Presets you created')).action(async (opts) => {
|
|
34
|
-
const client = new WjiClient(globals());
|
|
35
|
-
const result = await client.get('/prompt-preset/search/my', query(client, opts));
|
|
36
|
-
render(result, (r) => printPresets(r));
|
|
37
|
-
});
|
|
38
|
-
listOptions(preset.command('public').description('Public preset library')).action(async (opts) => {
|
|
39
|
-
const client = new WjiClient(globals());
|
|
40
|
-
const result = await client.get('/prompt-preset/search/public', query(client, opts));
|
|
41
|
-
render(result, (r) => printPresets(r));
|
|
42
|
-
});
|
|
43
|
-
listOptions(preset
|
|
44
|
-
.command('workspace [id]')
|
|
45
|
-
.description('Presets belonging to a workspace')).action(async (id, opts) => {
|
|
46
|
-
const client = new WjiClient(globals());
|
|
47
|
-
const result = await client.get('/prompt-preset/search/workspace', query(client, opts, client.requireWorkspace(id)));
|
|
48
|
-
render(result, (r) => printPresets(r));
|
|
49
|
-
});
|
|
50
|
-
preset
|
|
51
|
-
.command('get <id>')
|
|
52
|
-
.description('Show a preset with its full prompt')
|
|
53
|
-
.action(async (id) => {
|
|
54
|
-
const client = new WjiClient(globals());
|
|
55
|
-
const p = await client.get(`/prompt-preset/list/${id}`);
|
|
56
|
-
render(p, () => detail([
|
|
57
|
-
['id', p.id],
|
|
58
|
-
['name', p.name],
|
|
59
|
-
['description', p.description],
|
|
60
|
-
['category', p.category],
|
|
61
|
-
['visibility', p.visibility],
|
|
62
|
-
['tags', (p.tags || []).join(', ')],
|
|
63
|
-
['workspace', p.workspace?.id || p.workspace],
|
|
64
|
-
['forkedFrom', p.forkedFrom?.id || p.forkedFrom],
|
|
65
|
-
['forkCount', p.forkCount],
|
|
66
|
-
['prompt', p.prompt],
|
|
67
|
-
]));
|
|
68
|
-
});
|
|
69
|
-
preset
|
|
70
|
-
.command('create <name>')
|
|
71
|
-
.description('Create a preset')
|
|
72
|
-
.requiredOption('-p, --prompt <text>', 'prompt body (@file / - supported)')
|
|
73
|
-
.option('-d, --description <text>', 'description')
|
|
74
|
-
.option('-c, --category <category>', CATEGORIES.join(' | '), 'utility')
|
|
75
|
-
.option('-t, --tag <tag...>', 'tags')
|
|
76
|
-
.option('--visibility <v>', 'private | public', 'private')
|
|
77
|
-
.option('-w, --workspace <id>', 'attach to a workspace')
|
|
78
|
-
.option('--set <key=value...>', 'arbitrary field')
|
|
79
|
-
.action(async (name, opts) => {
|
|
80
|
-
const client = new WjiClient(globals());
|
|
81
|
-
const created = await client.post('/prompt-preset/create', {
|
|
82
|
-
name,
|
|
83
|
-
prompt: await readValue(opts.prompt),
|
|
84
|
-
description: opts.description,
|
|
85
|
-
category: opts.category,
|
|
86
|
-
tags: opts.tag,
|
|
87
|
-
visibility: opts.visibility,
|
|
88
|
-
workspaceId: client.workspace(opts.workspace),
|
|
89
|
-
...parseKeyValues(opts.set),
|
|
90
|
-
});
|
|
91
|
-
render(created, (p) => success(`Created preset ${p.name} (${p.id})`));
|
|
92
|
-
});
|
|
93
|
-
preset
|
|
94
|
-
.command('update <id>')
|
|
95
|
-
.description('Update a preset')
|
|
96
|
-
.option('-n, --name <name>', 'new name')
|
|
97
|
-
.option('-p, --prompt <text>', 'new prompt body (@file / - supported)')
|
|
98
|
-
.option('-d, --description <text>', 'new description')
|
|
99
|
-
.option('-c, --category <category>', CATEGORIES.join(' | '))
|
|
100
|
-
.option('-t, --tag <tag...>', 'replace tags')
|
|
101
|
-
.option('--visibility <v>', 'private | public')
|
|
102
|
-
.option('--set <key=value...>', 'arbitrary field')
|
|
103
|
-
.action(async (id, opts) => {
|
|
104
|
-
const client = new WjiClient(globals());
|
|
105
|
-
// The endpoint rebuilds every field from the body, so an omitted field
|
|
106
|
-
// arrives as undefined and is rejected. Merge onto the current preset to
|
|
107
|
-
// keep `update` partial from the caller's point of view.
|
|
108
|
-
const current = await client.getFresh(`/prompt-preset/list/${id}`);
|
|
109
|
-
const updated = await client.put(`/prompt-preset/list/${id}`, {
|
|
110
|
-
name: opts.name ?? current.name,
|
|
111
|
-
prompt: opts.prompt ? await readValue(opts.prompt) : current.prompt,
|
|
112
|
-
description: opts.description ?? current.description,
|
|
113
|
-
category: opts.category ?? current.category,
|
|
114
|
-
tags: opts.tag ?? current.tags,
|
|
115
|
-
visibility: opts.visibility ?? current.visibility,
|
|
116
|
-
...parseKeyValues(opts.set),
|
|
117
|
-
});
|
|
118
|
-
render(updated, () => success(`Updated ${id}`));
|
|
119
|
-
});
|
|
120
|
-
preset
|
|
121
|
-
.command('rm <id>')
|
|
122
|
-
.description('Delete a preset')
|
|
123
|
-
.option('-y, --yes', 'skip confirmation')
|
|
124
|
-
.action(async (id, opts) => {
|
|
125
|
-
if (!(await confirm(`Delete preset ${id}?`, opts.yes)))
|
|
126
|
-
return info('Aborted');
|
|
127
|
-
const client = new WjiClient(globals());
|
|
128
|
-
const result = await client.delete(`/prompt-preset/list/${id}`);
|
|
129
|
-
render(result, () => success(`Deleted ${id}`));
|
|
130
|
-
});
|
|
131
|
-
preset
|
|
132
|
-
.command('fork <id>')
|
|
133
|
-
.description('Fork a preset into your own library')
|
|
134
|
-
.action(async (id) => {
|
|
135
|
-
const client = new WjiClient(globals());
|
|
136
|
-
const forked = await client.post(`/prompt-preset/fork/${id}`);
|
|
137
|
-
render(forked, (p) => success(`Forked to ${p.id}`));
|
|
138
|
-
});
|
|
139
|
-
preset
|
|
140
|
-
.command('copy <id> [workspaceId]')
|
|
141
|
-
.description('Copy a preset into a workspace')
|
|
142
|
-
.action(async (id, workspaceId) => {
|
|
143
|
-
const client = new WjiClient(globals());
|
|
144
|
-
const copied = await client.post(`/prompt-preset/copy-to-workspace/${id}`, {
|
|
145
|
-
workspaceId: client.requireWorkspace(workspaceId),
|
|
146
|
-
});
|
|
147
|
-
render(copied, (p) => success(`Copied to workspace as ${p.id}`));
|
|
148
|
-
});
|
|
149
|
-
preset
|
|
150
|
-
.command('tags')
|
|
151
|
-
.description('List preset tags in a scope')
|
|
152
|
-
.option('-s, --scope <scope>', 'my | public | workspace', 'public')
|
|
153
|
-
.option('-w, --workspace <id>', 'workspace when scope=workspace')
|
|
154
|
-
.action(async (opts) => {
|
|
155
|
-
const client = new WjiClient(globals());
|
|
156
|
-
const tags = await client.get('/prompt-preset/tags', {
|
|
157
|
-
scope: opts.scope,
|
|
158
|
-
workspaceId: opts.scope === 'workspace' ? client.requireWorkspace(opts.workspace) : undefined,
|
|
159
|
-
});
|
|
160
|
-
render(tags, (rows) => rows.forEach((t) => process.stdout.write(`${t?.tag ?? t}\n`)));
|
|
161
|
-
});
|
|
162
|
-
return preset;
|
|
163
|
-
}
|
|
164
|
-
function printPresets(result) {
|
|
165
|
-
const rows = result?.data || result || [];
|
|
166
|
-
table(rows, [
|
|
167
|
-
{ header: 'id', value: (p) => p.id },
|
|
168
|
-
{ header: 'name', value: (p) => truncate(p.name, 28) },
|
|
169
|
-
{ header: 'category', value: (p) => p.category },
|
|
170
|
-
{ header: 'vis', value: (p) => p.visibility },
|
|
171
|
-
{ header: 'tags', value: (p) => (p.tags || []).join(',') },
|
|
172
|
-
{ header: 'prompt', value: (p) => truncate(p.prompt, 40) },
|
|
173
|
-
]);
|
|
174
|
-
if (result?.pagination)
|
|
175
|
-
info(color.dim(`${result.pagination.total} total`));
|
|
176
|
-
}
|
package/dist/commands/profile.js
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import { WjiClient } from '../core/client.js';
|
|
3
|
-
import { askSecret, parseKeyValues, readValue } from '../core/prompt.js';
|
|
4
|
-
import { color, detail, info, relativeTime, render, success, table, truncate } from '../core/output.js';
|
|
5
|
-
export function profileCommand(globals) {
|
|
6
|
-
const profile = new Command('profile').description('Your account profile, wallet and login history');
|
|
7
|
-
profile
|
|
8
|
-
.command('show', { isDefault: true })
|
|
9
|
-
.description('Show your profile')
|
|
10
|
-
.action(async () => {
|
|
11
|
-
const client = new WjiClient(globals());
|
|
12
|
-
const me = await client.get('/user/me');
|
|
13
|
-
render(me, (u) => detail([
|
|
14
|
-
['id', u.id],
|
|
15
|
-
['name', u.name],
|
|
16
|
-
['handle', u.handle],
|
|
17
|
-
['email', u.email],
|
|
18
|
-
['bio', u.bio],
|
|
19
|
-
['website', u.website],
|
|
20
|
-
['contactEmail', u.contactEmail],
|
|
21
|
-
['phone', u.phone],
|
|
22
|
-
['balance', u.balance],
|
|
23
|
-
['vip', u.vip?.level],
|
|
24
|
-
['avatar', u.avatar?.id || u.avatar],
|
|
25
|
-
['banner', u.banner?.id || u.banner],
|
|
26
|
-
['socialLinks', (u.socialLinks || []).map((s) => `${s.platform}:${s.url}`).join(', ')],
|
|
27
|
-
['createdAt', u.createdAt],
|
|
28
|
-
]));
|
|
29
|
-
});
|
|
30
|
-
profile
|
|
31
|
-
.command('update')
|
|
32
|
-
.description('Update your profile')
|
|
33
|
-
.option('-n, --name <name>', 'display name')
|
|
34
|
-
.option('--handle <handle>', '3-24 chars, letter first, lowercase/digits/underscore')
|
|
35
|
-
.option('--bio <text>', 'bio, max 500 chars (@file / - supported)')
|
|
36
|
-
.option('--email <email>', 'account email')
|
|
37
|
-
.option('--contact-email <email>', 'public contact email')
|
|
38
|
-
.option('--phone <phone>', 'contact phone')
|
|
39
|
-
.option('--website <url>', 'website')
|
|
40
|
-
.option('--avatar <assetId>', 'avatar asset id ("null" to clear)')
|
|
41
|
-
.option('--banner <assetId>', 'banner asset id ("null" to clear)')
|
|
42
|
-
.option('--set <key=value...>', 'arbitrary field')
|
|
43
|
-
.action(async (opts) => {
|
|
44
|
-
const client = new WjiClient(globals());
|
|
45
|
-
const nullable = (v) => (v === 'null' ? null : v);
|
|
46
|
-
const updated = await client.patch('/user/me', {
|
|
47
|
-
name: opts.name,
|
|
48
|
-
handle: nullable(opts.handle),
|
|
49
|
-
bio: opts.bio ? await readValue(opts.bio) : undefined,
|
|
50
|
-
email: opts.email,
|
|
51
|
-
contactEmail: nullable(opts.contactEmail),
|
|
52
|
-
phone: nullable(opts.phone),
|
|
53
|
-
website: nullable(opts.website),
|
|
54
|
-
avatar: nullable(opts.avatar),
|
|
55
|
-
banner: nullable(opts.banner),
|
|
56
|
-
...parseKeyValues(opts.set),
|
|
57
|
-
});
|
|
58
|
-
render(updated, () => success('Profile updated'));
|
|
59
|
-
});
|
|
60
|
-
profile
|
|
61
|
-
.command('password')
|
|
62
|
-
.description('Change your password')
|
|
63
|
-
.option('-p, --password <password>', 'new password (prompted when omitted)')
|
|
64
|
-
.action(async (opts) => {
|
|
65
|
-
const client = new WjiClient(globals());
|
|
66
|
-
const password = opts.password || (await askSecret('New password: '));
|
|
67
|
-
if (password.length < 6)
|
|
68
|
-
throw new Error('Password must be at least 6 characters');
|
|
69
|
-
const result = await client.patch('/user/me/password', { password });
|
|
70
|
-
render(result, () => success('Password changed'));
|
|
71
|
-
});
|
|
72
|
-
profile
|
|
73
|
-
.command('logins')
|
|
74
|
-
.description('Show your last 10 successful logins')
|
|
75
|
-
.action(async () => {
|
|
76
|
-
const client = new WjiClient(globals());
|
|
77
|
-
const rows = await client.get('/user/loginHistory');
|
|
78
|
-
render(rows, (list) => table(list || [], [
|
|
79
|
-
{ header: 'when', value: (l) => relativeTime(l.createdAt) },
|
|
80
|
-
{ header: 'state', value: (l) => l.state },
|
|
81
|
-
{ header: 'ip', value: (l) => l.ip },
|
|
82
|
-
{ header: 'agent', value: (l) => truncate(l.userAgent, 40) },
|
|
83
|
-
]));
|
|
84
|
-
});
|
|
85
|
-
profile
|
|
86
|
-
.command('wallet')
|
|
87
|
-
.description('Show your credit balance and recent ledger entries')
|
|
88
|
-
.option('--limit <n>', 'ledger entries to show', '10')
|
|
89
|
-
.action(async (opts) => {
|
|
90
|
-
const client = new WjiClient(globals());
|
|
91
|
-
const me = await client.get('/user/me');
|
|
92
|
-
const ledgers = await client
|
|
93
|
-
.get('/wallet/ledgers/search', { limit: Number(opts.limit) })
|
|
94
|
-
.catch(() => null);
|
|
95
|
-
render({ balance: me.balance, vip: me.vip, ledgers }, () => {
|
|
96
|
-
detail([
|
|
97
|
-
['balance', me.balance],
|
|
98
|
-
['vip', me.vip?.level],
|
|
99
|
-
]);
|
|
100
|
-
if (ledgers?.data?.length) {
|
|
101
|
-
info('');
|
|
102
|
-
table(ledgers.data, [
|
|
103
|
-
{ header: 'when', value: (l) => relativeTime(l.createdAt) },
|
|
104
|
-
{ header: 'amount', value: (l) => l.amount },
|
|
105
|
-
{ header: 'type', value: (l) => l.type },
|
|
106
|
-
{ header: 'note', value: (l) => truncate(l.description || l.remark, 40) },
|
|
107
|
-
]);
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
profile
|
|
112
|
-
.command('search <keyword>')
|
|
113
|
-
.description('Find other users by name or handle (an exact email address also matches)')
|
|
114
|
-
.option('--limit <n>', 'max results (1-20)', '10')
|
|
115
|
-
.action(async (keyword, opts) => {
|
|
116
|
-
const client = new WjiClient(globals());
|
|
117
|
-
// Public search returns a bare array of {id, name, handle, avatar} — never email.
|
|
118
|
-
const result = await client.get('/user/search', { keyword, limit: Number(opts.limit) });
|
|
119
|
-
render(result, (users) => {
|
|
120
|
-
table(users || [], [
|
|
121
|
-
{ header: 'id', value: (u) => u.id },
|
|
122
|
-
{ header: 'name', value: (u) => u.name },
|
|
123
|
-
{ header: 'handle', value: (u) => u.handle },
|
|
124
|
-
]);
|
|
125
|
-
info(color.dim(`${users?.length ?? 0} found`));
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
return profile;
|
|
129
|
-
}
|
package/dist/commands/skill.js
DELETED
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
import { existsSync, statSync } from 'node:fs';
|
|
2
|
-
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
4
|
-
import { Command } from 'commander';
|
|
5
|
-
import { WjiClient } from '../core/client.js';
|
|
6
|
-
import { confirm } from '../core/prompt.js';
|
|
7
|
-
import { createZip } from '../core/zip.js';
|
|
8
|
-
import { color, detail, info, isJsonMode, printJson, render, success, table, truncate, warn } from '../core/output.js';
|
|
9
|
-
/**
|
|
10
|
-
* Import limits enforced by wji-backend (src/ai/skill/zip.ts, shared.service.ts).
|
|
11
|
-
* Checked locally so a push fails with a clear message instead of a 400 / 413.
|
|
12
|
-
*/
|
|
13
|
-
const SKILL_ZIP_MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
|
|
14
|
-
const SKILL_ZIP_MAX_ENTRIES = 200;
|
|
15
|
-
const SKILL_ZIP_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
16
|
-
const SKILL_ZIP_MAX_TOTAL_BYTES = 20 * 1024 * 1024;
|
|
17
|
-
const mb = (bytes) => `${bytes / (1024 * 1024)}MB`;
|
|
18
|
-
function assertUploadSize(bytes, what) {
|
|
19
|
-
if (bytes > SKILL_ZIP_MAX_UPLOAD_BYTES) {
|
|
20
|
-
throw new Error(`${what} is ${(bytes / (1024 * 1024)).toFixed(1)}MB; the server accepts at most ${mb(SKILL_ZIP_MAX_UPLOAD_BYTES)}.`);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
/** Recursively collect files under a dir as forward-slash relative paths. */
|
|
24
|
-
async function collectFiles(root) {
|
|
25
|
-
const out = [];
|
|
26
|
-
async function walk(dir) {
|
|
27
|
-
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
28
|
-
if (entry.name.startsWith('.'))
|
|
29
|
-
continue;
|
|
30
|
-
const full = join(dir, entry.name);
|
|
31
|
-
if (entry.isDirectory())
|
|
32
|
-
await walk(full);
|
|
33
|
-
else if (entry.isFile())
|
|
34
|
-
out.push(full);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
await walk(root);
|
|
38
|
-
return out;
|
|
39
|
-
}
|
|
40
|
-
/** Read a skill folder into zip entries. The backend keeps only .md files. */
|
|
41
|
-
async function zipSkillDir(dir) {
|
|
42
|
-
const root = resolve(dir);
|
|
43
|
-
if (!existsSync(root) || !statSync(root).isDirectory()) {
|
|
44
|
-
throw new Error(`Not a directory: ${dir}`);
|
|
45
|
-
}
|
|
46
|
-
const files = await collectFiles(root);
|
|
47
|
-
const rel = (f) => relative(root, f).split('\\').join('/');
|
|
48
|
-
const hasSkillMd = files.some((f) => {
|
|
49
|
-
const r = rel(f);
|
|
50
|
-
return r === 'SKILL.md' || r.endsWith('/SKILL.md');
|
|
51
|
-
});
|
|
52
|
-
if (!hasSkillMd) {
|
|
53
|
-
throw new Error(`No SKILL.md found in ${dir} — a skill must contain SKILL.md at its root.`);
|
|
54
|
-
}
|
|
55
|
-
const entries = [];
|
|
56
|
-
const kept = [];
|
|
57
|
-
const skipped = [];
|
|
58
|
-
for (const f of files) {
|
|
59
|
-
const path = rel(f);
|
|
60
|
-
if (path.toLowerCase().endsWith('.md')) {
|
|
61
|
-
entries.push({ path, content: await readFile(f) });
|
|
62
|
-
kept.push(path);
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
skipped.push(path);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
if (entries.length > SKILL_ZIP_MAX_ENTRIES) {
|
|
69
|
-
throw new Error(`${entries.length} markdown files; a skill holds at most ${SKILL_ZIP_MAX_ENTRIES}.`);
|
|
70
|
-
}
|
|
71
|
-
let total = 0;
|
|
72
|
-
for (const entry of entries) {
|
|
73
|
-
const bytes = Buffer.byteLength(entry.content);
|
|
74
|
-
if (bytes > SKILL_ZIP_MAX_FILE_BYTES) {
|
|
75
|
-
throw new Error(`${entry.path} is larger than ${mb(SKILL_ZIP_MAX_FILE_BYTES)}; the server rejects it.`);
|
|
76
|
-
}
|
|
77
|
-
total += bytes;
|
|
78
|
-
}
|
|
79
|
-
if (total > SKILL_ZIP_MAX_TOTAL_BYTES) {
|
|
80
|
-
throw new Error(`Markdown totals ${(total / (1024 * 1024)).toFixed(1)}MB; the server accepts at most ${mb(SKILL_ZIP_MAX_TOTAL_BYTES)}.`);
|
|
81
|
-
}
|
|
82
|
-
const zip = createZip(entries);
|
|
83
|
-
assertUploadSize(zip.length, 'The bundled zip');
|
|
84
|
-
return { zip, kept, skipped };
|
|
85
|
-
}
|
|
86
|
-
export function skillCommand(globals) {
|
|
87
|
-
const skill = new Command('skill').description('Manage agent skills (SKILL.md bundles)');
|
|
88
|
-
// ── system skills ────────────────────────────────────────────────────────
|
|
89
|
-
skill
|
|
90
|
-
.command('system')
|
|
91
|
-
.description('List built-in system skills you can attach to an agent')
|
|
92
|
-
.action(async () => {
|
|
93
|
-
const client = new WjiClient(globals());
|
|
94
|
-
const list = await client.get('/ai/skill/system');
|
|
95
|
-
render(list, (rows) => table(rows || [], [
|
|
96
|
-
{ header: 'key', value: (s) => s.key },
|
|
97
|
-
{ header: 'name', value: (s) => s.name },
|
|
98
|
-
{ header: 'runtime', value: (s) => s.runtime },
|
|
99
|
-
{ header: 'script', value: (s) => (s.script ? 'yes' : '') },
|
|
100
|
-
{ header: 'description', value: (s) => truncate(s.description, 46) },
|
|
101
|
-
]));
|
|
102
|
-
});
|
|
103
|
-
// ── user skills ──────────────────────────────────────────────────────────
|
|
104
|
-
skill
|
|
105
|
-
.command('list', { isDefault: true })
|
|
106
|
-
.description('List your skills')
|
|
107
|
-
.option('--page <n>', 'page number', '1')
|
|
108
|
-
.option('--limit <n>', 'page size', '20')
|
|
109
|
-
.action(async (opts) => {
|
|
110
|
-
const client = new WjiClient(globals());
|
|
111
|
-
const result = await client.get('/ai/skill/search', {
|
|
112
|
-
page: Number(opts.page),
|
|
113
|
-
limit: Number(opts.limit),
|
|
114
|
-
});
|
|
115
|
-
render(result, (r) => {
|
|
116
|
-
table(r.data || [], [
|
|
117
|
-
{ header: 'id', value: (s) => s.id },
|
|
118
|
-
{ header: 'name', value: (s) => truncate(s.name, 28) },
|
|
119
|
-
{ header: 'files', value: (s) => (s.files || []).length },
|
|
120
|
-
{ header: 'visibility', value: (s) => s.visibility },
|
|
121
|
-
{ header: 'description', value: (s) => truncate(s.description, 40) },
|
|
122
|
-
]);
|
|
123
|
-
info(color.dim(`${r.pagination?.total ?? 0} total`));
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
skill
|
|
127
|
-
.command('get <id>')
|
|
128
|
-
.description('Show a skill and its files')
|
|
129
|
-
.action(async (id) => {
|
|
130
|
-
const client = new WjiClient(globals());
|
|
131
|
-
const s = await client.get(`/ai/skill/${id}`);
|
|
132
|
-
render(s, () => {
|
|
133
|
-
detail([
|
|
134
|
-
['id', s.id],
|
|
135
|
-
['name', s.name],
|
|
136
|
-
['description', s.description],
|
|
137
|
-
['visibility', s.visibility],
|
|
138
|
-
['files', (s.files || []).length],
|
|
139
|
-
]);
|
|
140
|
-
for (const f of s.files || []) {
|
|
141
|
-
info(` ${color.cyan(f.path)} ${color.dim(`(${(f.content || '').length} chars)`)}`);
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
skill
|
|
146
|
-
.command('cat <id> [path]')
|
|
147
|
-
.description('Print the content of a skill file (defaults to SKILL.md)')
|
|
148
|
-
.action(async (id, path) => {
|
|
149
|
-
const client = new WjiClient(globals());
|
|
150
|
-
const s = await client.get(`/ai/skill/${id}`);
|
|
151
|
-
const files = s.files || [];
|
|
152
|
-
const target = (path && files.find((f) => f.path === path)) ||
|
|
153
|
-
files.find((f) => f.path === 'SKILL.md' || f.path.endsWith('/SKILL.md')) ||
|
|
154
|
-
files[0];
|
|
155
|
-
if (!target)
|
|
156
|
-
throw new Error('Skill has no files');
|
|
157
|
-
process.stdout.write(target.content + '\n');
|
|
158
|
-
});
|
|
159
|
-
skill
|
|
160
|
-
.command('import <zip>')
|
|
161
|
-
.description('Import a skill from a .zip whose root contains SKILL.md')
|
|
162
|
-
.action(async (zipPath) => {
|
|
163
|
-
const client = new WjiClient(globals());
|
|
164
|
-
if (!existsSync(zipPath))
|
|
165
|
-
throw new Error(`No such file: ${zipPath}`);
|
|
166
|
-
const buffer = await readFile(zipPath);
|
|
167
|
-
assertUploadSize(buffer.length, basename(zipPath));
|
|
168
|
-
const created = await client.uploadBuffer('/ai/skill/import', buffer, basename(zipPath));
|
|
169
|
-
render(created, (s) => success(`Imported skill ${s.name} (${s.id})`));
|
|
170
|
-
});
|
|
171
|
-
skill
|
|
172
|
-
.command('push <dir>')
|
|
173
|
-
.description('Bundle a local skill folder and import it (only .md files are stored)')
|
|
174
|
-
.action(async (dir) => {
|
|
175
|
-
const client = new WjiClient(globals());
|
|
176
|
-
const { zip, kept, skipped } = await zipSkillDir(dir);
|
|
177
|
-
if (skipped.length) {
|
|
178
|
-
warn(`Skipping ${skipped.length} non-markdown file(s): ${skipped.slice(0, 5).join(', ')}${skipped.length > 5 ? '…' : ''}`);
|
|
179
|
-
}
|
|
180
|
-
const created = await client.uploadBuffer('/ai/skill/import', zip, `${basename(resolve(dir))}.zip`);
|
|
181
|
-
if (isJsonMode())
|
|
182
|
-
return printJson(created);
|
|
183
|
-
success(`Imported skill ${created.name} (${created.id})`);
|
|
184
|
-
info(color.dim(`${kept.length} markdown file(s): ${kept.join(', ')}`));
|
|
185
|
-
});
|
|
186
|
-
skill
|
|
187
|
-
.command('pull <id> [dir]')
|
|
188
|
-
.description('Write a skill\'s files to disk (defaults to ./<skill-name>)')
|
|
189
|
-
.option('-f, --force', 'overwrite existing files')
|
|
190
|
-
.action(async (id, dir, opts) => {
|
|
191
|
-
const client = new WjiClient(globals());
|
|
192
|
-
const s = await client.get(`/ai/skill/${id}`);
|
|
193
|
-
const target = resolve(dir || slugify(s.name) || id);
|
|
194
|
-
const written = [];
|
|
195
|
-
for (const f of s.files || []) {
|
|
196
|
-
const dest = join(target, f.path);
|
|
197
|
-
if (existsSync(dest) && !opts.force) {
|
|
198
|
-
throw new Error(`${dest} exists — pass --force to overwrite`);
|
|
199
|
-
}
|
|
200
|
-
await mkdir(dirname(dest), { recursive: true });
|
|
201
|
-
await writeFile(dest, f.content);
|
|
202
|
-
written.push(relative(process.cwd(), dest));
|
|
203
|
-
}
|
|
204
|
-
render({ id, dir: target, files: written }, () => {
|
|
205
|
-
success(`Pulled ${written.length} file(s) to ${relative(process.cwd(), target) || '.'}`);
|
|
206
|
-
for (const w of written)
|
|
207
|
-
info(color.dim(` ${w}`));
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
skill
|
|
211
|
-
.command('update <id>')
|
|
212
|
-
.description('Update a skill\'s metadata, or replace its files from a folder')
|
|
213
|
-
.option('-n, --name <name>', 'new name')
|
|
214
|
-
.option('-d, --description <text>', 'new description')
|
|
215
|
-
.option('--visibility <v>', 'private | unlist')
|
|
216
|
-
.option('--dir <dir>', 'replace files from this skill folder')
|
|
217
|
-
.option('-y, --yes', 'skip the confirmation before --dir replaces files')
|
|
218
|
-
.action(async (id, opts) => {
|
|
219
|
-
const client = new WjiClient(globals());
|
|
220
|
-
const body = {
|
|
221
|
-
name: opts.name,
|
|
222
|
-
description: opts.description,
|
|
223
|
-
visibility: opts.visibility,
|
|
224
|
-
};
|
|
225
|
-
if (opts.dir) {
|
|
226
|
-
const root = resolve(opts.dir);
|
|
227
|
-
const files = await collectFiles(root);
|
|
228
|
-
const rel = (f) => relative(root, f).split('\\').join('/');
|
|
229
|
-
body.files = [];
|
|
230
|
-
for (const f of files) {
|
|
231
|
-
const path = rel(f);
|
|
232
|
-
if (path.toLowerCase().endsWith('.md')) {
|
|
233
|
-
body.files.push({ path, content: await readFile(f, 'utf8') });
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
if (!body.files.some((f) => f.path === 'SKILL.md' || f.path.endsWith('/SKILL.md'))) {
|
|
237
|
-
throw new Error(`No SKILL.md in ${opts.dir}`);
|
|
238
|
-
}
|
|
239
|
-
// --dir swaps the whole file set; files missing from the folder are gone for good.
|
|
240
|
-
const current = await client.get(`/ai/skill/${id}`).catch(() => null);
|
|
241
|
-
const label = current?.name ? `"${current.name}" (${id})` : id;
|
|
242
|
-
const had = Array.isArray(current?.files) ? `its ${current.files.length} file(s)` : 'all of its files';
|
|
243
|
-
const prompt = `Replace ${had} of skill ${label} with ${body.files.length} file(s) from ${opts.dir}?`;
|
|
244
|
-
if (!(await confirm(prompt, opts.yes)))
|
|
245
|
-
return info('Aborted');
|
|
246
|
-
}
|
|
247
|
-
const updated = await client.put(`/ai/skill/${id}`, body);
|
|
248
|
-
render(updated, () => success(`Updated skill ${id}`));
|
|
249
|
-
});
|
|
250
|
-
skill
|
|
251
|
-
.command('rm <id>')
|
|
252
|
-
.description('Delete a skill')
|
|
253
|
-
.option('-y, --yes', 'skip confirmation')
|
|
254
|
-
.action(async (id, opts) => {
|
|
255
|
-
const client = new WjiClient(globals());
|
|
256
|
-
const s = await client.get(`/ai/skill/${id}`).catch(() => ({ name: id }));
|
|
257
|
-
if (!(await confirm(`Delete skill "${s.name}" (${id})?`, opts.yes)))
|
|
258
|
-
return info('Aborted');
|
|
259
|
-
const result = await client.delete(`/ai/skill/${id}`);
|
|
260
|
-
render(result, () => success(`Deleted ${id}`));
|
|
261
|
-
});
|
|
262
|
-
return skill;
|
|
263
|
-
}
|
|
264
|
-
function slugify(name) {
|
|
265
|
-
if (!name)
|
|
266
|
-
return '';
|
|
267
|
-
return name
|
|
268
|
-
.trim()
|
|
269
|
-
.toLowerCase()
|
|
270
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
271
|
-
.replace(/^-+|-+$/g, '');
|
|
272
|
-
}
|