@wji.ai/cli 0.6.1 → 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.
Files changed (49) hide show
  1. package/README.md +23 -3
  2. package/dist/index.js +7162 -76
  3. package/package.json +6 -9
  4. package/skill/backreel/SKILL.md +171 -0
  5. package/skill/backreel/reference/crowdfunding.md +82 -0
  6. package/skill/backreel/reference/enquiries.md +80 -0
  7. package/skill/backreel/reference/episodes.md +104 -0
  8. package/skill/backreel/reference/images.md +76 -0
  9. package/skill/npc-character/SKILL.md +111 -0
  10. package/skill/npc-character/examples/nagi.character.md +117 -0
  11. package/skill/npc-character/examples/nagi.stage.md +34 -0
  12. package/skill/npc-character/examples/yuzu.character.md +110 -0
  13. package/skill/npc-character/examples/yuzu.stage.md +30 -0
  14. package/skill/npc-character/reference/character-craft.md +153 -0
  15. package/skill/npc-character/reference/document-format.md +151 -0
  16. package/skill/npc-character/reference/interview.md +44 -0
  17. package/skill/npc-character/reference/life.md +92 -0
  18. package/skill/npc-character/reference/runtime.md +95 -0
  19. package/skill/npc-character/reference/stage-craft.md +109 -0
  20. package/skill/npc-character/reference/visual-kit.md +81 -0
  21. package/skill/npc-character/reference/voice.md +101 -0
  22. package/skill/wji/SKILL.md +1 -1
  23. package/skill/wji/reference/commands.md +25 -5
  24. package/dist/commands/agent.js +0 -294
  25. package/dist/commands/asset.js +0 -241
  26. package/dist/commands/auth.js +0 -216
  27. package/dist/commands/backreel.js +0 -320
  28. package/dist/commands/canvas.js +0 -1706
  29. package/dist/commands/class.js +0 -395
  30. package/dist/commands/config.js +0 -113
  31. package/dist/commands/crew.js +0 -205
  32. package/dist/commands/fsnode.js +0 -288
  33. package/dist/commands/gen.js +0 -352
  34. package/dist/commands/model.js +0 -136
  35. package/dist/commands/npc.js +0 -413
  36. package/dist/commands/org.js +0 -85
  37. package/dist/commands/preset.js +0 -176
  38. package/dist/commands/profile.js +0 -129
  39. package/dist/commands/skill.js +0 -272
  40. package/dist/commands/workspace.js +0 -470
  41. package/dist/core/archive.js +0 -95
  42. package/dist/core/client.js +0 -252
  43. package/dist/core/config.js +0 -95
  44. package/dist/core/crew.js +0 -546
  45. package/dist/core/members.js +0 -54
  46. package/dist/core/npc.js +0 -108
  47. package/dist/core/output.js +0 -174
  48. package/dist/core/prompt.js +0 -118
  49. package/dist/core/zip.js +0 -87
package/dist/core/npc.js DELETED
@@ -1,108 +0,0 @@
1
- import { isObjectId } from './client.js';
2
- /**
3
- * NPC City residents (NPCs). Search endpoints return raw entities (`owner` is
4
- * an id); get / create / patch return the presented shape (`owner` is
5
- * `{id,name,avatarUrl}`). Everything the CLI prints reads fields both share.
6
- */
7
- export const RESIDENTS = '/npc-city/residents';
8
- export const NPC_STATUSES = ['active', 'debut', 'hiatus'];
9
- export const NPC_VISIBILITIES = ['public', 'private'];
10
- /** Work-history entry types; the server turns anything else into `event`. */
11
- export const NPC_JOB_TYPES = ['concert', 'release', 'collaboration', 'brand', 'event', 'ad', 'appearance'];
12
- /** How people refer to a resident: `NAME (@handle)`. */
13
- export const npcLabel = (r) => (r?.handle ? `${r.name} (${r.handle})` : String(r?.name ?? r?.id ?? ''));
14
- /** Every resident the caller manages in the current org context (`--org`). */
15
- export async function listMyNpcs(client) {
16
- const out = [];
17
- for (let page = 1; page <= 50; page++) {
18
- const res = await client.getFresh(`${RESIDENTS}/mine`, { page, limit: 100 });
19
- out.push(...(res?.data || []));
20
- if (!res?.pagination?.hasNext)
21
- break;
22
- }
23
- return out;
24
- }
25
- /**
26
- * A resident from what a person would type: id, `@handle` (or the handle
27
- * without `@`), exact name, else a unique name/handle prefix among yours; an
28
- * `@handle` that isn't yours falls back to the public directory. Ambiguity is
29
- * an error, never a guess. Returns the presented resident.
30
- */
31
- export async function resolveNpc(client, token) {
32
- const t = token.trim();
33
- if (isObjectId(t))
34
- return client.getFresh(`${RESIDENTS}/${t}`);
35
- const mine = await listMyNpcs(client);
36
- const lower = t.toLowerCase();
37
- const handle = lower.startsWith('@') ? lower : `@${lower}`;
38
- const pick = (r) => client.getFresh(`${RESIDENTS}/${r.id}`);
39
- const byHandle = mine.filter((r) => String(r.handle || '').toLowerCase() === handle);
40
- if (byHandle.length === 1)
41
- return pick(byHandle[0]);
42
- const byName = mine.filter((r) => String(r.name || '').toLowerCase() === lower);
43
- if (byName.length === 1)
44
- return pick(byName[0]);
45
- if (byName.length > 1) {
46
- throw new Error(`Several NPCs are called "${t}": ${byName.map(npcLabel).join(', ')} — use the @handle or id`);
47
- }
48
- const bare = lower.replace(/^@/, '');
49
- const prefix = mine.filter((r) => String(r.name || '').toLowerCase().startsWith(bare) ||
50
- String(r.handle || '').toLowerCase().replace(/^@/, '').startsWith(bare));
51
- if (prefix.length === 1)
52
- return pick(prefix[0]);
53
- if (prefix.length > 1)
54
- throw new Error(`"${t}" matches several NPCs: ${prefix.map(npcLabel).join(', ')}`);
55
- // Someone else's public NPC (e.g. to credit it on a Backreel post).
56
- if (t.startsWith('@')) {
57
- const pub = await client.getFresh(`${RESIDENTS}/by-handle/${encodeURIComponent(t)}`).catch(() => undefined);
58
- if (pub?.id)
59
- return pub;
60
- }
61
- throw new Error(`No NPC "${t}" among yours. See \`wji npc list\`; org-owned NPCs need \`--org <orgId>\`.`);
62
- }
63
- const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
64
- /**
65
- * A job from CLI flags, validated here because the server silently drops
66
- * entries without a title or date and rewrites unknown types to `event`.
67
- * With `base` the flags are merged onto an existing job (`job set`).
68
- */
69
- export function parseJob(opts, base) {
70
- const job = {
71
- type: opts.type ?? base?.type ?? '',
72
- title: opts.title ?? base?.title ?? '',
73
- detail: opts.detail ?? base?.detail ?? '',
74
- date: opts.date ?? base?.date ?? '',
75
- };
76
- const location = opts.location ?? base?.location;
77
- const url = opts.url ?? base?.url;
78
- if (location)
79
- job.location = location;
80
- if (url)
81
- job.url = url;
82
- if (!NPC_JOB_TYPES.includes(job.type)) {
83
- throw new Error(`Job type must be one of ${NPC_JOB_TYPES.join(', ')} (got "${job.type}")`);
84
- }
85
- if (!job.title.trim())
86
- throw new Error('A job needs --title');
87
- if (!DATE_RE.test(job.date))
88
- throw new Error(`A job needs --date as YYYY-MM-DD (got "${job.date}")`);
89
- return job;
90
- }
91
- /** Jobs have no ids: read the array fresh, change it, write it back whole. */
92
- export async function updateJobs(client, id, fn) {
93
- const current = await client.getFresh(`${RESIDENTS}/${id}`);
94
- const jobs = fn([...(current.jobs || [])]);
95
- return client.patch(`${RESIDENTS}/${id}`, { jobs });
96
- }
97
- /** 1-based positions from the command line, checked against the list length. */
98
- export function parsePositions(values, length) {
99
- const out = values.map((v) => {
100
- const n = Number(v);
101
- if (!Number.isInteger(n) || n < 1 || n > length) {
102
- throw new Error(`No #${v} — there ${length === 1 ? 'is 1 entry' : `are ${length} entries`}`);
103
- }
104
- return n - 1;
105
- });
106
- return [...new Set(out)].sort((a, b) => a - b);
107
- }
108
- export const today = () => new Date().toISOString().slice(0, 10);
@@ -1,174 +0,0 @@
1
- const useColor = process.env.NO_COLOR == null && process.env.WJI_NO_COLOR == null && process.stdout.isTTY;
2
- const wrap = (code) => (s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : s);
3
- export const color = {
4
- dim: wrap('2'),
5
- bold: wrap('1'),
6
- red: wrap('31'),
7
- green: wrap('32'),
8
- yellow: wrap('33'),
9
- blue: wrap('34'),
10
- cyan: wrap('36'),
11
- };
12
- /** Global --json: when on, every command prints raw JSON and nothing else. */
13
- let jsonMode = false;
14
- export function setJsonMode(on) {
15
- jsonMode = on;
16
- }
17
- export function isJsonMode() {
18
- return jsonMode;
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
- }
49
- export function printJson(value) {
50
- process.stdout.write(JSON.stringify(value, null, 2) + '\n');
51
- }
52
- /** Print `value` as JSON in --json mode, otherwise run `human`. */
53
- export function render(value, human) {
54
- if (jsonMode)
55
- return printJson(value);
56
- human(value);
57
- }
58
- export function info(message) {
59
- if (!jsonMode)
60
- process.stderr.write(message + '\n');
61
- }
62
- export function success(message) {
63
- if (!jsonMode)
64
- process.stderr.write(color.green('✓ ') + message + '\n');
65
- }
66
- export function warn(message) {
67
- process.stderr.write(color.yellow('! ') + message + '\n');
68
- }
69
- const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
70
- /**
71
- * Terminal cell width. CJK and emoji occupy two columns, so counting code
72
- * points misaligns every table that holds Chinese text.
73
- */
74
- function charWidth(code) {
75
- if (code === 0x200d)
76
- return 0;
77
- if (code >= 0x0300 && code <= 0x036f)
78
- return 0; // combining marks
79
- if (code >= 0xfe00 && code <= 0xfe0f)
80
- return 0; // variation selectors
81
- return (code >= 0x1100 && code <= 0x115f) ||
82
- (code >= 0x2e80 && code <= 0x303e) ||
83
- (code >= 0x3041 && code <= 0x33ff) ||
84
- (code >= 0x3400 && code <= 0x4dbf) ||
85
- (code >= 0x4e00 && code <= 0x9fff) ||
86
- (code >= 0xa000 && code <= 0xa4cf) ||
87
- (code >= 0xa960 && code <= 0xa97f) ||
88
- (code >= 0xac00 && code <= 0xd7a3) ||
89
- (code >= 0xf900 && code <= 0xfaff) ||
90
- (code >= 0xfe10 && code <= 0xfe19) ||
91
- (code >= 0xfe30 && code <= 0xfe6f) ||
92
- (code >= 0xff00 && code <= 0xff60) ||
93
- (code >= 0xffe0 && code <= 0xffe6) ||
94
- (code >= 0x1f300 && code <= 0x1f64f) ||
95
- (code >= 0x1f900 && code <= 0x1f9ff) ||
96
- (code >= 0x20000 && code <= 0x3fffd)
97
- ? 2
98
- : 1;
99
- }
100
- export function displayWidth(text) {
101
- let width = 0;
102
- for (const char of stripAnsi(text))
103
- width += charWidth(char.codePointAt(0));
104
- return width;
105
- }
106
- const visibleLength = displayWidth;
107
- export function table(rows, columns) {
108
- if (!rows.length) {
109
- info(color.dim('(empty)'));
110
- return;
111
- }
112
- const cells = rows.map((row) => columns.map((c) => {
113
- const v = c.value(row);
114
- return v == null ? color.dim('-') : String(v);
115
- }));
116
- const widths = columns.map((c, i) => Math.max(visibleLength(c.header), ...cells.map((r) => visibleLength(r[i]))));
117
- const pad = (s, w) => s + ' '.repeat(Math.max(0, w - displayWidth(s)));
118
- process.stdout.write(columns.map((c, i) => color.dim(pad(c.header.toUpperCase(), widths[i]))).join(' ').trimEnd() +
119
- '\n');
120
- for (const row of cells) {
121
- process.stdout.write(row.map((v, i) => pad(v, widths[i])).join(' ').trimEnd() + '\n');
122
- }
123
- }
124
- /** Key/value block for detail views. */
125
- export function detail(pairs) {
126
- const width = Math.max(...pairs.map(([k]) => displayWidth(k)));
127
- for (const [k, v] of pairs) {
128
- if (v == null || v === '')
129
- continue;
130
- const text = typeof v === 'object' ? JSON.stringify(v) : String(v);
131
- const padded = k + ' '.repeat(Math.max(0, width - displayWidth(k)));
132
- process.stdout.write(`${color.dim(padded)} ${text}\n`);
133
- }
134
- }
135
- /** Truncate to `max` terminal columns, not `max` code points. */
136
- export function truncate(value, max = 48) {
137
- if (value == null)
138
- return null;
139
- const s = String(value).replace(/\s+/g, ' ').trim();
140
- if (displayWidth(s) <= max)
141
- return s;
142
- let out = '';
143
- let width = 0;
144
- for (const char of s) {
145
- const next = width + charWidth(char.codePointAt(0));
146
- if (next > max - 1)
147
- break;
148
- out += char;
149
- width = next;
150
- }
151
- return out + '…';
152
- }
153
- export function relativeTime(value) {
154
- if (!value)
155
- return null;
156
- const then = new Date(value).getTime();
157
- if (Number.isNaN(then))
158
- return null;
159
- const diff = Date.now() - then;
160
- const units = [
161
- [86400000 * 365, 'y'],
162
- [86400000 * 30, 'mo'],
163
- [86400000, 'd'],
164
- [3600000, 'h'],
165
- [60000, 'm'],
166
- ];
167
- for (const [ms, label] of units) {
168
- if (Math.abs(diff) >= ms) {
169
- const n = Math.round(Math.abs(diff) / ms);
170
- return diff >= 0 ? `${n}${label} ago` : `in ${n}${label}`;
171
- }
172
- }
173
- return 'just now';
174
- }
@@ -1,118 +0,0 @@
1
- import { createInterface } from 'node:readline/promises';
2
- import { stdin, stdout } from 'node:process';
3
- const ETX = '\u0003'; // Ctrl-C
4
- const BACKSPACE = '\u007f';
5
- export async function ask(question) {
6
- const rl = createInterface({ input: stdin, output: stdout });
7
- try {
8
- return (await rl.question(question)).trim();
9
- }
10
- finally {
11
- rl.close();
12
- }
13
- }
14
- /** Read a secret without echoing it. Falls back to a normal read on a non-TTY stdin. */
15
- export async function askSecret(question) {
16
- if (!stdin.isTTY)
17
- return ask(question);
18
- stdout.write(question);
19
- stdin.setRawMode(true);
20
- stdin.resume();
21
- stdin.setEncoding('utf8');
22
- return new Promise((resolve, reject) => {
23
- let value = '';
24
- const finish = (fn) => {
25
- stdin.setRawMode(false);
26
- stdin.pause();
27
- stdin.removeListener('data', onData);
28
- stdout.write('\n');
29
- fn();
30
- };
31
- const onData = (chunk) => {
32
- for (const char of chunk) {
33
- if (char === '\r' || char === '\n')
34
- return finish(() => resolve(value));
35
- if (char === ETX)
36
- return finish(() => reject(new Error('Cancelled')));
37
- if (char === BACKSPACE || char === '\b') {
38
- value = value.slice(0, -1);
39
- continue;
40
- }
41
- value += char;
42
- }
43
- };
44
- stdin.on('data', onData);
45
- });
46
- }
47
- export async function confirm(question, force) {
48
- if (force)
49
- return true;
50
- if (!stdin.isTTY) {
51
- throw new Error(`Refusing to continue without confirmation on a non-TTY. Pass --yes. (${question})`);
52
- }
53
- const answer = (await ask(`${question} [y/N] `)).toLowerCase();
54
- return answer === 'y' || answer === 'yes';
55
- }
56
- /** Read a value given inline, as `@file`, or as `-` for stdin. */
57
- export async function readValue(value) {
58
- const { readFile } = await import('node:fs/promises');
59
- if (value === '-') {
60
- const chunks = [];
61
- for await (const chunk of stdin)
62
- chunks.push(chunk);
63
- return Buffer.concat(chunks).toString('utf8');
64
- }
65
- if (value.startsWith('@'))
66
- return readFile(value.slice(1), 'utf8');
67
- return value;
68
- }
69
- /** Parse a JSON option that may be inline JSON, `@file` or `-`. */
70
- export async function readJson(value) {
71
- if (value === undefined)
72
- return undefined;
73
- const text = await readValue(value);
74
- try {
75
- return JSON.parse(text);
76
- }
77
- catch (err) {
78
- throw new Error(`Invalid JSON: ${err.message}`);
79
- }
80
- }
81
- /** Parse repeated `--set key=value` pairs into a nested object. */
82
- export function parseKeyValues(pairs = []) {
83
- const out = {};
84
- for (const pair of pairs) {
85
- const index = pair.indexOf('=');
86
- if (index === -1)
87
- throw new Error(`Expected key=value, got "${pair}"`);
88
- const key = pair.slice(0, index);
89
- const raw = pair.slice(index + 1);
90
- let value = raw;
91
- if (raw === 'true')
92
- value = true;
93
- else if (raw === 'false')
94
- value = false;
95
- else if (raw === 'null')
96
- value = null;
97
- else if (raw !== '' && !Number.isNaN(Number(raw)))
98
- value = Number(raw);
99
- else if (raw.startsWith('{') || raw.startsWith('[')) {
100
- try {
101
- value = JSON.parse(raw);
102
- }
103
- catch {
104
- /* leave it as a string */
105
- }
106
- }
107
- // Dotted keys build nested objects, matching the API's qs allowDots parsing.
108
- const path = key.split('.');
109
- let cursor = out;
110
- for (const segment of path.slice(0, -1)) {
111
- if (typeof cursor[segment] !== 'object' || cursor[segment] === null)
112
- cursor[segment] = {};
113
- cursor = cursor[segment];
114
- }
115
- cursor[path[path.length - 1]] = value;
116
- }
117
- return out;
118
- }
package/dist/core/zip.js DELETED
@@ -1,87 +0,0 @@
1
- /**
2
- * Minimal ZIP writer — "stored" (no compression) entries only.
3
- *
4
- * The backend's skill importer keeps just the `.md` files and re-parses them, so
5
- * compression buys nothing here; storing avoids pulling a zip dependency into an
6
- * otherwise dependency-light CLI. Enough of the spec is implemented to produce a
7
- * archive JSZip (what the backend uses) reads back correctly.
8
- */
9
- const CRC_TABLE = (() => {
10
- const table = new Uint32Array(256);
11
- for (let n = 0; n < 256; n++) {
12
- let c = n;
13
- for (let k = 0; k < 8; k++)
14
- c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
15
- table[n] = c >>> 0;
16
- }
17
- return table;
18
- })();
19
- function crc32(buf) {
20
- let crc = 0xffffffff;
21
- for (let i = 0; i < buf.length; i++)
22
- crc = CRC_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
23
- return (crc ^ 0xffffffff) >>> 0;
24
- }
25
- /** DOS date/time — a fixed, valid timestamp is fine; the backend ignores it. */
26
- function dosDateTime(date = new Date()) {
27
- const time = (date.getHours() << 11) | (date.getMinutes() << 5) | (Math.floor(date.getSeconds() / 2));
28
- const day = (((date.getFullYear() - 1980) & 0x7f) << 9) | ((date.getMonth() + 1) << 5) | date.getDate();
29
- return { time: time & 0xffff, day: day & 0xffff };
30
- }
31
- export function createZip(entries) {
32
- const { time, day } = dosDateTime();
33
- const locals = [];
34
- const centrals = [];
35
- let offset = 0;
36
- for (const entry of entries) {
37
- const nameBuf = Buffer.from(entry.path.replace(/\\/g, '/'), 'utf8');
38
- const data = Buffer.isBuffer(entry.content) ? entry.content : Buffer.from(entry.content, 'utf8');
39
- const crc = crc32(data);
40
- const local = Buffer.alloc(30 + nameBuf.length);
41
- local.writeUInt32LE(0x04034b50, 0); // local file header signature
42
- local.writeUInt16LE(20, 4); // version needed
43
- local.writeUInt16LE(0, 6); // flags
44
- local.writeUInt16LE(0, 8); // method: store
45
- local.writeUInt16LE(time, 10);
46
- local.writeUInt16LE(day, 12);
47
- local.writeUInt32LE(crc, 14);
48
- local.writeUInt32LE(data.length, 18); // compressed size
49
- local.writeUInt32LE(data.length, 22); // uncompressed size
50
- local.writeUInt16LE(nameBuf.length, 26);
51
- local.writeUInt16LE(0, 28); // extra length
52
- nameBuf.copy(local, 30);
53
- locals.push(local, data);
54
- const central = Buffer.alloc(46 + nameBuf.length);
55
- central.writeUInt32LE(0x02014b50, 0); // central directory header signature
56
- central.writeUInt16LE(20, 4); // version made by
57
- central.writeUInt16LE(20, 6); // version needed
58
- central.writeUInt16LE(0, 8); // flags
59
- central.writeUInt16LE(0, 10); // method
60
- central.writeUInt16LE(time, 12);
61
- central.writeUInt16LE(day, 14);
62
- central.writeUInt32LE(crc, 16);
63
- central.writeUInt32LE(data.length, 20);
64
- central.writeUInt32LE(data.length, 24);
65
- central.writeUInt16LE(nameBuf.length, 28);
66
- central.writeUInt16LE(0, 30); // extra length
67
- central.writeUInt16LE(0, 32); // comment length
68
- central.writeUInt16LE(0, 34); // disk number
69
- central.writeUInt16LE(0, 36); // internal attrs
70
- central.writeUInt32LE(0, 38); // external attrs
71
- central.writeUInt32LE(offset, 42); // local header offset
72
- nameBuf.copy(central, 46);
73
- centrals.push(central);
74
- offset += local.length + data.length;
75
- }
76
- const centralDir = Buffer.concat(centrals);
77
- const end = Buffer.alloc(22);
78
- end.writeUInt32LE(0x06054b50, 0); // end of central directory signature
79
- end.writeUInt16LE(0, 4); // disk number
80
- end.writeUInt16LE(0, 6); // central dir start disk
81
- end.writeUInt16LE(entries.length, 8); // entries on this disk
82
- end.writeUInt16LE(entries.length, 10); // total entries
83
- end.writeUInt32LE(centralDir.length, 12);
84
- end.writeUInt32LE(offset, 16); // central dir offset
85
- end.writeUInt16LE(0, 20); // comment length
86
- return Buffer.concat([...locals, centralDir, end]);
87
- }