@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.
@@ -0,0 +1,143 @@
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
+ export function printJson(value) {
21
+ process.stdout.write(JSON.stringify(value, null, 2) + '\n');
22
+ }
23
+ /** Print `value` as JSON in --json mode, otherwise run `human`. */
24
+ export function render(value, human) {
25
+ if (jsonMode)
26
+ return printJson(value);
27
+ human(value);
28
+ }
29
+ export function info(message) {
30
+ if (!jsonMode)
31
+ process.stderr.write(message + '\n');
32
+ }
33
+ export function success(message) {
34
+ if (!jsonMode)
35
+ process.stderr.write(color.green('✓ ') + message + '\n');
36
+ }
37
+ export function warn(message) {
38
+ process.stderr.write(color.yellow('! ') + message + '\n');
39
+ }
40
+ const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
41
+ /**
42
+ * Terminal cell width. CJK and emoji occupy two columns, so counting code
43
+ * points misaligns every table that holds Chinese text.
44
+ */
45
+ function charWidth(code) {
46
+ if (code === 0x200d)
47
+ return 0;
48
+ if (code >= 0x0300 && code <= 0x036f)
49
+ return 0; // combining marks
50
+ if (code >= 0xfe00 && code <= 0xfe0f)
51
+ return 0; // variation selectors
52
+ return (code >= 0x1100 && code <= 0x115f) ||
53
+ (code >= 0x2e80 && code <= 0x303e) ||
54
+ (code >= 0x3041 && code <= 0x33ff) ||
55
+ (code >= 0x3400 && code <= 0x4dbf) ||
56
+ (code >= 0x4e00 && code <= 0x9fff) ||
57
+ (code >= 0xa000 && code <= 0xa4cf) ||
58
+ (code >= 0xa960 && code <= 0xa97f) ||
59
+ (code >= 0xac00 && code <= 0xd7a3) ||
60
+ (code >= 0xf900 && code <= 0xfaff) ||
61
+ (code >= 0xfe10 && code <= 0xfe19) ||
62
+ (code >= 0xfe30 && code <= 0xfe6f) ||
63
+ (code >= 0xff00 && code <= 0xff60) ||
64
+ (code >= 0xffe0 && code <= 0xffe6) ||
65
+ (code >= 0x1f300 && code <= 0x1f64f) ||
66
+ (code >= 0x1f900 && code <= 0x1f9ff) ||
67
+ (code >= 0x20000 && code <= 0x3fffd)
68
+ ? 2
69
+ : 1;
70
+ }
71
+ export function displayWidth(text) {
72
+ let width = 0;
73
+ for (const char of stripAnsi(text))
74
+ width += charWidth(char.codePointAt(0));
75
+ return width;
76
+ }
77
+ const visibleLength = displayWidth;
78
+ export function table(rows, columns) {
79
+ if (!rows.length) {
80
+ info(color.dim('(empty)'));
81
+ return;
82
+ }
83
+ const cells = rows.map((row) => columns.map((c) => {
84
+ const v = c.value(row);
85
+ return v == null ? color.dim('-') : String(v);
86
+ }));
87
+ const widths = columns.map((c, i) => Math.max(visibleLength(c.header), ...cells.map((r) => visibleLength(r[i]))));
88
+ const pad = (s, w) => s + ' '.repeat(Math.max(0, w - displayWidth(s)));
89
+ process.stdout.write(columns.map((c, i) => color.dim(pad(c.header.toUpperCase(), widths[i]))).join(' ').trimEnd() +
90
+ '\n');
91
+ for (const row of cells) {
92
+ process.stdout.write(row.map((v, i) => pad(v, widths[i])).join(' ').trimEnd() + '\n');
93
+ }
94
+ }
95
+ /** Key/value block for detail views. */
96
+ export function detail(pairs) {
97
+ const width = Math.max(...pairs.map(([k]) => displayWidth(k)));
98
+ for (const [k, v] of pairs) {
99
+ if (v == null || v === '')
100
+ continue;
101
+ const text = typeof v === 'object' ? JSON.stringify(v) : String(v);
102
+ const padded = k + ' '.repeat(Math.max(0, width - displayWidth(k)));
103
+ process.stdout.write(`${color.dim(padded)} ${text}\n`);
104
+ }
105
+ }
106
+ /** Truncate to `max` terminal columns, not `max` code points. */
107
+ export function truncate(value, max = 48) {
108
+ if (value == null)
109
+ return null;
110
+ const s = String(value).replace(/\s+/g, ' ').trim();
111
+ if (displayWidth(s) <= max)
112
+ return s;
113
+ let out = '';
114
+ let width = 0;
115
+ for (const char of s) {
116
+ const next = width + charWidth(char.codePointAt(0));
117
+ if (next > max - 1)
118
+ break;
119
+ out += char;
120
+ width = next;
121
+ }
122
+ return out + '…';
123
+ }
124
+ export function relativeTime(value) {
125
+ if (!value)
126
+ return null;
127
+ const then = new Date(value).getTime();
128
+ if (Number.isNaN(then))
129
+ return null;
130
+ const diff = Date.now() - then;
131
+ const units = [
132
+ [86400000 * 365, 'y'],
133
+ [86400000 * 30, 'mo'],
134
+ [86400000, 'd'],
135
+ [3600000, 'h'],
136
+ [60000, 'm'],
137
+ ];
138
+ for (const [ms, label] of units) {
139
+ if (Math.abs(diff) >= ms)
140
+ return `${Math.round(diff / ms)}${label} ago`;
141
+ }
142
+ return 'just now';
143
+ }
@@ -0,0 +1,118 @@
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
+ }
@@ -0,0 +1,87 @@
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
+ }
package/dist/index.js ADDED
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { ApiError } from './core/client.js';
4
+ import { color, setJsonMode } from './core/output.js';
5
+ import { authCommand } from './commands/auth.js';
6
+ import { configCommand } from './commands/config.js';
7
+ import { orgCommand } from './commands/org.js';
8
+ import { workspaceCommand } from './commands/workspace.js';
9
+ import { datasetCommand, fsCommand } from './commands/fsnode.js';
10
+ import { assetCommand } from './commands/asset.js';
11
+ import { genCommand, historyCommand } from './commands/gen.js';
12
+ import { modelCommand } from './commands/model.js';
13
+ import { agentCommand } from './commands/agent.js';
14
+ import { profileCommand } from './commands/profile.js';
15
+ import { presetCommand } from './commands/preset.js';
16
+ import { skillCommand } from './commands/skill.js';
17
+ import { canvasCommand } from './commands/canvas.js';
18
+ const VERSION = '0.1.0';
19
+ const program = new Command();
20
+ program
21
+ .name('wji')
22
+ .description('Command line interface for the WJI AI platform')
23
+ .version(VERSION, '-v, --version')
24
+ .option('--profile <name>', 'config profile to use')
25
+ .option('--endpoint <url>', 'API base URL')
26
+ .option('--token <token>', 'bearer token or sk_ API key for this call')
27
+ .option('--org <orgId>', 'organization context (X-Org-Id)')
28
+ .option('--no-org', 'force personal context')
29
+ .option('--locale <locale>', 'response language (cht, chs, en)')
30
+ .option('--json', 'print raw JSON instead of tables')
31
+ .configureHelp({ sortSubcommands: true });
32
+ /**
33
+ * Global flags live on the root command, but subcommand actions run before
34
+ * we can read them off `program.opts()` in a nested scope — so hand every
35
+ * command a getter instead of a snapshot.
36
+ */
37
+ const globals = () => {
38
+ const opts = program.opts();
39
+ return {
40
+ profile: opts.profile,
41
+ endpoint: opts.endpoint,
42
+ token: opts.token,
43
+ org: typeof opts.org === 'string' ? opts.org : undefined,
44
+ // commander turns `--no-org` into org === false
45
+ personal: opts.org === false,
46
+ locale: opts.locale,
47
+ };
48
+ };
49
+ program.hook('preAction', () => {
50
+ setJsonMode(!!program.opts().json);
51
+ });
52
+ program.addCommand(authCommand(globals));
53
+ program.addCommand(configCommand(globals));
54
+ program.addCommand(orgCommand(globals));
55
+ program.addCommand(workspaceCommand(globals));
56
+ program.addCommand(fsCommand(globals));
57
+ program.addCommand(datasetCommand(globals));
58
+ program.addCommand(assetCommand(globals));
59
+ program.addCommand(genCommand(globals));
60
+ program.addCommand(historyCommand(globals));
61
+ program.addCommand(modelCommand(globals));
62
+ program.addCommand(agentCommand(globals));
63
+ program.addCommand(profileCommand(globals));
64
+ program.addCommand(presetCommand(globals));
65
+ program.addCommand(skillCommand(globals));
66
+ program.addCommand(canvasCommand(globals));
67
+ program.addHelpText('after', `
68
+ Examples:
69
+ wji auth login --email you@example.com
70
+ wji ws list
71
+ wji ws use 65f0a1b2c3d4e5f6a7b8c9d0
72
+ wji models list --feature image # pass the MODEL column to -m
73
+ wji gen image -m nano-banana -p "a rainy alley at dusk" --ratio 16:9 --wait
74
+ wji fs tree $(wji ws get --json | jq -r .id) --depth 2
75
+ wji asset upload ./reference.png --workspace <id>
76
+ wji canvas status # after pressing the link button on a canvas
77
+
78
+ Auth precedence: --token > $WJI_TOKEN / $WJI_API_KEY > profile apiKey > profile accessToken.
79
+ Config lives in ~/.wji/config.json (chmod 600).`);
80
+ async function main() {
81
+ try {
82
+ await program.parseAsync(process.argv);
83
+ }
84
+ catch (err) {
85
+ if (err instanceof ApiError) {
86
+ process.stderr.write(`${color.red('✗')} ${err.message}\n`);
87
+ process.stderr.write(color.dim(` ${err.status} ${err.path}\n`));
88
+ process.exit(err.status === 401 || err.status === 403 ? 77 : 1);
89
+ }
90
+ process.stderr.write(`${color.red('✗')} ${err.message}\n`);
91
+ process.exit(1);
92
+ }
93
+ }
94
+ main();
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@wji.ai/cli",
3
+ "version": "0.1.0",
4
+ "description": "Command line interface for the WJI AI platform",
5
+ "license": "UNLICENSED",
6
+ "author": "WJI AI",
7
+ "homepage": "https://wji.ai",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/ultra-instinct-ai/wji-cli.git"
11
+ },
12
+ "keywords": [
13
+ "wji",
14
+ "cli",
15
+ "ai",
16
+ "image-generation",
17
+ "video-generation",
18
+ "claude-code"
19
+ ],
20
+ "type": "module",
21
+ "bin": {
22
+ "wji": "dist/index.js"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "skill",
27
+ "README.md"
28
+ ],
29
+ "engines": {
30
+ "node": ">=20"
31
+ },
32
+ "scripts": {
33
+ "build": "tsc -p tsconfig.json && chmod +x dist/index.js",
34
+ "dev": "tsc -p tsconfig.json --watch",
35
+ "typecheck": "tsc -p tsconfig.json --noEmit",
36
+ "start": "node dist/index.js",
37
+ "prepublishOnly": "npm run build"
38
+ },
39
+ "dependencies": {
40
+ "commander": "^12.1.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^22.10.0",
44
+ "typescript": "^5.7.2"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ }
49
+ }