@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 +242 -0
- package/dist/commands/agent.js +274 -0
- package/dist/commands/asset.js +241 -0
- package/dist/commands/auth.js +216 -0
- package/dist/commands/canvas.js +915 -0
- package/dist/commands/config.js +113 -0
- package/dist/commands/fsnode.js +252 -0
- package/dist/commands/gen.js +366 -0
- package/dist/commands/model.js +134 -0
- package/dist/commands/org.js +85 -0
- package/dist/commands/preset.js +176 -0
- package/dist/commands/profile.js +129 -0
- package/dist/commands/skill.js +272 -0
- package/dist/commands/workspace.js +305 -0
- package/dist/core/client.js +252 -0
- package/dist/core/config.js +95 -0
- package/dist/core/output.js +143 -0
- package/dist/core/prompt.js +118 -0
- package/dist/core/zip.js +87 -0
- package/dist/index.js +94 -0
- package/package.json +49 -0
- package/skill/wji/SKILL.md +398 -0
- package/skill/wji/reference/canvas-recipes.md +179 -0
- package/skill/wji/reference/commands.md +381 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { WjiClient } from '../core/client.js';
|
|
3
|
+
import { DEFAULT_ENDPOINT, configPath, getProfile, readConfig, updateProfile, writeConfig, } from '../core/config.js';
|
|
4
|
+
import { confirm } from '../core/prompt.js';
|
|
5
|
+
import { color, detail, info, render, success, table } from '../core/output.js';
|
|
6
|
+
const REDACTED_KEYS = new Set(['accessToken', 'refreshToken', 'apiKey']);
|
|
7
|
+
function redact(profile) {
|
|
8
|
+
const out = {};
|
|
9
|
+
for (const [k, v] of Object.entries(profile)) {
|
|
10
|
+
out[k] = REDACTED_KEYS.has(k) && typeof v === 'string' ? `${v.slice(0, 8)}…` : v;
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
}
|
|
14
|
+
export function configCommand(globals) {
|
|
15
|
+
const config = new Command('config').description('Manage CLI profiles and defaults');
|
|
16
|
+
config
|
|
17
|
+
.command('show', { isDefault: true })
|
|
18
|
+
.description('Show the active profile (secrets redacted)')
|
|
19
|
+
.action(() => {
|
|
20
|
+
const g = globals();
|
|
21
|
+
const { name, profile } = getProfile(g.profile);
|
|
22
|
+
const view = redact(profile);
|
|
23
|
+
// The endpoint is resolved at request time from flag > env > profile >
|
|
24
|
+
// built-in default, so show what will actually be used, not just what is
|
|
25
|
+
// stored — an unset profile would otherwise print nothing here.
|
|
26
|
+
const client = new WjiClient(g);
|
|
27
|
+
const effective = client.endpoint + (profile.endpoint ? '' : ` ${color.dim('(default)')}`);
|
|
28
|
+
render({ profile: name, path: configPath(), endpoint: client.endpoint, ...view }, () => {
|
|
29
|
+
detail([
|
|
30
|
+
['profile', name],
|
|
31
|
+
['file', configPath()],
|
|
32
|
+
['endpoint', effective],
|
|
33
|
+
...Object.entries(view).filter(([k]) => k !== 'endpoint'),
|
|
34
|
+
['org', client.orgId || 'personal'],
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
config
|
|
39
|
+
.command('set <key> <value>')
|
|
40
|
+
.description('Set a profile field (endpoint, orgId, workspace, locale, apiKey)')
|
|
41
|
+
.action((key, value) => {
|
|
42
|
+
const allowed = ['endpoint', 'orgId', 'workspace', 'locale', 'apiKey', 'email', 'name'];
|
|
43
|
+
if (!allowed.includes(key)) {
|
|
44
|
+
throw new Error(`Unknown key "${key}". Allowed: ${allowed.join(', ')}`);
|
|
45
|
+
}
|
|
46
|
+
updateProfile(globals().profile, { [key]: value === 'null' ? null : value });
|
|
47
|
+
success(`${key} = ${value}`);
|
|
48
|
+
});
|
|
49
|
+
config
|
|
50
|
+
.command('unset <key>')
|
|
51
|
+
.description('Remove a profile field')
|
|
52
|
+
.action((key) => {
|
|
53
|
+
updateProfile(globals().profile, { [key]: undefined });
|
|
54
|
+
success(`Cleared ${key}`);
|
|
55
|
+
});
|
|
56
|
+
config
|
|
57
|
+
.command('profiles')
|
|
58
|
+
.description('List all profiles')
|
|
59
|
+
.action(() => {
|
|
60
|
+
const file = readConfig();
|
|
61
|
+
// Spread first: a profile's own `name` field is the account display name,
|
|
62
|
+
// which must not shadow the profile key.
|
|
63
|
+
const rows = Object.entries(file.profiles).map(([key, p]) => ({ ...p, key }));
|
|
64
|
+
render({ current: file.current, profiles: rows.map((r) => redact(r)) }, () => table(rows, [
|
|
65
|
+
{ header: '', value: (r) => (r.key === file.current ? color.green('*') : ' ') },
|
|
66
|
+
{ header: 'profile', value: (r) => r.key },
|
|
67
|
+
{ header: 'endpoint', value: (r) => r.endpoint || `${DEFAULT_ENDPOINT} (default)` },
|
|
68
|
+
{ header: 'account', value: (r) => r.email },
|
|
69
|
+
{ header: 'org', value: (r) => r.orgId || 'personal' },
|
|
70
|
+
]));
|
|
71
|
+
});
|
|
72
|
+
config
|
|
73
|
+
.command('use <profile>')
|
|
74
|
+
.description('Switch the active profile (creates it when new)')
|
|
75
|
+
.action((name) => {
|
|
76
|
+
const file = readConfig();
|
|
77
|
+
if (!file.profiles[name])
|
|
78
|
+
file.profiles[name] = {};
|
|
79
|
+
file.current = name;
|
|
80
|
+
writeConfig(file);
|
|
81
|
+
success(`Using profile ${name}`);
|
|
82
|
+
});
|
|
83
|
+
config
|
|
84
|
+
.command('rm <profile>')
|
|
85
|
+
.description('Delete a profile')
|
|
86
|
+
.option('-y, --yes', 'skip confirmation')
|
|
87
|
+
.action(async (name, opts) => {
|
|
88
|
+
const file = readConfig();
|
|
89
|
+
if (!file.profiles[name])
|
|
90
|
+
throw new Error(`No such profile: ${name}`);
|
|
91
|
+
const p = file.profiles[name];
|
|
92
|
+
// A profile is often the only copy of an sk_ secret — say so before it goes.
|
|
93
|
+
const holds = [p.apiKey && 'API key', p.refreshToken && 'login'].filter(Boolean).join(' and ');
|
|
94
|
+
const prompt = `Delete profile "${name}"${holds ? ` and the ${holds} stored in it` : ''}?`;
|
|
95
|
+
if (!(await confirm(prompt, opts.yes)))
|
|
96
|
+
return info('Aborted');
|
|
97
|
+
delete file.profiles[name];
|
|
98
|
+
if (file.current === name)
|
|
99
|
+
file.current = Object.keys(file.profiles)[0] || 'default';
|
|
100
|
+
if (!file.profiles[file.current])
|
|
101
|
+
file.profiles[file.current] = {};
|
|
102
|
+
writeConfig(file);
|
|
103
|
+
success(`Deleted profile ${name}`);
|
|
104
|
+
info(color.dim(`Active profile is now ${file.current}`));
|
|
105
|
+
});
|
|
106
|
+
config
|
|
107
|
+
.command('path')
|
|
108
|
+
.description('Print the config file path')
|
|
109
|
+
.action(() => {
|
|
110
|
+
process.stdout.write(configPath() + '\n');
|
|
111
|
+
});
|
|
112
|
+
return config;
|
|
113
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { WjiClient } from '../core/client.js';
|
|
3
|
+
import { confirm, parseKeyValues, readJson, readValue } from '../core/prompt.js';
|
|
4
|
+
import { color, detail, info, relativeTime, render, success, table, truncate } from '../core/output.js';
|
|
5
|
+
import { printTree } from './workspace.js';
|
|
6
|
+
export function fsCommand(globals) {
|
|
7
|
+
const fs = new Command('fs')
|
|
8
|
+
.aliases(['fsnode'])
|
|
9
|
+
.description('Browse and edit the fsnode tree (ideas, datasets, files)');
|
|
10
|
+
fs.command('root [key]')
|
|
11
|
+
.description('Get a root node (defaults to your personal drive; pass a workspace id for its root)')
|
|
12
|
+
.option('-d, --depth <n>', 'expand this many levels')
|
|
13
|
+
.action(async (key, opts) => {
|
|
14
|
+
const client = new WjiClient(globals());
|
|
15
|
+
const path = key ? `/fsnode/root/${key}` : '/fsnode/root';
|
|
16
|
+
const node = await client.get(path, opts.depth ? { depth: Number(opts.depth) } : undefined);
|
|
17
|
+
render(node, (n) => (opts.depth ? printTree(n) : printNode(n)));
|
|
18
|
+
});
|
|
19
|
+
fs.command('ls <id>')
|
|
20
|
+
.description('List child nodes')
|
|
21
|
+
.option('--limit <n>', 'page size')
|
|
22
|
+
.option('--scope <scope>', 'server-side scope filter')
|
|
23
|
+
.action(async (id, opts) => {
|
|
24
|
+
const client = new WjiClient(globals());
|
|
25
|
+
const result = await client.get(`/fsnode/list/${id}/children`, {
|
|
26
|
+
limit: opts.limit ? Number(opts.limit) : undefined,
|
|
27
|
+
scope: opts.scope,
|
|
28
|
+
});
|
|
29
|
+
const rows = Array.isArray(result) ? result : result?.data || [];
|
|
30
|
+
render(result, () => table(rows, [
|
|
31
|
+
{ header: 'id', value: (n) => n.id },
|
|
32
|
+
{ header: 'type', value: (n) => (n.type === 'folder' ? color.blue('dir') : 'file') },
|
|
33
|
+
{ header: 'name', value: (n) => truncate(n.name, 32) },
|
|
34
|
+
{ header: 'nickname', value: (n) => truncate(n.nickname, 24) },
|
|
35
|
+
{ header: 'mimetype', value: (n) => n.mimetype },
|
|
36
|
+
{ header: 'updated', value: (n) => relativeTime(n.updatedAt) },
|
|
37
|
+
]));
|
|
38
|
+
});
|
|
39
|
+
fs.command('tree <id>')
|
|
40
|
+
.description('Print the subtree')
|
|
41
|
+
.option('-d, --depth <n>', 'levels to expand')
|
|
42
|
+
.action(async (id, opts) => {
|
|
43
|
+
const client = new WjiClient(globals());
|
|
44
|
+
const node = await client.get(`/fsnode/list/${id}/tree`, opts.depth ? { depth: Number(opts.depth) } : undefined);
|
|
45
|
+
render(node, (n) => printTree(n));
|
|
46
|
+
});
|
|
47
|
+
fs.command('get <id>')
|
|
48
|
+
.description('Show one node (comma-separate ids for a batch)')
|
|
49
|
+
.option('--scope <scope>', 'server-side scope filter')
|
|
50
|
+
.action(async (id, opts) => {
|
|
51
|
+
const client = new WjiClient(globals());
|
|
52
|
+
const node = await client.get(`/fsnode/list/${id}`, { scope: opts.scope });
|
|
53
|
+
render(node, (n) => (Array.isArray(n) ? n.forEach(printNode) : printNode(n)));
|
|
54
|
+
});
|
|
55
|
+
fs.command('path <id>')
|
|
56
|
+
.description('Print the ancestor chain from root to this node')
|
|
57
|
+
.action(async (id) => {
|
|
58
|
+
const client = new WjiClient(globals());
|
|
59
|
+
const nodes = await client.get(`/fsnode/list/${id}/path`);
|
|
60
|
+
render(nodes, (rows) => {
|
|
61
|
+
process.stdout.write(rows.map((n) => n.nickname || n.name).join(' / ') + '\n');
|
|
62
|
+
info(color.dim(rows.map((n) => n.id).join(' / ')));
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
fs.command('mkdir <parent> <name>')
|
|
66
|
+
.description('Create a folder')
|
|
67
|
+
.option('--nickname <text>', 'display name')
|
|
68
|
+
.option('--content <json>', 'content JSON, @file or - for stdin')
|
|
69
|
+
.action(async (parent, name, opts) => {
|
|
70
|
+
const client = new WjiClient(globals());
|
|
71
|
+
const created = await client.post('/fsnode/create', {
|
|
72
|
+
parent,
|
|
73
|
+
name,
|
|
74
|
+
type: 'folder',
|
|
75
|
+
nickname: opts.nickname,
|
|
76
|
+
content: await readJson(opts.content),
|
|
77
|
+
});
|
|
78
|
+
render(created, (n) => success(`Created folder ${n.name} (${n.id})`));
|
|
79
|
+
});
|
|
80
|
+
fs.command('touch <parent> <name>')
|
|
81
|
+
.description('Create a file node')
|
|
82
|
+
.option('--mimetype <type>', 'e.g. workspace/dataset')
|
|
83
|
+
.option('--nickname <text>', 'display name')
|
|
84
|
+
.option('--asset <assetId>', 'attach an asset as the file')
|
|
85
|
+
.option('--content <json>', 'content JSON, @file or - for stdin')
|
|
86
|
+
.action(async (parent, name, opts) => {
|
|
87
|
+
const client = new WjiClient(globals());
|
|
88
|
+
const created = await client.post('/fsnode/create', {
|
|
89
|
+
parent,
|
|
90
|
+
name,
|
|
91
|
+
type: 'file',
|
|
92
|
+
mimetype: opts.mimetype,
|
|
93
|
+
nickname: opts.nickname,
|
|
94
|
+
file: opts.asset,
|
|
95
|
+
content: await readJson(opts.content),
|
|
96
|
+
});
|
|
97
|
+
render(created, (n) => success(`Created file ${n.name} (${n.id})`));
|
|
98
|
+
});
|
|
99
|
+
fs.command('set <id>')
|
|
100
|
+
.description('Update a node')
|
|
101
|
+
.option('-n, --name <name>', 'new name')
|
|
102
|
+
.option('--nickname <text>', 'new display name')
|
|
103
|
+
.option('--content <json>', 'replace content JSON (@file / - supported)')
|
|
104
|
+
.option('--tags <tag...>', 'replace tags')
|
|
105
|
+
.option('--images <assetId...>', 'replace the image asset list')
|
|
106
|
+
.option('--icon <assetId>', 'set the icon asset')
|
|
107
|
+
.option('--asset <assetId>', 'set the attached file asset (use "null" to clear)')
|
|
108
|
+
.option('--sort <n>', 'sort order')
|
|
109
|
+
.option('--set <key=value...>', 'arbitrary field, dotted keys allowed')
|
|
110
|
+
.action(async (id, opts) => {
|
|
111
|
+
const client = new WjiClient(globals());
|
|
112
|
+
const body = {
|
|
113
|
+
name: opts.name,
|
|
114
|
+
nickname: opts.nickname,
|
|
115
|
+
content: await readJson(opts.content),
|
|
116
|
+
tags: opts.tags,
|
|
117
|
+
images: opts.images,
|
|
118
|
+
icon: opts.icon,
|
|
119
|
+
file: opts.asset === 'null' ? null : opts.asset,
|
|
120
|
+
sort: opts.sort != null ? Number(opts.sort) : undefined,
|
|
121
|
+
...parseKeyValues(opts.set),
|
|
122
|
+
};
|
|
123
|
+
const updated = await client.patch(`/fsnode/list/${id}`, body);
|
|
124
|
+
render(updated, (n) => success(`Updated ${n?.id || id}`));
|
|
125
|
+
});
|
|
126
|
+
fs.command('describe <id> <text>')
|
|
127
|
+
.description('Set content.description (accepts @file or - for stdin)')
|
|
128
|
+
.action(async (id, text) => {
|
|
129
|
+
const client = new WjiClient(globals());
|
|
130
|
+
const node = await client.getFresh(`/fsnode/list/${id}`);
|
|
131
|
+
const description = await readValue(text);
|
|
132
|
+
const updated = await client.patch(`/fsnode/list/${id}`, {
|
|
133
|
+
content: { ...(node.content || {}), description },
|
|
134
|
+
});
|
|
135
|
+
render(updated, () => success(`Updated description of ${id}`));
|
|
136
|
+
});
|
|
137
|
+
fs.command('rm <id>')
|
|
138
|
+
.description('Delete a node and its subtree (soft delete)')
|
|
139
|
+
.option('-y, --yes', 'skip confirmation')
|
|
140
|
+
.action(async (id, opts) => {
|
|
141
|
+
const client = new WjiClient(globals());
|
|
142
|
+
const node = await client.get(`/fsnode/list/${id}`).catch(() => ({ name: id }));
|
|
143
|
+
if (!(await confirm(`Delete "${node.name}" (${id}) and everything under it?`, opts.yes))) {
|
|
144
|
+
return info('Aborted');
|
|
145
|
+
}
|
|
146
|
+
const result = await client.delete(`/fsnode/list/${id}`);
|
|
147
|
+
render(result, () => success(`Deleted ${id}`));
|
|
148
|
+
});
|
|
149
|
+
fs.command('sort <parent> <ids...>')
|
|
150
|
+
.description('Reorder children of a folder')
|
|
151
|
+
.action(async (parent, ids) => {
|
|
152
|
+
const client = new WjiClient(globals());
|
|
153
|
+
const result = await client.patch(`/fsnode/list/${parent}/children/sort`, { ids });
|
|
154
|
+
render(result, () => success(`Reordered ${ids.length} children`));
|
|
155
|
+
});
|
|
156
|
+
fs.command('batch <json>')
|
|
157
|
+
.description('Batch update nodes: [{id, name?, content?, sort?, file?}] (@file / - supported)')
|
|
158
|
+
.action(async (json) => {
|
|
159
|
+
const client = new WjiClient(globals());
|
|
160
|
+
const items = await readJson(json);
|
|
161
|
+
const result = await client.patch('/fsnode/batch', {
|
|
162
|
+
items: Array.isArray(items) ? items : items.items,
|
|
163
|
+
});
|
|
164
|
+
render(result, () => success('Batch update applied'));
|
|
165
|
+
});
|
|
166
|
+
fs.command('used-space')
|
|
167
|
+
.description('Show storage used by your nodes')
|
|
168
|
+
.action(async () => {
|
|
169
|
+
const client = new WjiClient(globals());
|
|
170
|
+
const usage = await client.get('/fsnode/drive/used-space');
|
|
171
|
+
render(usage, (u) => typeof u === 'object' ? detail(Object.entries(u)) : process.stdout.write(`${u}\n`));
|
|
172
|
+
});
|
|
173
|
+
return fs;
|
|
174
|
+
}
|
|
175
|
+
/** Dataset nodes are fsnodes with a `workspace/dataset` mimetype. */
|
|
176
|
+
export function datasetCommand(globals) {
|
|
177
|
+
const dataset = new Command('dataset').description('Dataset nodes (ideas: character, location, prop, …)');
|
|
178
|
+
dataset
|
|
179
|
+
.command('new <parent> <name>')
|
|
180
|
+
.description('Create a dataset node under a folder')
|
|
181
|
+
.option('-t, --type <type>', 'outline | screenplay | character | location | prop | outfit | scene | shot | other', 'other')
|
|
182
|
+
.option('-d, --description <text>', 'description (@file / - supported)')
|
|
183
|
+
.option('--images <assetId...>', 'attach image assets')
|
|
184
|
+
.option('--nickname <text>', 'display name')
|
|
185
|
+
.action(async (parent, name, opts) => {
|
|
186
|
+
const client = new WjiClient(globals());
|
|
187
|
+
const created = await client.post('/fsnode/create', {
|
|
188
|
+
parent,
|
|
189
|
+
name,
|
|
190
|
+
type: 'file',
|
|
191
|
+
mimetype: 'workspace/dataset',
|
|
192
|
+
nickname: opts.nickname,
|
|
193
|
+
images: opts.images,
|
|
194
|
+
content: {
|
|
195
|
+
type: opts.type,
|
|
196
|
+
description: opts.description ? await readValue(opts.description) : undefined,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
render(created, (n) => success(`Created dataset ${n.name} (${n.id})`));
|
|
200
|
+
});
|
|
201
|
+
dataset
|
|
202
|
+
.command('list <folderId>')
|
|
203
|
+
.description('List dataset nodes in a folder')
|
|
204
|
+
.action(async (folderId) => {
|
|
205
|
+
const client = new WjiClient(globals());
|
|
206
|
+
const result = await client.get(`/fsnode/list/${folderId}/children`);
|
|
207
|
+
const rows = (Array.isArray(result) ? result : result?.data || []).filter((n) => n.mimetype === 'workspace/dataset' || n.type === 'file');
|
|
208
|
+
render(rows, () => table(rows, [
|
|
209
|
+
{ header: 'id', value: (n) => n.id },
|
|
210
|
+
{ header: 'name', value: (n) => truncate(n.name, 28) },
|
|
211
|
+
{ header: 'kind', value: (n) => n.content?.type },
|
|
212
|
+
{ header: 'images', value: (n) => (n.images || []).length },
|
|
213
|
+
{ header: 'description', value: (n) => truncate(n.content?.description, 40) },
|
|
214
|
+
]));
|
|
215
|
+
});
|
|
216
|
+
dataset
|
|
217
|
+
.command('get <key>')
|
|
218
|
+
.description('Read a vector-store dataset record by key')
|
|
219
|
+
.action(async (key) => {
|
|
220
|
+
const client = new WjiClient(globals());
|
|
221
|
+
const result = await client.get(`/dataset/list/${key}`);
|
|
222
|
+
render(result, (d) => (d ? detail(Object.entries(d)) : info('Not found')));
|
|
223
|
+
});
|
|
224
|
+
dataset
|
|
225
|
+
.command('rm <key>')
|
|
226
|
+
.description('Delete a vector-store dataset record by key')
|
|
227
|
+
.option('-y, --yes', 'skip confirmation')
|
|
228
|
+
.action(async (key, opts) => {
|
|
229
|
+
if (!(await confirm(`Delete dataset record ${key}?`, opts.yes)))
|
|
230
|
+
return info('Aborted');
|
|
231
|
+
const client = new WjiClient(globals());
|
|
232
|
+
const result = await client.delete(`/dataset/list/${key}`);
|
|
233
|
+
render(result, () => success(`Deleted ${key}`));
|
|
234
|
+
});
|
|
235
|
+
return dataset;
|
|
236
|
+
}
|
|
237
|
+
function printNode(n) {
|
|
238
|
+
detail([
|
|
239
|
+
['id', n.id],
|
|
240
|
+
['name', n.name],
|
|
241
|
+
['nickname', n.nickname],
|
|
242
|
+
['type', n.type],
|
|
243
|
+
['mimetype', n.mimetype],
|
|
244
|
+
['parent', n.parent?.id || n.parent],
|
|
245
|
+
['workspace', n.workspace?.id || n.workspace],
|
|
246
|
+
['tags', (n.tags || []).join(', ')],
|
|
247
|
+
['images', (n.images || []).map((a) => a?.id || a).join(', ')],
|
|
248
|
+
['file', n.file?.id || n.file],
|
|
249
|
+
['content', n.content],
|
|
250
|
+
['updatedAt', n.updatedAt],
|
|
251
|
+
]);
|
|
252
|
+
}
|