ainize 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/LICENSE +21 -0
- package/README.md +225 -0
- package/dist/bin.d.ts +10 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +711 -0
- package/dist/bin.js.map +1 -0
- package/dist/client.d.ts +38 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +164 -0
- package/dist/client.js.map +1 -0
- package/dist/commands/auth.d.ts +30 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +216 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/branch.d.ts +326 -0
- package/dist/commands/branch.d.ts.map +1 -0
- package/dist/commands/branch.js +467 -0
- package/dist/commands/branch.js.map +1 -0
- package/dist/commands/chain.d.ts +43 -0
- package/dist/commands/chain.d.ts.map +1 -0
- package/dist/commands/chain.js +224 -0
- package/dist/commands/chain.js.map +1 -0
- package/dist/commands/chat.d.ts +157 -0
- package/dist/commands/chat.d.ts.map +1 -0
- package/dist/commands/chat.js +395 -0
- package/dist/commands/chat.js.map +1 -0
- package/dist/commands/dataset.d.ts +51 -0
- package/dist/commands/dataset.d.ts.map +1 -0
- package/dist/commands/dataset.js +157 -0
- package/dist/commands/dataset.js.map +1 -0
- package/dist/commands/drive.d.ts +32 -0
- package/dist/commands/drive.d.ts.map +1 -0
- package/dist/commands/drive.js +68 -0
- package/dist/commands/drive.js.map +1 -0
- package/dist/commands/huggingface-dataset.d.ts +63 -0
- package/dist/commands/huggingface-dataset.d.ts.map +1 -0
- package/dist/commands/huggingface-dataset.js +254 -0
- package/dist/commands/huggingface-dataset.js.map +1 -0
- package/dist/commands/init.d.ts +107 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +538 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/ledger.d.ts +43 -0
- package/dist/commands/ledger.d.ts.map +1 -0
- package/dist/commands/ledger.js +125 -0
- package/dist/commands/ledger.js.map +1 -0
- package/dist/commands/node.d.ts +250 -0
- package/dist/commands/node.d.ts.map +1 -0
- package/dist/commands/node.js +651 -0
- package/dist/commands/node.js.map +1 -0
- package/dist/commands/patch.d.ts +852 -0
- package/dist/commands/patch.d.ts.map +1 -0
- package/dist/commands/patch.js +1527 -0
- package/dist/commands/patch.js.map +1 -0
- package/dist/commands/peers.d.ts +86 -0
- package/dist/commands/peers.d.ts.map +1 -0
- package/dist/commands/peers.js +143 -0
- package/dist/commands/peers.js.map +1 -0
- package/dist/commands/teach-dataset.d.ts +381 -0
- package/dist/commands/teach-dataset.d.ts.map +1 -0
- package/dist/commands/teach-dataset.js +780 -0
- package/dist/commands/teach-dataset.js.map +1 -0
- package/dist/commands/teach.d.ts +351 -0
- package/dist/commands/teach.d.ts.map +1 -0
- package/dist/commands/teach.js +435 -0
- package/dist/commands/teach.js.map +1 -0
- package/dist/context.d.ts +55 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +102 -0
- package/dist/context.js.map +1 -0
- package/dist/ens.d.ts +50 -0
- package/dist/ens.d.ts.map +1 -0
- package/dist/ens.js +155 -0
- package/dist/ens.js.map +1 -0
- package/dist/help.d.ts +57 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/help.js +227 -0
- package/dist/help.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/output.d.ts +104 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +383 -0
- package/dist/output.js.map +1 -0
- package/dist/pid.d.ts +5 -0
- package/dist/pid.d.ts.map +1 -0
- package/dist/pid.js +25 -0
- package/dist/pid.js.map +1 -0
- package/dist/quiet.d.ts +2 -0
- package/dist/quiet.d.ts.map +1 -0
- package/dist/quiet.js +12 -0
- package/dist/quiet.js.map +1 -0
- package/package.json +55 -0
package/dist/bin.js
ADDED
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `ainize` — operate an Ainize knowledge-marketplace node.
|
|
4
|
+
*
|
|
5
|
+
* Ainize = AI + -ize, "make it usable by AI". The 2019 `ainize` CLI ainized GitHub repos into running AI
|
|
6
|
+
* services; this one ainizes *knowledge*: publish verified knowledge patches, test them live against the model
|
|
7
|
+
* (`chat`), trade them with automatic payment and load them into a running model in seconds.
|
|
8
|
+
*/
|
|
9
|
+
import './quiet.js';
|
|
10
|
+
import yargs, {} from 'yargs';
|
|
11
|
+
import { hideBin } from 'yargs/helpers';
|
|
12
|
+
import chalk from 'chalk';
|
|
13
|
+
import { CliError, PROG, buildContext, requireNodeTarget } from './context.js';
|
|
14
|
+
import { explainUsageError } from './help.js';
|
|
15
|
+
import { flushJson, jsonError, setWide } from './output.js';
|
|
16
|
+
import * as init from './commands/init.js';
|
|
17
|
+
import * as node from './commands/node.js';
|
|
18
|
+
import * as auth from './commands/auth.js';
|
|
19
|
+
import * as peers from './commands/peers.js';
|
|
20
|
+
import * as patch from './commands/patch.js';
|
|
21
|
+
import * as ledger from './commands/ledger.js';
|
|
22
|
+
import * as branch from './commands/branch.js';
|
|
23
|
+
import * as drive from './commands/drive.js';
|
|
24
|
+
import * as chain from './commands/chain.js';
|
|
25
|
+
import * as chat from './commands/chat.js';
|
|
26
|
+
import * as teach from './commands/teach.js';
|
|
27
|
+
import * as teachData from './commands/teach-dataset.js';
|
|
28
|
+
import * as dataset from './commands/dataset.js';
|
|
29
|
+
import { datasetImportHuggingFace } from './commands/huggingface-dataset.js';
|
|
30
|
+
import { EVENT_KINDS, EVENT_LEVELS } from '@ainize/core';
|
|
31
|
+
import { RECORD_KINDS } from '@ainize/core';
|
|
32
|
+
const ctxOf = (a) => buildContext({ home: a.home, node: a.node, json: a.json, quiet: a.quiet });
|
|
33
|
+
/** `teach status <target>`: a target that carries a host names its own node, so no config is needed for it. */
|
|
34
|
+
const namesItsOwnNode = (a) => {
|
|
35
|
+
try {
|
|
36
|
+
return !!teach.parseTeachTarget(a.target, '').nodeUrl;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
/** Was `--json` asked for? Read from argv, because a parse failure never reaches a context (item 104). */
|
|
43
|
+
const jsonMode = () => { const v = hideBin(process.argv); return v.includes('--json') && !v.includes('--no-json'); };
|
|
44
|
+
/** `patch get` — the command a JSON error names, taken from what yargs matched rather than from a second list. */
|
|
45
|
+
const commandOf = (raw) => (raw._ ?? []).map(String).join(' ') || null;
|
|
46
|
+
const run = (fn, keepAlive = false, noConfig = false) => async (raw) => {
|
|
47
|
+
const a = raw;
|
|
48
|
+
let ctx = null;
|
|
49
|
+
try {
|
|
50
|
+
ctx = ctxOf(a);
|
|
51
|
+
setWide(!!a.wide);
|
|
52
|
+
if (!(typeof noConfig === 'function' ? noConfig(raw) : noConfig))
|
|
53
|
+
requireNodeTarget(ctx);
|
|
54
|
+
await fn(ctx, a);
|
|
55
|
+
// a command whose only output was `ok()` still owes `--json` one document (item 104)
|
|
56
|
+
flushJson(ctx);
|
|
57
|
+
// a command that finished but found something wrong (e.g. `status` on a port a stranger answers) sets its own code
|
|
58
|
+
if (!keepAlive && !process.exitCode)
|
|
59
|
+
process.exitCode = 0;
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
const err = e;
|
|
63
|
+
const code = err instanceof CliError ? err.exitCode : 1;
|
|
64
|
+
// Item 104: under `--json` the failure is a document too — with the node's own error body, which `CliError`
|
|
65
|
+
// has been carrying and nothing ever printed. A CI job reads the reason instead of matching `error:` prose.
|
|
66
|
+
if (ctx?.json ?? !!a.json)
|
|
67
|
+
jsonError(err, commandOf(raw), code);
|
|
68
|
+
else
|
|
69
|
+
process.stderr.write(chalk.red('error: ') + (err.message ?? String(e)) + '\n');
|
|
70
|
+
process.exit(code);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Every failure of the command line itself (item 103). yargs' own strings — `Not enough non-option arguments: got 0,
|
|
75
|
+
* need at least 1` for a bare `ainize publish`, `Unknown argument: statsu` for a typo — are rewritten from the help
|
|
76
|
+
* of the command that failed: what is missing, in its own words, with its usage line and first worked example.
|
|
77
|
+
*/
|
|
78
|
+
const usageError = (msg, err, yy) => {
|
|
79
|
+
let help = '';
|
|
80
|
+
// yargs renders the help of the level that failed synchronously into this callback; an older/odd path leaves it
|
|
81
|
+
// empty, and the explanation degrades to yargs' own sentence rather than to nothing.
|
|
82
|
+
try {
|
|
83
|
+
yy.showHelp((s) => { help = s; });
|
|
84
|
+
}
|
|
85
|
+
catch { /* the message below stands on its own */ }
|
|
86
|
+
const e = explainUsageError(msg, err, help, hideBin(process.argv));
|
|
87
|
+
if (jsonMode())
|
|
88
|
+
jsonError({ message: e.message, details: e.hints.length ? { help: e.hints } : undefined }, e.command, 1);
|
|
89
|
+
else
|
|
90
|
+
process.stderr.write(chalk.red('error: ') + e.message + '\n' + e.hints.map((h) => chalk.gray(' ' + h) + '\n').join(''));
|
|
91
|
+
process.exit(1);
|
|
92
|
+
};
|
|
93
|
+
const fail = (y) => y.fail((msg, err, yy) => usageError(msg, err, yy));
|
|
94
|
+
const cli = yargs(hideBin(process.argv))
|
|
95
|
+
.scriptName(PROG)
|
|
96
|
+
.usage([
|
|
97
|
+
'$0 <command> [options]', '',
|
|
98
|
+
'Ainize — ainize your knowledge (AI + -ize: make it usable by AI).',
|
|
99
|
+
'Run an Ainize knowledge-marketplace node: publish knowledge patches, let independent nodes',
|
|
100
|
+
'verify them, test them live (`$0 chat`), trade them with automatic payment (x402) and load',
|
|
101
|
+
'them into a running model without restart.',
|
|
102
|
+
].join('\n'))
|
|
103
|
+
.option('home', { type: 'string', describe: 'node home directory (AINIZE_HOME)', global: true })
|
|
104
|
+
.option('node', { type: 'string', describe: 'node API URL (default: http://localhost:<config port>)', global: true })
|
|
105
|
+
.option('json', { type: 'boolean', describe: 'machine-readable JSON output — one document per command, errors included, on failure to stderr', global: true, default: false })
|
|
106
|
+
.option('quiet', { type: 'boolean', describe: 'print nothing but the id of whatever was created or changed', global: true, default: false })
|
|
107
|
+
.option('wide', { type: 'boolean', describe: 'do not fit tables to the terminal width (piped output is never fitted)', global: true, default: false })
|
|
108
|
+
.alias('h', 'help').help('help').version()
|
|
109
|
+
.showHelpOnFail(false, `Specify --help for available options.`)
|
|
110
|
+
.strict()
|
|
111
|
+
// a one-character typo answers with the command it meant instead of "Unknown argument" (item 103)
|
|
112
|
+
.recommendCommands()
|
|
113
|
+
.wrap(Math.min(110, process.stdout.columns || 100))
|
|
114
|
+
.demandCommand(1, `Specify a command. Try \`${PROG} --help\`.`)
|
|
115
|
+
// Item 104: the codes that make a failure legible to a script were documented nowhere at all.
|
|
116
|
+
.epilogue([
|
|
117
|
+
'Exit codes:',
|
|
118
|
+
' 0 it worked',
|
|
119
|
+
' 1 the command ran and failed (the node refused it, a file was missing)',
|
|
120
|
+
' 2 no node to talk to: no config here, a bad --node, or nothing answering',
|
|
121
|
+
' 3 not yours to ask: log in (`' + PROG + ' login`), or you may not read it',
|
|
122
|
+
' 4 the node did not answer in time — it may be running the work anyway',
|
|
123
|
+
' 5 in the way: the shared model lock, or an incomplete subscription',
|
|
124
|
+
' 130 cancelled at a confirmation prompt',
|
|
125
|
+
' `' + PROG + ' teach train --wait` sets 4-8 from the lesson\'s own outcome.',
|
|
126
|
+
'',
|
|
127
|
+
'Docs: <node url>/docs · one command: `' + PROG + ' <command> --help`',
|
|
128
|
+
].join('\n'));
|
|
129
|
+
// ---------------------------------------------------------------- init / config / keys
|
|
130
|
+
cli.command('init', 'Create a node identity and config in AINIZE_HOME', (y) => fail(y)
|
|
131
|
+
.option('name', { type: 'string', describe: 'node display name' })
|
|
132
|
+
.option('port', { type: 'number', describe: 'HTTP port', default: undefined })
|
|
133
|
+
.option('ledger', { choices: ['local', 'ain'], describe: 'ledger backend: local P2P record DAG or AIN blockchain' })
|
|
134
|
+
.option('ain-provider', { type: 'string', describe: 'AIN JSON-RPC URL (ain ledger)', default: undefined })
|
|
135
|
+
.option('ain-chain-id', { type: 'number', describe: 'AIN chain id (0 = local/testnet)', default: undefined })
|
|
136
|
+
.option('peer', { type: 'string', array: true, describe: 'seed peer URL(s)' })
|
|
137
|
+
.option('roles', { type: 'string', describe: 'comma list of seller,verifier,serving,gateway' })
|
|
138
|
+
.option('runtime-repo', { type: 'string', describe: 'reference runtime repo (scripts/patch.py)' })
|
|
139
|
+
.option('runtime-api', { type: 'string', describe: 'serving API (OpenAI-compatible) URL' })
|
|
140
|
+
.option('private-key', { type: 'string', describe: 'import an existing AIN private key (hex)' })
|
|
141
|
+
.option('public-url', { type: 'string', describe: 'URL peers can reach this node at' })
|
|
142
|
+
.option('host', { type: 'string', describe: 'interface to bind (default 127.0.0.1 — this machine only)' })
|
|
143
|
+
.option('public', { type: 'boolean', default: false, describe: 'bind 0.0.0.0 (every interface) — only behind a firewall or proxy' })
|
|
144
|
+
.option('password', { type: 'string', describe: 'operator password, set now so nobody else can claim this node (or AINIZE_PASSWORD)' })
|
|
145
|
+
.option('no-password', { type: 'boolean', default: false, describe: `leave the node unclaimed; \`${PROG} login\` claims it later (loopback only)` })
|
|
146
|
+
.option('force', { type: 'boolean', describe: 'rewrite an existing config.json (the node identity and operator password are kept; the old file is copied aside)', default: false })
|
|
147
|
+
.option('new-identity', { type: 'boolean', describe: 'with --force: mint a NEW node key, orphaning everything the old one published (asks you to type the current address)', default: false })
|
|
148
|
+
.example('$0 init --name alice --port 3402', 'local ledger node')
|
|
149
|
+
.example('$0 init --name alice --password "…" --host 0.0.0.0', 'a node others can reach, claimed before it listens')
|
|
150
|
+
.example('$0 init --ledger ain --ain-provider http://localhost:8081', `AIN blockchain ledger (see \`${PROG} chain up\`)`), run((ctx, a) => init.init(ctx, { ...a, ainProvider: a['ain-provider'], ainChainId: a['ain-chain-id'], runtimeRepo: a['runtime-repo'], runtimeApi: a['runtime-api'], privateKey: a['private-key'], publicUrl: a['public-url'], newIdentity: a['new-identity'], noPassword: a['no-password'] }), false, true));
|
|
151
|
+
cli.command('config', 'Show or edit the node config', (y) => fail(y)
|
|
152
|
+
.command('show', 'Print config.json (secrets hidden)', (yy) => yy, run((ctx) => init.configShow(ctx), false, true))
|
|
153
|
+
.command('get <key>', 'Print one config key (dotted path)', (yy) => yy.positional('key', { type: 'string', demandOption: true })
|
|
154
|
+
.example('$0 config get market.defaultPrice', ''), run((ctx, a) => init.configGet(ctx, a.key), false, true))
|
|
155
|
+
.command('set <key> <value>', 'Set a config key (dotted path, e.g. market.defaultPrice 0.5)', (yy) => yy
|
|
156
|
+
.positional('key', { type: 'string', demandOption: true }).positional('value', { type: 'string', demandOption: true })
|
|
157
|
+
.example('$0 config set ledger.kind ain', '').example('$0 config set peers http://a:3402,http://b:3403', ''), run((ctx, a) => init.configSet(ctx, a.key, a.value), false, true))
|
|
158
|
+
.command('unset <key>', 'Remove a config key so the node uses its built-in default', (yy) => yy.positional('key', { type: 'string', demandOption: true })
|
|
159
|
+
.example('$0 config unset teach.trainer.gpus', ''), run((ctx, a) => init.configUnset(ctx, a.key), false, true))
|
|
160
|
+
.demandCommand(1, 'Subcommand is required (show|get|set|unset).'), () => undefined);
|
|
161
|
+
cli.command('keys', 'Node identity: the key that owns everything this node published', (y) => fail(y)
|
|
162
|
+
.command('show', 'Print address and public key', (yy) => yy
|
|
163
|
+
.option('reveal', { type: 'boolean', default: false, describe: 'also print the private key (asks first)' })
|
|
164
|
+
.option('yes', { type: 'boolean', default: false, describe: 'with --reveal: skip the confirmation' }), run((ctx, a) => init.keysShow(ctx, a.reveal, a.yes), false, true))
|
|
165
|
+
.command(['backup <file>', 'export <file>'], 'Save the node key to a file (encrypted with --passphrase) — the only way back after a wiped disk', (yy) => yy
|
|
166
|
+
.positional('file', { type: 'string', demandOption: true })
|
|
167
|
+
.option('passphrase', { type: 'string', describe: 'encrypt with this passphrase (or AINIZE_KEY_PASSPHRASE); without one the key is stored in the clear' })
|
|
168
|
+
.option('force', { type: 'boolean', default: false, describe: 'overwrite an existing file' })
|
|
169
|
+
.example('$0 keys backup ~/node-key.json --passphrase "…"', ''), run((ctx, a) => init.keysBackup(ctx, a.file, a), false, true))
|
|
170
|
+
.command('import <file>', 'Make a backed-up key this node\'s identity (asks you to type the current address)', (yy) => yy
|
|
171
|
+
.positional('file', { type: 'string', demandOption: true }).option('passphrase', { type: 'string', describe: 'or AINIZE_KEY_PASSPHRASE' }), run((ctx, a) => init.keysImport(ctx, a.file, a), false, true))
|
|
172
|
+
.command('rotate', 'Mint a NEW node identity, keeping every other setting (asks you to type the current address)', (yy) => yy, run((ctx) => init.keysRotate(ctx), false, true))
|
|
173
|
+
.demandCommand(1, 'Subcommand is required (show|backup|import|rotate).'), () => undefined);
|
|
174
|
+
// ---------------------------------------------------------------- lifecycle
|
|
175
|
+
cli.command('start', 'Start the node (foreground unless --detach)', (y) => fail(y)
|
|
176
|
+
.option('port', { type: 'number', describe: 'HTTP port for this run (default: the port in config.json)' })
|
|
177
|
+
.option('peer', { type: 'string', array: true, describe: 'extra peer URL(s)' })
|
|
178
|
+
.option('roles', { type: 'string', describe: 'comma list of seller,verifier,serving,gateway for this run (default: the config value)' })
|
|
179
|
+
.option('public-url', { type: 'string', describe: 'URL peers should reach this node at — an address on this machine is useless to them (default: the config value)' })
|
|
180
|
+
.option('detach', { alias: 'd', type: 'boolean', default: false, describe: 'run in the background (pid in AINIZE_HOME/node.pid)' })
|
|
181
|
+
.example('$0 start', '').example('$0 start -d --peer http://localhost:3402', 'second node joining the first'), run(async (ctx, a) => {
|
|
182
|
+
const r = await node.start(ctx, { ...a, publicUrl: a['public-url'] });
|
|
183
|
+
if ('detached' in r)
|
|
184
|
+
return r;
|
|
185
|
+
// Only to a terminal: `start -d` spawns this same foreground path with stdout redirected into node.log, so this
|
|
186
|
+
// line used to end every detached run's log with an instruction nobody can carry out there (item 131).
|
|
187
|
+
if (process.stdout.isTTY)
|
|
188
|
+
process.stdout.write(chalk.gray('press Ctrl+C to stop\n'));
|
|
189
|
+
await new Promise(() => undefined); // keep alive
|
|
190
|
+
}, true, true));
|
|
191
|
+
cli.command('stop', 'Stop a background node', (y) => fail(y), run((ctx) => node.stop(ctx), false, true));
|
|
192
|
+
cli.command('status', 'Show node / ledger / runtime status', (y) => fail(y)
|
|
193
|
+
.option('check', { type: 'boolean', default: false, describe: 'readiness only (GET /readyz): exits 1 when a check fails' })
|
|
194
|
+
.example('$0 status --check', 'for a monitor or a deploy script'), run((ctx, a) => (a.check ? node.statusCheck(ctx) : node.status(ctx))));
|
|
195
|
+
cli.command('logs', 'Show node events', (y) => fail(y)
|
|
196
|
+
.option('follow', { alias: 'f', type: 'boolean', default: false, describe: 'keep printing events as they happen (Ctrl-C to stop)' })
|
|
197
|
+
.option('patch', { type: 'string', describe: 'only events of a patch' })
|
|
198
|
+
.option('kind', { choices: EVENT_KINDS, describe: 'only this kind of event' })
|
|
199
|
+
.option('level', { choices: EVENT_LEVELS, describe: 'this level and worse (warn shows warn + error)' })
|
|
200
|
+
.option('limit', { type: 'number', default: 100, describe: 'how many past events to print, newest last' })
|
|
201
|
+
.example('$0 logs --level warn', 'everything that went wrong, newest last')
|
|
202
|
+
.example('$0 logs --kind trade --limit 20', ''), run((ctx, a) => node.logs(ctx, a), true));
|
|
203
|
+
cli.command('seed', 'Seed demo data (prototype ledger, real Qwen3.8 patches if present, synthetic branches)', (y) => fail(y)
|
|
204
|
+
.option('real', { type: 'boolean', default: true, describe: 'register real patches from the runtime repo' })
|
|
205
|
+
.option('synthetic', { type: 'boolean', default: false, describe: 'create synthetic law/KR vs law/US demo patches' })
|
|
206
|
+
.option('prototype', { type: 'boolean', default: false, describe: 'import the reference prototype ledger' })
|
|
207
|
+
.option('announce', { type: 'boolean', default: true, describe: 'announce the seeded knowledge on the ledger (--no-announce leaves drafts)' }), run((ctx, a) => node.seed(ctx, a), false, true));
|
|
208
|
+
cli.command('nodes', 'List the peers this node talks to and the nodes it knows of', (y) => fail(y)
|
|
209
|
+
.option('all', { type: 'boolean', default: false, describe: 'include node records not seen for over an hour (they are permanent, so there are many)' })
|
|
210
|
+
.option('limit', { type: 'number', describe: 'show at most this many node records' })
|
|
211
|
+
.example('$0 nodes', 'peers first, then the nodes seen in the last hour')
|
|
212
|
+
.example('$0 nodes --all', 'every node record this node has ever read'), run((ctx, a) => node.nodesTable(ctx, a)));
|
|
213
|
+
cli.command('blobs', 'Knowledge files this node holds on disk, and what they cost', (y) => fail(y)
|
|
214
|
+
.command(['ls', 'list'], 'List every knowledge file with its size and why it is held', (yy) => yy, run((ctx) => node.blobsLs(ctx)))
|
|
215
|
+
.demandCommand(1, 'Subcommand is required (ls).'), () => undefined);
|
|
216
|
+
cli.command('gc', 'Delete knowledge files this node neither published nor bought (verification copies)', (y) => fail(y)
|
|
217
|
+
.option('dry-run', { type: 'boolean', default: false, describe: 'list what would go and delete nothing' })
|
|
218
|
+
.option('keep-purchased', { type: 'boolean', default: true, describe: 'keep bodies bought through the market (--no-keep-purchased includes them)' })
|
|
219
|
+
.option('older-than', { type: 'string', describe: 'only files fetched longer ago than this (30d, 12h, 90m)' })
|
|
220
|
+
.option('allow-sole-copy', { type: 'boolean', default: false, describe: 'also delete bodies no peer advertises (this node may be the last copy)' })
|
|
221
|
+
.option('yes', { alias: 'y', type: 'boolean', default: false, describe: 'do not ask for confirmation' })
|
|
222
|
+
.example('$0 gc --dry-run', 'what would be freed')
|
|
223
|
+
.example('$0 gc --older-than 30d', 'verification copies older than a month'), run((ctx, a) => node.gc(ctx, { dryRun: a['dry-run'], keepPurchased: a['keep-purchased'], olderThan: a['older-than'], allowSoleCopy: a['allow-sole-copy'], yes: a.yes })));
|
|
224
|
+
// ---------------------------------------------------------------- auth
|
|
225
|
+
cli.command('login', 'Log in as the node operator (sets the password on first use)', (y) => fail(y)
|
|
226
|
+
.option('password', { type: 'string', describe: 'the operator password, at least 4 characters — or AINIZE_PASSWORD. Without either you are asked; a script with no terminal can also pipe it in' })
|
|
227
|
+
.option('setup-token', { type: 'string', describe: 'claim a node over the network with the one-time token in its AINIZE_HOME/setup-token (or AINIZE_SETUP_TOKEN)' })
|
|
228
|
+
.example('$0 login', 'asks for the password (it is not echoed)')
|
|
229
|
+
.example('AINIZE_PASSWORD="…" $0 login', 'in a script, a cron line or over ssh — as does --password, and so does piping it in')
|
|
230
|
+
.example('$0 login --setup-token "$(ssh host cat ~/.ainize/setup-token)"', 'claim a node that has no password yet, from another machine'), run((ctx, a) => auth.login(ctx, { password: a.password, setupToken: a['setup-token'] })));
|
|
231
|
+
cli.command('password', 'Change the operator password (--reset rewrites it in config.json when you have forgotten it)', (y) => fail(y)
|
|
232
|
+
.option('password', { type: 'string', describe: 'the new password, at least 4 characters (or AINIZE_NEW_PASSWORD)' })
|
|
233
|
+
.option('current', { type: 'string', describe: 'the current password (or AINIZE_PASSWORD)' })
|
|
234
|
+
.option('reset', { type: 'boolean', default: false, describe: 'forgotten password: write a new hash into config.json (the node must be stopped)' })
|
|
235
|
+
.example('$0 password', 'change it on the running node')
|
|
236
|
+
.example('$0 stop && $0 password --reset', 'the way back when it is forgotten'), run((ctx, a) => auth.password(ctx, a), false, true));
|
|
237
|
+
cli.command('logout', 'Forget the operator session', (y) => fail(y), run((ctx) => auth.logout(ctx), false, true));
|
|
238
|
+
// ---------------------------------------------------------------- peers
|
|
239
|
+
cli.command('peers', 'Manage peers', (y) => fail(y)
|
|
240
|
+
.command('ls', 'List peers', (yy) => yy, run((ctx) => peers.peersLs(ctx)))
|
|
241
|
+
.command('add <url>', 'Add a peer', (yy) => yy.positional('url', { type: 'string', demandOption: true }), run((ctx, a) => peers.peersAdd(ctx, a.url)))
|
|
242
|
+
.command('rm <url>', 'Remove a peer', (yy) => yy.positional('url', { type: 'string', demandOption: true }), run((ctx, a) => peers.peersRm(ctx, a.url)))
|
|
243
|
+
.demandCommand(1, 'Subcommand is required (ls|add|rm).'), () => undefined);
|
|
244
|
+
/**
|
|
245
|
+
* `ainize publish <file>` and `ainize patch publish <file>` are ONE operation with two names (item 112). They used
|
|
246
|
+
* to be two near-identical builders with opposite `--announce` defaults and different flags on each, so which of
|
|
247
|
+
* the two you happened to type decided whether your knowledge landed on the permanent ledger — and neither help
|
|
248
|
+
* mentioned the other. One option list now, declared here; the two commands differ in exactly one thing, the
|
|
249
|
+
* default of `--announce`, which each of them declares and states in its own words.
|
|
250
|
+
*/
|
|
251
|
+
const publishOpts = (y) => y
|
|
252
|
+
.option('name', { type: 'string', demandOption: true, describe: 'what buyers see in the catalogue' })
|
|
253
|
+
.option('model', { type: 'string', demandOption: true, describe: 'target model id_M (e.g. Qwen3.8-Flash-Next)' })
|
|
254
|
+
.option('benchmark', { type: 'string', demandOption: true, describe: 'bench.json path or inline JSON {schema, queries, format, samples:[{prompt,expect}]}' })
|
|
255
|
+
.option('id', { type: 'string', describe: 'catalog id — permanent (default: a slug of --name)' })
|
|
256
|
+
.option('price', { type: 'string', describe: `price per download in this node's currency, AIN or node credit (default: \`${PROG} config get market.defaultPrice\`); editable while it is a draft, fixed for good at announce` })
|
|
257
|
+
.option('description', { type: 'string', describe: 'one or two sentences about what it knows' })
|
|
258
|
+
.option('parents', { type: 'string', describe: `comma list of the knowledge ids this was built on — their creators are paid the lineage share (\`${PROG} config get market.royaltyShare\`) out of every sale of this one` })
|
|
259
|
+
.option('branch', { type: 'string', describe: `knowledge track to publish it on (see \`${PROG} branch ls\`)` })
|
|
260
|
+
// Item 267 — the day the DATA is true of, which is not the day it was uploaded. Without it a daily baker's only
|
|
261
|
+
// place for the date was the name string, and every consumer had to parse someone's naming convention.
|
|
262
|
+
.option('as-of', { type: 'string', describe: 'the day the DATA is true of (YYYY-MM-DD) — not the day it is published. Shown as "Data as of …" and sorted by `--sort fresh`' })
|
|
263
|
+
.option('topic', { type: 'string', describe: 'ain-js knowledge topic path (e.g. finance/krx); default: patches/<model>' })
|
|
264
|
+
.option('license', { type: 'string', describe: 'licence written onto the public record: an SPDX id (CC-BY-4.0, MIT, Proprietary) or free text. Omitted: no licence on the record' })
|
|
265
|
+
// Item 360: per-hour and per-use were offered here and metered nowhere — a sale settles the price once, per download.
|
|
266
|
+
.option('billing', { choices: ['per_download'], describe: 'how buyers are charged. Only per_download is metered: one payment per download (per-hour and per-use are not implemented by any node)' })
|
|
267
|
+
.option('contributor', { type: 'string', array: true, describe: 'data provider credited and paid on the record: addr:name:share — share = fraction of YOUR share of each sale (repeatable, ≤ 4, Σ ≤ 1)' })
|
|
268
|
+
.option('dataset', { type: 'string', describe: 'the training set behind this knowledge (.jsonl/.csv on the node machine) — pinned and served under --dataset-access' })
|
|
269
|
+
.option('dataset-access', { choices: ['public', 'derivative', 'private'], describe: 'who may read those questions: anyone / people building on this knowledge (default) / nobody' })
|
|
270
|
+
.option('dataset-license', { type: 'string', describe: 'licence for the questions: CC0-1.0, CC-BY-4.0, CC-BY-SA-4.0, ODC-By-1.0, Proprietary' })
|
|
271
|
+
.option('supersede', { type: 'string', array: true, describe: 'with --announce: the listing(s) of yours this version replaces — required when the overlap rule found any, and the way to declare one whose rows do not overlap' })
|
|
272
|
+
// Item 248 — a dated snapshot, published on purpose, that the row-overlap rule must not retire.
|
|
273
|
+
.option('keep-others', { type: 'boolean', default: false, describe: 'with --announce: retire nothing — every overlapping listing of yours stays on sale' })
|
|
274
|
+
// Item 188 — a derivative could only be a new root-style listing with a dashed "declared" edge to its base: no
|
|
275
|
+
// version, no correction, no fork. The kind is the publisher's word; the row counts under it are the node's.
|
|
276
|
+
.option('kind', { choices: ['extend', 'contradict', 'update', 'merge'], describe: 'what this is to --parents: extend (adds answers on top) · contradict (disagrees with some of theirs) · update (your own next version) · merge. Needs the base\'s file on this node, which is how the row counts are measured' })
|
|
277
|
+
.option('force', { type: 'boolean', default: false, describe: 'publish bytes this node already published on this subject, or for a model it cannot test (never another author\'s bytes)' })
|
|
278
|
+
.option('test', { type: 'boolean', default: false, describe: 'hidden test listing (not shown in public catalogs)' });
|
|
279
|
+
/** The one handler behind both names. `--announce` is the only thing the two commands decide differently. */
|
|
280
|
+
const publishRun = run((ctx, a) => patch.patchPublish(ctx, { ...a, datasetAccess: a['dataset-access'], datasetLicense: a['dataset-license'], keepOthers: a['keep-others'], asOf: a['as-of'], announce: !!a.announce }));
|
|
281
|
+
// ---------------------------------------------------------------- patches
|
|
282
|
+
cli.command('patch [name]', 'Publish, inspect, verify, buy and apply knowledge patches — or give an ENS name to use one in a single line', (y) => fail(y)
|
|
283
|
+
// `ainize patch vaults.defi.engram.eth` — the default command of the group, so a NAME lands here while every
|
|
284
|
+
// subcommand below still wins on an exact match. It replaces the sequence an operator used to type by hand:
|
|
285
|
+
// ainize patch ls --node http://their-node:3402 --status LISTED -q "<topic>"
|
|
286
|
+
// ainize login && ainize use <id>
|
|
287
|
+
// The name carries both halves those lines supplied: `ainize.node` is the --node, `ainize.patch` is the id.
|
|
288
|
+
.command('$0 [name]', false, (yy) => yy
|
|
289
|
+
.positional('name', { type: 'string', describe: 'an ENS name, e.g. vaults.defi.engram.eth' })
|
|
290
|
+
.option('apply', { type: 'boolean', default: true, describe: 'load it into the model after buying (--no-apply to only buy)' })
|
|
291
|
+
.option('yes', { type: 'boolean', default: false, describe: 'do not ask before paying' })
|
|
292
|
+
.option('max-price', { type: 'number', describe: 'refuse if the total is above this' })
|
|
293
|
+
.option('resolve-only', { type: 'boolean', default: false, describe: 'print where the name points and stop — buy nothing' })
|
|
294
|
+
// Declared as `peer`, not `no-peer`: yargs' boolean negation turns `--no-X` into `X: false` for an option
|
|
295
|
+
// NAMED X, so an option literally called `no-peer` left "peer" on the line as a stray positional and the
|
|
296
|
+
// parser reported `unknown command "peer"`. `--no-apply` worked for exactly the reason this did not.
|
|
297
|
+
.option('peer', { type: 'boolean', default: true, describe: 'add the seller node as a peer so the body can come over p2p (--no-peer to skip)' })
|
|
298
|
+
.option('rpc', { type: 'string', describe: 'JSON-RPC endpoint for on-chain resolution (or ENS_RPC_URL)' })
|
|
299
|
+
.option('registry', { type: 'string', describe: 'ENS registry address — never assumed, because ENSv2 is not final (or ENS_REGISTRY)' })
|
|
300
|
+
.option('names', { type: 'string', describe: 'names file to resolve from, instead of the default search order' })
|
|
301
|
+
.example('$0 patch vaults.defi.engram.eth', 'resolve, peer, log in, quote, pay, download, load')
|
|
302
|
+
.example('$0 patch vaults.defi.engram.eth --resolve-only', 'just show where the name points')
|
|
303
|
+
.example('$0 patch vaults.defi.engram.eth --yes --max-price 30', 'unattended, with a budget'), run((ctx, a) => {
|
|
304
|
+
if (!a.name)
|
|
305
|
+
throw new CliError(`give an ENS name (e.g. ${PROG} patch vaults.defi.engram.eth) or a subcommand — see ${PROG} patch --help`);
|
|
306
|
+
return patch.patchByName(ctx, a.name, { apply: a.apply, yes: a.yes, maxPrice: a['max-price'], resolveOnly: a['resolve-only'], noPeer: a.peer === false, rpc: a.rpc, registry: a.registry, namesFile: a.names });
|
|
307
|
+
}))
|
|
308
|
+
.command('ls', 'List patches in the catalog', (yy) => yy
|
|
309
|
+
.option('status', { type: 'string', describe: 'comma list: DRAFT,ANNOUNCED,VERIFYING,LISTED,REJECTED,CHALLENGED,SUPERSEDED,RETIRED (retired knowledge is hidden unless you ask for it)' })
|
|
310
|
+
.option('model', { type: 'string', describe: 'only knowledge for this model id_M (e.g. Qwen3.8-Flash-Next)' })
|
|
311
|
+
.option('schema', { type: 'string', describe: 'benchmark schema' })
|
|
312
|
+
.option('branch', { type: 'string', describe: `only knowledge on this track (see \`${PROG} branch ls\`)` })
|
|
313
|
+
.option('author', { type: 'string', describe: 'only knowledge published by this node address' })
|
|
314
|
+
.option('q', { type: 'string', describe: 'text search' })
|
|
315
|
+
// `fresh` is item 267's: newest DATA (`as_of`, falling back to the registration date), which on a daily bake is
|
|
316
|
+
// a different order from `latest` — the node has answered `?sort=fresh` since the anchor field landed.
|
|
317
|
+
.option('sort', { choices: ['latest', 'fresh', 'popular', 'price', 'rows'], default: 'latest', describe: 'newest published, freshest data (--as-of), most sold, cheapest, or biggest' })
|
|
318
|
+
.option('limit', { type: 'number', default: 100, describe: 'how many rows' }).option('mine', { type: 'boolean', default: false, describe: 'only my patches (needs login)' })
|
|
319
|
+
.option('drafts', { type: 'boolean', default: false, describe: 'include my drafts (needs login)' }), run((ctx, a) => patch.patchLs(ctx, a)))
|
|
320
|
+
.command('get <id>', 'Show a patch in detail', (yy) => yy.positional('id', { type: 'string', demandOption: true }), run((ctx, a) => patch.patchGet(ctx, a.id)))
|
|
321
|
+
.command('publish <file>', `Register a .npz patch body as a draft — it stays a DRAFT until --announce (\`${PROG} publish\` is the same operation, announcing at once)`, (yy) => publishOpts(yy)
|
|
322
|
+
.positional('file', { type: 'string', demandOption: true, describe: 'path to the learned knowledge (.npz: addrs/before/after) on the node machine' })
|
|
323
|
+
.option('announce', { type: 'boolean', default: false, describe: `announce to the network immediately — the permanent record, and the one step with no undo (default here: no. \`${PROG} publish\` announces by default)` })
|
|
324
|
+
.example('$0 patch publish ./rows.npz --name "KRX tickers" --model Qwen3.8-Flash-Next --benchmark bench.json --price 25', 'a draft nobody can see yet')
|
|
325
|
+
.example('$0 patch publish ./rows.npz --name "KRX tickers" --model Qwen3.8-Flash-Next --benchmark bench.json --price 25 --announce', 'draft and announce in one line — the same thing `$0 publish` does'), publishRun)
|
|
326
|
+
.command('import <file>', 'Import a downloaded lesson (.npz + recipe.json) as a PRIVATE draft: no announce, no ledger record', (yy) => yy
|
|
327
|
+
.positional('file', { type: 'string', demandOption: true, describe: 'lesson-<slug>.npz on the node machine (stays in place)' })
|
|
328
|
+
.option('recipe', { type: 'string', demandOption: true, describe: 'recipe.json downloaded with the lesson (benchmark, model, facts)' })
|
|
329
|
+
.option('id', { type: 'string', describe: 'draft id (default: the lesson\'s draft id, taught-<slug>)' })
|
|
330
|
+
.option('name', { type: 'string', describe: 'name for the draft (default: the lesson\'s own name)' })
|
|
331
|
+
.option('model', { type: 'string', describe: 'target model id_M when the recipe names none' })
|
|
332
|
+
.option('price', { type: 'string', describe: `price if you later publish it (default: this node's market.defaultPrice)` })
|
|
333
|
+
.option('license', { type: 'string', describe: 'licence for the draft: an SPDX id or free text' })
|
|
334
|
+
.option('description', { type: 'string', describe: 'one or two sentences about what it knows' })
|
|
335
|
+
.option('drop-lineage', { type: 'boolean', default: false, describe: 'import as a ROOT even though the lesson names a base this node does not have — no credit and no royalty to the base creator' })
|
|
336
|
+
.example('$0 patch import ./lesson-pixelplus-1a2b3c.npz --recipe ./recipe.json', `then: ${PROG} patch apply taught-pixelplus-1a2b3c`), run((ctx, a) => patch.patchImport(ctx, a)))
|
|
337
|
+
.command('announce <id>', 'DRAFT → ANNOUNCED (anchor on the ledger)', (yy) => yy.positional('id', { type: 'string', demandOption: true })
|
|
338
|
+
// Item 248 — the same flag DECLARES a replacement whose rows do not overlap (the daily case), and
|
|
339
|
+
// --keep-others keeps a dated snapshot listed on purpose instead of letting the overlap rule retire it.
|
|
340
|
+
.option('supersede', { type: 'string', array: true, describe: 'the listing(s) of yours this version replaces — it refuses until every one the overlap rule found is named, and names one it did not' })
|
|
341
|
+
.option('keep-others', { type: 'boolean', default: false, describe: 'retire nothing: every overlapping listing of yours stays on sale (a dated snapshot published on purpose)' })
|
|
342
|
+
.example('$0 patch announce krx-codes-v3 --supersede krx-codes-v2', 'v2 goes off sale the moment v3 is verified')
|
|
343
|
+
.example('$0 patch announce krx-codes-2026-09-04 --supersede krx-codes-2026-09-03', 'today replaces yesterday even though their rows do not overlap')
|
|
344
|
+
.example('$0 patch announce krx-snapshot-2026-09-01 --keep-others', 'a dated snapshot that retires nothing'), run((ctx, a) => patch.patchAnnounce(ctx, a.id, { supersede: a.supersede, keepOthers: a['keep-others'] })))
|
|
345
|
+
.command('retire <id>', 'Take your published knowledge off sale for good (the record stays; buyers keep their copy)', (yy) => yy
|
|
346
|
+
.positional('id', { type: 'string', demandOption: true })
|
|
347
|
+
.option('reason', { type: 'string', describe: 'why, in one line — shown to anyone who asks for it afterwards' })
|
|
348
|
+
.example('$0 patch retire krx-codes-2026-08 --reason "the source feed changed; use krx-codes-2026-09"', ''), run((ctx, a) => patch.patchRetire(ctx, a.id, { reason: a.reason })))
|
|
349
|
+
.command('verify <id>', 'Run this node\'s verifier on a patch and publish an attestation', (yy) => yy
|
|
350
|
+
.positional('id', { type: 'string', demandOption: true })
|
|
351
|
+
// Item 339 — a verifier with a doubt had two options: stay silent, or challenge, which takes the seller off
|
|
352
|
+
// sale. Most operators stay silent, which is the opposite of what the trust story needs.
|
|
353
|
+
.option('recheck', { type: 'boolean', describe: 'measure it again and record the result WITHOUT taking it off sale — a failing recheck withdraws this node\'s earlier PASS, a passing one confirms it' })
|
|
354
|
+
.example('$0 patch verify krx-codes-2026-08 --recheck', 'you doubt a result you signed: re-measure it and put that on the record, instead of challenging the seller'), run((ctx, a) => patch.patchVerify(ctx, a.id, { recheck: a.recheck })))
|
|
355
|
+
.command('challenge <id>', 'Dispute a verification: takes the knowledge off sale until a verifier re-runs it', (yy) => yy.positional('id', { type: 'string', demandOption: true })
|
|
356
|
+
.option('reason', { type: 'string', demandOption: true, describe: 'why, in one line — it goes on the public record next to your address' }), run((ctx, a) => patch.patchChallenge(ctx, a.id, a.reason)))
|
|
357
|
+
.command('buy <ids..>', 'Buy listed knowledge via HTTP 402 (x402) and download the body — several ids buy them in the order given', (yy) => yy
|
|
358
|
+
.positional('ids', { type: 'string', array: true, demandOption: true, describe: 'knowledge id(s) — `a b` or `a,b`, bought in the order given' })
|
|
359
|
+
.option('apply', { type: 'boolean', default: false, describe: 'apply to the serving runtime after download' })
|
|
360
|
+
// Item 102: the price is quoted and confirmed before anything is spent. `--yes` answers in advance; a
|
|
361
|
+
// non-terminal without it is refused, never taken as a yes. `--max-price` is the FAMILY total (item 270).
|
|
362
|
+
.option('yes', { alias: 'y', type: 'boolean', default: false, describe: 'skip the confirmation (answer yes in advance)' })
|
|
363
|
+
.option('max-price', { type: 'number', describe: 'refuse if the total for one knowledge (it + the bases it needs) is above this' })
|
|
364
|
+
.option('bundle', { type: 'boolean', default: false, describe: 'buy the bases this knowledge needs underneath it too, deepest first (one payment each). Without it you are asked' })
|
|
365
|
+
.option('with-base', { type: 'boolean', default: false, describe: 'the older name of --bundle', hidden: true })
|
|
366
|
+
// Item 271: without this, a knowledge this node has already paid for is collected, not bought a second time.
|
|
367
|
+
.option('again', { type: 'boolean', default: false, describe: 'pay again for something this node already bought (per-hit / per-apply-hour billing)' })
|
|
368
|
+
// Item 236: a superseded version is bought on purpose or not at all — a script must not stack last month's build in silence.
|
|
369
|
+
.option('allow-superseded', { type: 'boolean', default: false, describe: 'buy a version that has been superseded by a newer one on the same subject (otherwise you are asked)' })
|
|
370
|
+
.example('$0 patch buy krx-all-2761', 'quote the price, ask, then pay')
|
|
371
|
+
.example('$0 patch buy krx-all-2761 pixelplus-087600', 'two knowledges, one quote and one confirmation each')
|
|
372
|
+
.example('$0 patch buy krx-all-2761 --bundle', 'the add-on and the knowledge it needs underneath, in one go')
|
|
373
|
+
.example('$0 patch buy krx-all-2761 --yes --max-price 30', 'unattended, with a budget for the whole family'), run((ctx, a) => {
|
|
374
|
+
const ids = patch.parseIds(a.ids);
|
|
375
|
+
return patch.overIds(ctx, ids, 'bought', (id, batched) => patch.patchBuy(ctx, id, { apply: a.apply, yes: a.yes, again: a.again, maxPrice: a['max-price'], withRequired: a.bundle || a['with-base'], allowSuperseded: a['allow-superseded'], batched }));
|
|
376
|
+
}))
|
|
377
|
+
// Item 278: an anchor is immutable, and before this the only way to re-price a listing was to publish a new one
|
|
378
|
+
// that superseded it — restarting verification and splitting the sales history to run a discount.
|
|
379
|
+
.command('price <id> <price>', 'Change what a published knowledge sells for (0 makes it free)', (yy) => yy
|
|
380
|
+
.positional('id', { type: 'string', demandOption: true, describe: 'the knowledge to re-price (you must be its author)' })
|
|
381
|
+
.positional('price', { type: 'string', demandOption: true, describe: `the new price in this node's currency, e.g. 2.5 — "0" makes it free` })
|
|
382
|
+
.option('reason', { type: 'string', describe: 'why, in your words — shown to buyers on the price history' })
|
|
383
|
+
.option('yes', { alias: 'y', type: 'boolean', default: false, describe: 'skip the confirmation' })
|
|
384
|
+
.example('$0 patch price krx-all-2761 1.5 --reason "launch price"', 'a discount, on the public record')
|
|
385
|
+
.example('$0 patch price krx-all-2761 0', 'make an obsolete knowledge free'), run((ctx, a) => patch.patchPrice(ctx, a.id, a.price, { reason: a.reason, yes: a.yes })))
|
|
386
|
+
.command('download <id>', 'Collect a knowledge this node already paid for — no second payment', (yy) => yy.positional('id', { type: 'string', demandOption: true })
|
|
387
|
+
.example('$0 patch download krx-all-2761', 'after a lost manifest, a forgotten body or a purchase that died mid-payment'), run((ctx, a) => patch.patchDownload(ctx, a.id)))
|
|
388
|
+
.command('apply <ids..>', 'Load held knowledge into the serving model (no restart) — several ids load in the order given, the last winning on any entry they share', (yy) => yy.positional('ids', { type: 'string', array: true, demandOption: true, describe: 'knowledge id(s) — `a b` or `a,b`, loaded in the order given' })
|
|
389
|
+
.option('with-base', { type: 'boolean', default: false, describe: 'also load everything this knowledge was trained on top of, underneath it' })
|
|
390
|
+
.example('$0 patch apply krx-all-2761 pixelplus-087600', 'the set, in that order'), run((ctx, a) => patch.overIds(ctx, patch.parseIds(a.ids), 'loaded', (id, batched) => patch.patchApply(ctx, id, { withBase: a.withBase, batched }))))
|
|
391
|
+
.command('remove <ids..>', 'Unload knowledge from the serving model, putting back whatever was underneath', (yy) => yy.positional('ids', { type: 'string', array: true, demandOption: true, describe: 'knowledge id(s) — `a b` or `a,b`' })
|
|
392
|
+
.option('cascade', { type: 'boolean', default: false, describe: 'also unload everything that is loaded on top of it' }), run((ctx, a) => patch.overIds(ctx, patch.parseIds(a.ids), 'unloaded', (id, batched) => patch.patchRemove(ctx, id, { cascade: a.cascade, batched }))))
|
|
393
|
+
.command('stack', 'What is loaded in the serving model, bottom first', (yy) => yy, run((ctx) => patch.patchStack(ctx)))
|
|
394
|
+
.command('fork <id>', 'Copy this knowledge\'s questions into your own training set, and continue from there', (yy) => yy
|
|
395
|
+
.positional('id', { type: 'string', demandOption: true })
|
|
396
|
+
.option('name', { type: 'string', describe: 'name for your copy' })
|
|
397
|
+
.option('key-file', { type: 'string', describe: 'teaching key file (default: <home>/teaching-key.json)' })
|
|
398
|
+
.option('key', { type: 'string', describe: 'teaching key as hex / json (or AINIZE_TEACH_KEY)' })
|
|
399
|
+
.example('$0 patch fork krx-all-2761 --name "KRX + biotech"', 'start from its questions')
|
|
400
|
+
.example('$0 teach train <dataset> --on krx-all-2761', 'then teach your additions on top of it'), run((ctx, a) => teachData.patchFork(ctx, a.id, { name: a.name, key: a.key, keyFile: a['key-file'] })))
|
|
401
|
+
.command('merge <a> <b>', 'Combine two knowledges into one: what overlaps, what they answer differently, and how to build it', (yy) => yy
|
|
402
|
+
.positional('a', { type: 'string', demandOption: true, describe: 'the first knowledge' })
|
|
403
|
+
.positional('b', { type: 'string', demandOption: true, describe: 'the second one — where they disagree, this one is the alternative answer' })
|
|
404
|
+
.option('preview', { type: 'boolean', default: false, describe: 'only measure: questions, rows and which builds are possible' })
|
|
405
|
+
.option('resolve', { type: 'string', describe: 'JSON file of {"<question key>": "a" | "b" | "drop" | {"answer": "…"}}' })
|
|
406
|
+
.option('tier', { type: 'string', choices: ['union', 'retrain', 'rebuild'], describe: 'union = just combine (no training) · retrain = teach the disagreeing questions on top of both · rebuild = train everything from the combined questions' })
|
|
407
|
+
.option('name', { type: 'string', describe: 'name for the combined knowledge' })
|
|
408
|
+
.option('wait', { type: 'boolean', default: false, describe: 'wait for the build and exit with its status' })
|
|
409
|
+
.option('key-file', { type: 'string', describe: 'teaching key file (default: <home>/teaching-key.json)' })
|
|
410
|
+
.option('key', { type: 'string', describe: 'teaching key as hex / json (or AINIZE_TEACH_KEY)' })
|
|
411
|
+
.example('$0 patch merge krx-all-2761 pixelplus --preview', 'what combining them would mean')
|
|
412
|
+
.example('$0 patch merge krx-all-2761 pixelplus --resolve answers.json --tier retrain', 'after choosing an answer for each disagreement (unresolved ones print as JSON, exit 3)'), run((ctx, a) => teachData.patchMerge(ctx, a.a, a.b, { preview: a.preview, resolve: a.resolve, tier: a.tier, name: a.name, wait: a.wait, key: a.key, keyFile: a['key-file'] })))
|
|
413
|
+
.command('tree <id>', 'The family tree: what this was built on, what was built on it, and what each one added', (yy) => yy
|
|
414
|
+
.positional('id', { type: 'string', demandOption: true })
|
|
415
|
+
.option('depth', { type: 'number', default: 4, describe: 'how many hops in each direction (max 8)' })
|
|
416
|
+
.option('dir', { type: 'string', choices: ['up', 'down', 'both'], default: 'both', describe: 'ancestors, descendants, or both' })
|
|
417
|
+
.example('$0 patch tree krx-all-2761 --depth 6', 'the whole line, with what each knowledge added'), run((ctx, a) => patch.patchTree(ctx, a.id, { depth: a.depth, dir: a.dir })))
|
|
418
|
+
.command('missing <id>', 'Open questions: what people asked this knowledge that it could not answer', (yy) => yy
|
|
419
|
+
.positional('id', { type: 'string', demandOption: true })
|
|
420
|
+
.option('kind', { type: 'string', choices: ['own_miss', 'preflight', 'free_wrong', 'request', 'gap'], describe: 'only one source' })
|
|
421
|
+
.option('all', { type: 'boolean', default: false, describe: 'include the ones a later knowledge already answered' })
|
|
422
|
+
.option('limit', { type: 'number', default: 50, describe: 'how many questions to print' })
|
|
423
|
+
.example('$0 patch missing krx-all-2761', 'what to add on top of it'), run((ctx, a) => patch.patchMissing(ctx, a.id, { kind: a.kind, all: a.all, limit: a.limit })))
|
|
424
|
+
.command('signals <id>', 'How a knowledge is doing: network facts, and this node\'s last 30 days', (yy) => yy.positional('id', { type: 'string', demandOption: true }), run((ctx, a) => patch.patchSignals(ctx, a.id)))
|
|
425
|
+
.command('conflicts <id>', 'Address-set overlaps with other patches', (yy) => yy.positional('id', { type: 'string', demandOption: true }), run((ctx, a) => patch.patchConflicts(ctx, a.id)))
|
|
426
|
+
.command('records <id>', 'Ledger records about a patch', (yy) => yy.positional('id', { type: 'string', demandOption: true }), run((ctx, a) => patch.patchRecords(ctx, a.id)))
|
|
427
|
+
.command('rm <id>', 'Delete a draft (says what goes, and asks first)', (yy) => yy.positional('id', { type: 'string', demandOption: true, describe: 'draft id (`$0 patch ls --drafts`)' })
|
|
428
|
+
.option('yes', { alias: 'y', type: 'boolean', default: false, describe: 'answer the confirmation in advance (a script has no terminal to be asked in)' }), run((ctx, a) => patch.patchRm(ctx, a.id, { yes: a.yes })))
|
|
429
|
+
.command('forget <id>', 'Delete this node\'s copy of the knowledge file. NOT a takedown: it stays listed and the gateway keeps charging — use `patch retire` for that', (yy) => yy
|
|
430
|
+
.positional('id', { type: 'string', demandOption: true })
|
|
431
|
+
.option('all-sharing', { type: 'boolean', default: false, describe: 'also stop serving every other knowledge built from the same file (the command lists them first)' }), run((ctx, a) => patch.patchForget(ctx, a.id, { allSharing: a['all-sharing'] })))
|
|
432
|
+
.demandCommand(1, 'Subcommand is required (ls|get|publish|import|announce|retire|verify|challenge|buy|download|apply|remove|stack|fork|merge|tree|missing|signals|conflicts|records|rm|forget).'), () => undefined);
|
|
433
|
+
// ---------------------------------------------------------------- one-liners (publish / use)
|
|
434
|
+
cli.command('publish <file>', `One line to sell knowledge: register a .npz + benchmark and announce it at once — the network verifies, you get paid per sale (\`${PROG} patch publish\` is the same operation, stopping at a draft)`, (y) => publishOpts(fail(y))
|
|
435
|
+
.positional('file', { type: 'string', demandOption: true, describe: 'path to the learned knowledge (.npz: addrs/before/after)' })
|
|
436
|
+
.option('announce', { type: 'boolean', default: true, describe: `announce immediately — the permanent record, and the one step with no undo (--no-announce keeps a draft, which is what \`${PROG} patch publish\` does by default)` })
|
|
437
|
+
.example('$0 publish ./my-knowledge.npz --name "KRX ticker codes" --model Qwen3.8-Flash-Next --benchmark ./bench.json --price 25', '')
|
|
438
|
+
.example('$0 publish ./lesson.npz --name "…" --model … --benchmark ./bench.json --contributor 0xAbC…:Alice:0.7', 'Alice (data provider) gets 70 % of your share of every sale'), publishRun);
|
|
439
|
+
// ---------------------------------------------------------------- teach (visitor-taught lessons)
|
|
440
|
+
/**
|
|
441
|
+
* The teaching key every teach request is signed with. There is no account: the key IS the identity. Without one of
|
|
442
|
+
* these the CLI keeps its own at `<AINIZE_HOME>/teaching-key.json` and creates it on first use.
|
|
443
|
+
*/
|
|
444
|
+
const keyOpts = (y) => y
|
|
445
|
+
.option('key', { type: 'string', describe: 'teaching key (64-hex) — or AINIZE_TEACH_KEY' })
|
|
446
|
+
.option('key-file', { type: 'string', describe: 'the key backup JSON from the browser (ainize-teaching-key-….json); default: <home>/teaching-key.json, created on first use' });
|
|
447
|
+
cli.command('teach', 'Teach mode: turn your own questions and answers into knowledge. Two doors, one pipeline — a dataset file here, or corrections collected in the browser (<node>/chat?teach=1)', (y) => fail(y)
|
|
448
|
+
.command('status [target]', 'Teaching policy of a node, the status of a lesson, or a data provider\'s lessons and earnings', (yy) => yy
|
|
449
|
+
.positional('target', { type: 'string', describe: 'node URL · lesson URL (…/chat?lesson=<id>) or job id · teacher page (…/teacher/<address>) or 0x address; default: this node' })
|
|
450
|
+
.option('key', { type: 'string', describe: 'teaching key (64-hex) — or AINIZE_TEACH_KEY; shows the full lesson body for your own lessons' })
|
|
451
|
+
.option('key-file', { type: 'string', describe: 'the key backup JSON downloaded from the browser (ainize-teaching-key-….json)' })
|
|
452
|
+
.example('$0 teach status http://localhost:3402', 'is this node accepting lessons? publish mode, trainer, queue')
|
|
453
|
+
.example('$0 teach status "http://localhost:3402/chat?lesson=8f0c…" --key-file ainize-teaching-key-1a2b3c4d.json', 'your lesson: progress, checks, before/after')
|
|
454
|
+
.example('$0 teach status http://localhost:3402/teacher/0xAbC…', 'a data provider\'s lessons and earnings'), run((ctx, a) => teach.teachStatus(ctx, a.target, { key: a.key, keyFile: a['key-file'] }), false, namesItsOwnNode))
|
|
455
|
+
// ---- teach mode v2: the file door. One pipeline (dataset → validate → train → check → lesson), two entry points.
|
|
456
|
+
.command('dataset', 'The questions a lesson is trained from: upload a file, list, inspect, download, delete', (yy) => yy
|
|
457
|
+
.command(['upload <file>', '$0 <file>'], 'Validate a dataset file and upload it (nothing is trained until you say so)', (z) => keyOpts(z)
|
|
458
|
+
.positional('file', { type: 'string', demandOption: true, describe: '.jsonl · .json · .csv · .tsv · .txt with one question and its answer per row' })
|
|
459
|
+
.option('name', { type: 'string', describe: 'name for the dataset (default: the file name)' })
|
|
460
|
+
.option('format', { choices: ['jsonl', 'json', 'csv', 'tsv', 'txt'], describe: 'override the detected format' })
|
|
461
|
+
.option('delimiter', { type: 'string', describe: 'csv/tsv separator when it is not detected (e.g. ";" or "\\t")' })
|
|
462
|
+
.option('header', { type: 'boolean', describe: '--no-header when the first row is already a question' })
|
|
463
|
+
.option('columns', { type: 'string', describe: 'JSON mapping when the column names are unusual: \'{"prompt":0,"answer":2}\'' })
|
|
464
|
+
.option('encoding', { type: 'string', describe: 'force an encoding (utf-8, euc-kr, …) when the preview looks like mojibake' })
|
|
465
|
+
.option('retention', { choices: ['keep', 'delete_after_training'], describe: 'delete_after_training removes the questions from this node as soon as the lesson finishes' })
|
|
466
|
+
.option('train', { type: 'boolean', default: false, describe: 'queue a lesson from it right away' })
|
|
467
|
+
.option('effort', { choices: ['quick', 'balanced', 'thorough'], describe: 'with --train: how hard to train' })
|
|
468
|
+
.option('check', { type: 'boolean', describe: 'with --train: --no-check skips the side-effect check (publishing then stays blocked)' })
|
|
469
|
+
.option('rows', { type: 'number', describe: 'with --train: train only the first N questions' })
|
|
470
|
+
.option('wait', { type: 'boolean', default: false, describe: 'with --train: follow the lesson until it is ready and exit with its outcome (0 ready · 4 did not stick · 5 failed · 6 declined · 7 timed out · 8 never measured) — the same wait as `teach train --wait`' })
|
|
471
|
+
.option('timeout', { type: 'number', describe: 'with --wait: give up after this many minutes and exit 7 (default 60)' })
|
|
472
|
+
.example('$0 teach dataset ./questions.csv', 'validate + upload, print every line that will not train')
|
|
473
|
+
.example('$0 teach dataset ./qa.jsonl --train --effort thorough', 'upload and teach it in one line')
|
|
474
|
+
.example('$0 teach dataset ./data.csv --columns \'{"prompt":"질문","answer":"답"}\'', 'unusual column names')
|
|
475
|
+
.example('$0 teach dataset ./today.csv --train --wait && $0 teach publish <id> …', 'a nightly bake that only publishes when the lesson stuck'), run((ctx, a) => teachData.datasetUpload(ctx, a.file, { key: a.key, keyFile: a['key-file'], name: a.name, format: a.format, delimiter: a.delimiter, header: a.header, columns: a.columns, encoding: a.encoding, retention: a.retention, train: a.train, effort: a.effort, check: a.check, rows: a.rows, wait: a.wait, timeout: a.timeout })))
|
|
476
|
+
.command('ls', 'My datasets on this node', (z) => keyOpts(z), run((ctx, a) => teachData.datasetLs(ctx, { key: a.key, keyFile: a['key-file'] })))
|
|
477
|
+
.command(['get <id>', 'download <id>'], 'One dataset: every source line with the reason it was or was not used; -o writes the questions to a file', (z) => keyOpts(z)
|
|
478
|
+
.positional('id', { type: 'string', demandOption: true })
|
|
479
|
+
.option('out', { alias: 'o', type: 'string', describe: 'write the questions to this file (re-uploading it lands on the same dataset)' })
|
|
480
|
+
.option('format', { choices: ['jsonl', 'csv'], default: 'jsonl', describe: 'download format (the .jsonl bytes are the fingerprint subject)' })
|
|
481
|
+
.option('rows', { type: 'number', describe: 'how many source lines to print (default 50, max 200)' })
|
|
482
|
+
.option('offset', { type: 'number', describe: 'start at this source line' })
|
|
483
|
+
.option('status', { type: 'string', describe: 'only lines with this status: ok|rejected|duplicate|conflict|too_long|empty|blocked|not_parsed|over_cap' })
|
|
484
|
+
.option('all', { type: 'boolean', default: false, describe: 'print every line, not only the ones that will not train' })
|
|
485
|
+
.example('$0 teach dataset get 6f2c… -o questions.jsonl', 'exactly what a lesson was trained on'), run((ctx, a) => teachData.datasetGet(ctx, a.id, { key: a.key, keyFile: a['key-file'], out: a.out, format: a.format, rows: a.rows, offset: a.offset, status: a.status, all: a.all })))
|
|
486
|
+
.command('rm <id>', 'Delete a dataset (the lessons trained from it are kept)', (z) => keyOpts(z).positional('id', { type: 'string', demandOption: true }), run((ctx, a) => teachData.datasetRm(ctx, a.id, { key: a.key, keyFile: a['key-file'] })))
|
|
487
|
+
.demandCommand(1, 'Give a dataset file, or a subcommand (ls|get|rm).'), () => undefined)
|
|
488
|
+
.command('train <target>', 'Teach a lesson from a dataset id or a dataset file', (yy) => keyOpts(yy)
|
|
489
|
+
.positional('target', { type: 'string', demandOption: true, describe: `dataset id (\`${PROG} teach dataset ls\`) or a dataset file, which is uploaded first` })
|
|
490
|
+
.option('effort', { choices: ['quick', 'balanced', 'thorough'], describe: `how hard to train (see \`${PROG} teach status <node>\`)` })
|
|
491
|
+
.option('check', { type: 'boolean', describe: '--no-check skips the side-effect check on the live model (publishing then stays blocked until a recheck)' })
|
|
492
|
+
.option('alt', { type: 'boolean', describe: '--no-alt trains only the wording in the file, not the second phrasing' })
|
|
493
|
+
.option('rows', { type: 'number', describe: 'train only the first N questions of the dataset' })
|
|
494
|
+
.option('name', { type: 'string', describe: 'name for the lesson (and for the dataset, when a file is uploaded here)' })
|
|
495
|
+
.option('patch', { type: 'string', describe: 'knowledge id(s) loaded while teaching, comma-separated — for comparison only' })
|
|
496
|
+
.option('on', { type: 'string', describe: 'the knowledge this lesson is trained ON TOP OF: its questions are kept as known answers, it is recorded as the base, and buyers need it too' })
|
|
497
|
+
.option('inherit', { type: 'boolean', describe: '--no-inherit checks against the base without keeping its questions as known answers' })
|
|
498
|
+
.option('yes-change', { type: 'boolean', default: false, describe: 'my answers are meant to replace the base\'s where they differ' })
|
|
499
|
+
.option('wait', { type: 'boolean', default: false, describe: 'follow it until it is ready (prints each stage). Exit code says what happened: 0 ready · 4 did not stick (NEEDS_MORE) · 5 failed/cancelled/expired · 6 declined by the operator · 7 still running when the wait ran out · 8 ready but never measured on the live model' })
|
|
500
|
+
.option('timeout', { type: 'number', describe: 'with --wait: give up after this many minutes and exit 7 (default 60)' })
|
|
501
|
+
.example('$0 teach train 6f2c1b2a-…', 'train an uploaded dataset')
|
|
502
|
+
.example('$0 teach train ./questions.csv --effort quick --wait', 'file → lesson in one line')
|
|
503
|
+
.example('$0 teach train 6f2c1b2a-… --on krx-all-2761', 'teach it on top of someone else\'s knowledge')
|
|
504
|
+
.example('$0 teach train 6f2c1b2a-… --effort thorough', 'the same questions again, harder'), run((ctx, a) => teachData.teachTrain(ctx, a.target, { key: a.key, keyFile: a['key-file'], effort: a.effort, check: a.check, alt: a.alt, rows: a.rows, name: a.name, patch: a.patch, on: a.on, inherit: a.inherit, yesChange: a['yes-change'], wait: a.wait, timeout: a.timeout })))
|
|
505
|
+
.command('jobs', 'My lessons on this node and the dataset each came from', (yy) => keyOpts(yy)
|
|
506
|
+
.option('dataset', { type: 'string', describe: 'only lessons trained from this dataset' }), run((ctx, a) => teachData.teachJobs(ctx, { key: a.key, keyFile: a['key-file'], dataset: a.dataset })))
|
|
507
|
+
// Item 245 — a lesson saved unchecked because the model server was down cannot be published, and the only way to
|
|
508
|
+
// measure it again was a button in the browser.
|
|
509
|
+
.command('recheck <job-id>', 'Measure a lesson that was saved unchecked (the model server was unavailable)', (yy) => keyOpts(yy)
|
|
510
|
+
.positional('job-id', { type: 'string', demandOption: true, describe: `lesson id (\`${PROG} teach jobs\`)` })
|
|
511
|
+
.option('wait', { type: 'boolean', default: false, describe: 'follow it until it is measured (same exit codes as `teach train --wait`)' })
|
|
512
|
+
.example('$0 teach recheck 3a417bb4-… --wait', 'the morning after a night when the model server was off'), run((ctx, a) => teachData.teachRecheck(ctx, a['job-id'], { key: a.key, keyFile: a['key-file'], wait: a.wait })))
|
|
513
|
+
// ---- item 238: the last step of the loop, which only the browser could do. The consents are the publisher's own
|
|
514
|
+
// and are never defaulted: without both flags the command refuses and quotes what is being consented to.
|
|
515
|
+
.command('publish <job-id>', 'Publish a READY lesson as knowledge (the last step of `teach train` — needs both consent flags)', (yy) => keyOpts(yy)
|
|
516
|
+
.positional('job-id', { type: 'string', demandOption: true, describe: `lesson id (\`${PROG} teach jobs\`)` })
|
|
517
|
+
.option('name', { type: 'string', demandOption: true, describe: 'what buyers see, 2-80 characters' })
|
|
518
|
+
.option('price', { type: 'string', describe: 'price per download in this node\'s currency (default 0 = free)' })
|
|
519
|
+
.option('license', { type: 'string', describe: 'licence for the knowledge (CC-BY-4.0, CC0-1.0, Proprietary, …)' })
|
|
520
|
+
.option('description', { type: 'string', describe: 'one or two sentences about what it knows' })
|
|
521
|
+
.option('payout', { type: 'string', describe: 'AIN address to be paid at, or `none` for credit without payment (default: this teaching key)' })
|
|
522
|
+
.option('access', { choices: ['public', 'derivative', 'private'], describe: 'who may read the training set: anyone, only people who declare they build on this (default), nobody' })
|
|
523
|
+
.option('dataset-license', { type: 'string', describe: 'licence for the questions themselves' })
|
|
524
|
+
.option('include-notes', { type: 'boolean', default: false, describe: 'include your per-row notes in the shared questions' })
|
|
525
|
+
.option('declare', { choices: ['own', 'public', 'licensed'], describe: 'where the questions came from: your own work, a public source, or licensed to you (the node requires this above a few hundred rows)' })
|
|
526
|
+
.option('consent-permanent', { type: 'boolean', default: false, describe: 'I understand this becomes a permanent public record that cannot be edited or deleted' })
|
|
527
|
+
.option('consent-rights', { type: 'boolean', default: false, describe: 'I have the right to share this information, and it is not private or personal data' })
|
|
528
|
+
.example('$0 teach publish 8f0c… --name "KRX codes" --price 2 --consent-permanent --consent-rights', 'the last line of a nightly bake')
|
|
529
|
+
.example('$0 teach train today.jsonl --wait && $0 teach publish <id> --name … --consent-permanent --consent-rights', 'train, then publish only if the lesson stuck (--wait exits non-zero otherwise)'), run((ctx, a) => teachData.teachPublish(ctx, a['job-id'], {
|
|
530
|
+
key: a.key, keyFile: a['key-file'], name: a.name, price: a.price, license: a.license, description: a.description, payout: a.payout,
|
|
531
|
+
access: a.access, datasetLicense: a['dataset-license'], includeNotes: a['include-notes'], declare: a.declare,
|
|
532
|
+
consentPermanent: a['consent-permanent'], consentRights: a['consent-rights'],
|
|
533
|
+
})))
|
|
534
|
+
.demandCommand(1, 'Subcommand is required (status|dataset|train|publish|jobs).'), () => undefined);
|
|
535
|
+
// ---------------------------------------------------------------- dataset (the questions behind a published knowledge)
|
|
536
|
+
cli.command('dataset', 'Import a Hugging Face dataset, or inspect the training set of published knowledge', (y) => fail(y)
|
|
537
|
+
.command(['import <url>', '$0 <url>'], 'Import an existing Hugging Face dataset into this node (no Hub publication)', (builder) => keyOpts(builder)
|
|
538
|
+
.positional('url', { type: 'string', demandOption: true, describe: 'https://huggingface.co/datasets/<owner>/<name> or a /resolve/<revision>/<file> URL' })
|
|
539
|
+
.option('config', { type: 'string', describe: 'Hugging Face configuration/subset; required when there are several' })
|
|
540
|
+
.option('split', { type: 'string', describe: 'dataset split (train when available, otherwise the only split)' })
|
|
541
|
+
.option('revision', { type: 'string', describe: 'source revision, resolved to a commit SHA before import' })
|
|
542
|
+
.option('file', { type: 'string', describe: 'import a JSONL/JSON/CSV/TSV/TXT file at that revision, without the dataset viewer' })
|
|
543
|
+
.option('limit', { type: 'number', describe: 'explicit viewer row limit (1..10000); without it, imports the whole split up to 10000 rows' })
|
|
544
|
+
.option('offset', { type: 'number', describe: 'first viewer row (default 0)' })
|
|
545
|
+
.option('columns', { type: 'string', describe: 'column mapping, e.g. {"prompt":"question","answer":"answer"}' })
|
|
546
|
+
.option('name', { type: 'string', describe: 'name of the imported dataset on this node' })
|
|
547
|
+
.option('hf-token-file', { type: 'string', describe: 'private token file for a restricted HF dataset; never sent to the Ainize node' })
|
|
548
|
+
.option('train', { type: 'boolean', default: false, describe: 'also queue a lesson from the imported dataset' })
|
|
549
|
+
.option('effort', { choices: ['quick', 'balanced', 'thorough'], describe: 'training effort with --train' })
|
|
550
|
+
.option('wait', { type: 'boolean', default: false, describe: 'with --train: observe the same lesson until its terminal result' })
|
|
551
|
+
.option('timeout', { type: 'number', describe: 'with --wait: observation timeout in minutes; does not cancel the lesson' })
|
|
552
|
+
.example('$0 dataset https://huggingface.co/datasets/owner/qa --config default --split train', 'import existing data, without creating a Hugging Face repository')
|
|
553
|
+
.example('$0 dataset https://huggingface.co/datasets/owner/qa --file data/train.jsonl --train', 'import an immutable file and teach it'), run((ctx, args) => datasetImportHuggingFace(ctx, args.url, { ...args, hfTokenFile: args['hf-token-file'] })))
|
|
554
|
+
.command(['get <id>', 'download <id>'], 'The training set of a knowledge — what it is, and with -o the questions themselves', (yy) => keyOpts(yy)
|
|
555
|
+
.positional('id', { type: 'string', demandOption: true, describe: `knowledge id (\`${PROG} patch ls\`) or the sha256 of the training set` })
|
|
556
|
+
.option('out', { alias: 'o', type: 'string', describe: `write the questions to this file (.jsonl — re-uploadable with \`${PROG} teach dataset <file>\`)` })
|
|
557
|
+
.option('manifest', { type: 'boolean', default: false, describe: 'also print the manifest: row origin, licence, benchmark hash, PII scan, declaration' })
|
|
558
|
+
.option('include-notes', { type: 'boolean', default: false, describe: 'keep the publisher’s per-row notes in the written file (they are left out by default)' })
|
|
559
|
+
.example('$0 dataset get krx-all-2761', 'access, licence, where it came from, and the first questions')
|
|
560
|
+
.example('$0 dataset get krx-all-2761 -o questions.jsonl', 'the exact bytes, ready to build on'), run((ctx, a) => dataset.datasetGetPublished(ctx, a.id, { key: a.key, keyFile: a['key-file'], out: a.out, manifest: a.manifest, includeNotes: a['include-notes'] })))
|
|
561
|
+
.demandCommand(1, 'A Hugging Face URL or subcommand (import|get) is required.'), () => undefined);
|
|
562
|
+
cli.command('use <ids..>', 'One line to use knowledge: check it is verified → quote the price → pay → download → load into your model. Several ids are used in the order given', (y) => fail(y)
|
|
563
|
+
.positional('ids', { type: 'string', array: true, demandOption: true, describe: `knowledge id(s) — \`a b\` or \`a,b\`, in load order (see \`${PROG} patch ls\`)` })
|
|
564
|
+
.option('apply', { type: 'boolean', default: true, describe: 'load into the serving model after download (--no-apply to only download)' })
|
|
565
|
+
.option('yes', { alias: 'y', type: 'boolean', default: false, describe: 'skip the confirmation (answer yes in advance)' })
|
|
566
|
+
.option('max-price', { type: 'number', describe: 'refuse if the total for one knowledge (it + the bases it needs) is above this' })
|
|
567
|
+
.option('bundle', { type: 'boolean', default: false, describe: 'buy the bases this knowledge needs underneath it too (one payment each). Without it you are asked' })
|
|
568
|
+
.option('with-base', { type: 'boolean', default: false, describe: 'the older name of --bundle', hidden: true })
|
|
569
|
+
.option('again', { type: 'boolean', default: false, describe: 'pay again for something this node already bought (per-hit / per-apply-hour billing)' })
|
|
570
|
+
// Item 236: `use <id>` on a superseded id used to print a green tick and buy it anyway.
|
|
571
|
+
.option('allow-superseded', { type: 'boolean', default: false, describe: 'use a version that has been superseded by a newer one on the same subject (otherwise you are asked)' })
|
|
572
|
+
.example('$0 use krx-all-2761', 'quote, ask, pay, download, load')
|
|
573
|
+
.example('$0 use krx-all-2761 pixelplus-087600', 'two knowledges, loaded in that order')
|
|
574
|
+
.example('$0 use krx-all-2761 --yes --max-price 30', 'unattended, with a budget'), run((ctx, a) => patch.overIds(ctx, patch.parseIds(a.ids), a.apply === false ? 'bought' : 'loaded', (id, batched) => patch.patchUse(ctx, id, { apply: a.apply, yes: a.yes, again: a.again, maxPrice: a['max-price'], withRequired: a.bundle || a['with-base'], allowSuperseded: a['allow-superseded'], batched }))));
|
|
575
|
+
// ---------------------------------------------------------------- chat (live test)
|
|
576
|
+
cli.command('chat [patchId] [prompt..]', 'Live-test a knowledge patch: the model\'s answer before vs after the patch is loaded (correct-answer check)', (y) => fail(y)
|
|
577
|
+
.positional('patchId', { type: 'string', describe: 'patch to test (see --list); `a,b` loads several together' })
|
|
578
|
+
.positional('prompt', { type: 'string', array: true, describe: 'question; omit for an interactive session (/quit to exit)' })
|
|
579
|
+
.option('list', { alias: 'l', type: 'boolean', default: false, describe: 'list patches testable on this node and the runtime state' })
|
|
580
|
+
.option('patch', { alias: 'p', type: 'string', describe: 'knowledge to load together, comma-separated (up to 3, in load order; the last wins where they overlap)' })
|
|
581
|
+
.option('mode', { alias: 'm', choices: ['base', 'patched', 'compare'], default: 'compare', describe: 'base = model only, patched = with the patch loaded, compare = both' })
|
|
582
|
+
.option('thinking', { type: 'boolean', default: false, describe: 'let the model think first and show its reasoning' })
|
|
583
|
+
.option('max-tokens', { type: 'number', default: 200, describe: 'answer length limit (1–1024)' })
|
|
584
|
+
.check((a) => {
|
|
585
|
+
// the help has always said 1–1024; check it here instead of letting the node answer with a zod sentence
|
|
586
|
+
const n = a['max-tokens'];
|
|
587
|
+
if (n !== undefined && (!Number.isInteger(n) || n < 1 || n > 1024))
|
|
588
|
+
throw new Error(`--max-tokens must be a whole number between 1 and 1024 (got ${n})`);
|
|
589
|
+
return true;
|
|
590
|
+
})
|
|
591
|
+
.option('system', { type: 'string', describe: 'system prompt prepended to the conversation' })
|
|
592
|
+
.example('$0 chat --list', 'what can be tested here')
|
|
593
|
+
.example('$0 chat pixelplus-087600 "Pixelplus ticker code? Digits only."', 'before/after in one shot')
|
|
594
|
+
.example('$0 chat krx-all-2761 --mode patched', 'interactive session with the patch loaded')
|
|
595
|
+
.example('$0 chat --patch krx-all-2761,pixelplus-087600 "픽셀플러스 종목코드 알려줘. 숫자만."', 'two knowledges loaded together (up to 3)'), run(async (ctx, a) => {
|
|
596
|
+
// `--patch a,b` (or a comma-separated positional) selects the knowledge; with --patch the positional is part of the question.
|
|
597
|
+
const ids = a.patch ? chat.parsePatchIds(a.patch) : a.patchId ? chat.parsePatchIds(a.patchId) : [];
|
|
598
|
+
const promptParts = a.patch && a.patchId ? [a.patchId, ...(a.prompt ?? [])] : (a.prompt ?? []);
|
|
599
|
+
if (a.list || ids.length === 0) {
|
|
600
|
+
if (!a.list && ids.length === 0)
|
|
601
|
+
throw new CliError(`patch id required — \`${PROG} chat --list\` shows what this node can test`);
|
|
602
|
+
return chat.chatPatches(ctx);
|
|
603
|
+
}
|
|
604
|
+
const opts = { mode: a.mode, thinking: a.thinking, maxTokens: a['max-tokens'], system: a.system };
|
|
605
|
+
// D2: keep the prompt exactly as typed — this product's knowledge is trained on prompts that end with a space
|
|
606
|
+
// ("종목코드 픽셀플러스 "), so `ainize chat <id> "종목코드 픽셀플러스 "` must send that space too.
|
|
607
|
+
const prompt = promptParts.join(' ');
|
|
608
|
+
if (prompt.trim())
|
|
609
|
+
return chat.chat(ctx, ids, prompt, opts);
|
|
610
|
+
await chat.chatRepl(ctx, ids, opts);
|
|
611
|
+
process.exit(0);
|
|
612
|
+
}, true));
|
|
613
|
+
// ---------------------------------------------------------------- ledger
|
|
614
|
+
cli.command('ledger', 'Inspect the ledger', (y) => fail(y)
|
|
615
|
+
.command('ls', 'List records', (yy) => yy.option('kind', { choices: RECORD_KINDS, describe: 'only this kind of record' }).option('limit', { type: 'number', default: 50, describe: 'how many records, newest last' }), run((ctx, a) => ledger.ledgerLs(ctx, a)))
|
|
616
|
+
.command('verify', 'Verify hashes, signatures and chain linkage', (yy) => yy, run((ctx) => ledger.ledgerVerify(ctx)))
|
|
617
|
+
.command('graph', 'ASCII lineage tree', (yy) => yy, run((ctx) => ledger.ledgerGraph(ctx)))
|
|
618
|
+
.command('export <file>', 'Export records as JSON lines', (yy) => yy.positional('file', { type: 'string', demandOption: true }), run((ctx, a) => ledger.ledgerExport(ctx, a.file)))
|
|
619
|
+
.demandCommand(1, 'Subcommand is required (ls|verify|graph|export).'), () => undefined);
|
|
620
|
+
// ---------------------------------------------------------------- branches / routing / wallet
|
|
621
|
+
cli.command('branch', 'Knowledge branches (parallel, possibly contradictory patch sets)', (y) => fail(y)
|
|
622
|
+
.command('ls', 'List branches', (yy) => yy
|
|
623
|
+
.option('all', { type: 'boolean', default: false, describe: 'include test and archived tracks (hidden from every public list)' }), run((ctx, a) => branch.branchLs(ctx, { all: a.all })))
|
|
624
|
+
.command('create <name>', 'Create a branch', (yy) => yy.positional('name', { type: 'string', demandOption: true })
|
|
625
|
+
.option('description', { type: 'string', describe: 'what this track is for, in one line' })
|
|
626
|
+
.option('context', { type: 'string', array: true, describe: 'k=v routing attributes (e.g. jurisdiction=KR)' })
|
|
627
|
+
.option('patch', { type: 'string', array: true, describe: 'patch id(s) in the branch' })
|
|
628
|
+
.option('test', { type: 'boolean', default: false, describe: 'a fixture track: on the record, but off /network, off the router and out of `branch ls`' })
|
|
629
|
+
.example('$0 branch create law/KR --context jurisdiction=KR --patch law-kr-2025', ''), run((ctx, a) => branch.branchCreate(ctx, a.name, a)))
|
|
630
|
+
// Item 269 — a track could never be taken off the shelf, so the public list was 32 test tracks around three real ones.
|
|
631
|
+
.command('archive <name>', 'Take a track of yours off /network, the router and `branch ls` (the record and its subscribers stay)', (yy) => yy
|
|
632
|
+
.positional('name', { type: 'string', demandOption: true })
|
|
633
|
+
.example('$0 branch archive e2e/KR-1788174110', 'a fixture track written by a test run comes off the shelf'), run((ctx, a) => branch.branchArchive(ctx, a.name, true)))
|
|
634
|
+
.command('unarchive <name>', 'Put an archived track back on the lists', (yy) => yy.positional('name', { type: 'string', demandOption: true }), run((ctx, a) => branch.branchArchive(ctx, a.name, false)))
|
|
635
|
+
// Item 359: a track had no price object at all — the most loyal subscriber to a daily track was the most
|
|
636
|
+
// expensive customer, and nobody was paid to keep a channel good.
|
|
637
|
+
.command('terms <name>', 'What following your track costs, per period (the curation fee)', (yy) => yy
|
|
638
|
+
.positional('name', { type: 'string', demandOption: true, describe: 'a track you own' })
|
|
639
|
+
.option('price', { type: 'string', describe: `the fee per period in this node's currency ("0" = free to follow)` })
|
|
640
|
+
.option('period-days', { type: 'number', describe: 'how many days one payment covers (default 30)' })
|
|
641
|
+
.option('clear', { type: 'boolean', default: false, describe: 'remove the fee — the track becomes free to follow again' })
|
|
642
|
+
.example('$0 branch terms law/KR --price 5 --period-days 30', '5 a month for curating it; the knowledge on it is still bought from its publishers')
|
|
643
|
+
.example('$0 branch terms law/KR --clear', 'free to follow again'), run((ctx, a) => branch.branchTerms(ctx, a.name, { price: a.price, periodDays: a['period-days'], clear: a.clear })))
|
|
644
|
+
.command('add <name> <patchId>', 'Add knowledge to a track you own (verified knowledge only)', (yy) => yy
|
|
645
|
+
.positional('name', { type: 'string', demandOption: true, describe: `the track (see \`${PROG} branch ls\`)` })
|
|
646
|
+
.positional('patchId', { type: 'string', demandOption: true, describe: 'the knowledge to add — every subscriber buys and loads it' })
|
|
647
|
+
.option('force', { type: 'boolean', default: false, describe: 'add it even though it is not LISTED — every subscriber will buy and load it' }), run((ctx, a) => branch.branchAdd(ctx, a.name, a.patchId, { force: a.force })))
|
|
648
|
+
.command('quote <name>', 'What subscribing to this track would spend, item by item, before anything is spent', (yy) => yy.positional('name', { type: 'string', demandOption: true }), run((ctx, a) => branch.branchQuote(ctx, a.name)))
|
|
649
|
+
.command('subscribe <name>', 'Subscribe this node: buy the track\'s current knowledge, load it, and keep it up to date', (yy) => yy.positional('name', { type: 'string', demandOption: true })
|
|
650
|
+
.option('yes', { type: 'boolean', default: false, describe: 'answer the spend confirmation in advance' })
|
|
651
|
+
// Item 214 — the track is applied on top of what is loaded, and the model is last-wins on a shared row.
|
|
652
|
+
.option('replace', { type: 'boolean', default: false, describe: 'load the track even though it writes over knowledge already in the model (it answers instead of it on the shared rows)' })
|
|
653
|
+
.example('$0 branch subscribe daily/krx --yes', ''), run((ctx, a) => branch.branchSubscribe(ctx, a.name, 'subscribe', { yes: a.yes, replace: a.replace })))
|
|
654
|
+
.command('sync <name>', 'Bring a subscribed track up to date now (buy and load what it added, unload what it retired)', (yy) => yy.positional('name', { type: 'string', demandOption: true }), run((ctx, a) => branch.branchSync(ctx, a.name)))
|
|
655
|
+
.command('unsubscribe <name>', 'Unsubscribe (unload the track\'s knowledge; nothing is refunded)', (yy) => yy.positional('name', { type: 'string', demandOption: true }), run((ctx, a) => branch.branchSubscribe(ctx, a.name, 'unsubscribe')))
|
|
656
|
+
// Item 264 — a track was append-only: an unverified or rejected bake could be added and never taken off.
|
|
657
|
+
.command('rm <name> <patchId>', 'Take a knowledge off a track you own (subscribers stop buying and loading it)', (yy) => yy
|
|
658
|
+
.positional('name', { type: 'string', demandOption: true, describe: 'track name' })
|
|
659
|
+
.positional('patchId', { type: 'string', demandOption: true, describe: 'the knowledge to remove from it' })
|
|
660
|
+
.option('yes', { type: 'boolean', default: false, describe: 'answer the confirmation in advance' })
|
|
661
|
+
.example('$0 branch rm daily/krx krx-daily-2026-09-03', 'a bake that failed verification comes off the track'), run((ctx, a) => branch.branchRemove(ctx, a.name, a.patchId, { yes: a.yes })))
|
|
662
|
+
.demandCommand(1, 'Subcommand is required (ls|create|add|rm|archive|unarchive|quote|subscribe|sync|unsubscribe).'), () => undefined);
|
|
663
|
+
cli.command('route <context..>', 'Gateway routing: which track and which nodes serve a request context', (y) => fail(y).positional('context', { type: 'string', array: true, demandOption: true, describe: 'k=v pairs' })
|
|
664
|
+
// Item 234 — a track that answers only some of the attributes you named is not an answer unless you say so.
|
|
665
|
+
.option('partial', { type: 'boolean', default: false, describe: 'accept the closest track even though it does not match every attribute' })
|
|
666
|
+
.example('$0 route jurisdiction=KR', '')
|
|
667
|
+
.example('$0 route market=KRX freshness=daily --partial', 'route to the closest track when nothing matches both'), run((ctx, a) => branch.route(ctx, a.context, { partial: a.partial })));
|
|
668
|
+
// Item 320: every money verb was read-only or inward — a creator who had earned could spend it only by buying
|
|
669
|
+
// knowledge through this same node.
|
|
670
|
+
cli.command('wallet', 'Balance, sales, royalties and pending payouts of this node', (y) => fail(y)
|
|
671
|
+
.command(['show', '$0'], 'Balance, sales, royalties and pending payouts', (yy) => yy, run((ctx) => branch.wallet(ctx)))
|
|
672
|
+
.command('send <address> <amount>', 'Send AIN from this node\'s wallet to another address', (yy) => yy
|
|
673
|
+
.positional('address', { type: 'string', demandOption: true, describe: 'where the money goes (0x… AIN address)' })
|
|
674
|
+
.positional('amount', { type: 'number', demandOption: true, describe: 'how much, in AIN' })
|
|
675
|
+
.option('memo', { type: 'string', describe: 'a note for this node\'s own log (it does not travel with the transfer)' })
|
|
676
|
+
.option('yes', { alias: 'y', type: 'boolean', default: false, describe: 'skip the confirmation' })
|
|
677
|
+
.example('$0 wallet send 0xabc… 25', 'move 25 AIN of earnings to your own wallet'), run((ctx, a) => branch.walletSend(ctx, a.address, a.amount, { memo: a.memo, yes: a.yes }))), run((ctx) => branch.wallet(ctx)));
|
|
678
|
+
// `patch ls --mine` is the knowledge this node REGISTERED; a buyer's own purchases had no listing anywhere (items 216, 289).
|
|
679
|
+
cli.command('purchases', 'Knowledge this node bought: what, from whom, for how much, and whether it is loaded', (y) => fail(y)
|
|
680
|
+
.example('$0 purchases', 'every purchase with its seller, tx and file'), run((ctx) => patch.purchasesLs(ctx)));
|
|
681
|
+
cli.command('payouts', 'Royalty transfers this node owes creators and data providers (AIN ledger)', (y) => fail(y)
|
|
682
|
+
.command(['ls', '$0'], 'List payouts', (yy) => yy.option('status', { type: 'string', choices: ['pending', 'paid', 'failed'], describe: 'only payouts in this state' }).option('address', { type: 'string', describe: 'only this recipient' }).option('limit', { type: 'number', describe: 'how many rows (default: all of them)' })
|
|
683
|
+
.example('$0 payouts ls --status failed', ''), run((ctx, a) => branch.payoutsLs(ctx, a)))
|
|
684
|
+
.command('retry <id>', 'Retry one failed / pending payout now', (yy) => yy.positional('id', { type: 'number', demandOption: true, describe: `the payout row id (\`${PROG} payouts ls\`)` }), run((ctx, a) => branch.payoutRetry(ctx, a.id)))
|
|
685
|
+
// Item 313: the rows exist only in this node's SQLite and were never rebuilt from the settlements that created
|
|
686
|
+
// the debt — a wiped data dir or a crash between the record and the row left money owed and unpayable.
|
|
687
|
+
.command('reconcile', 'Rebuild the payouts this node owes from its own settlements, then pay what is due', (yy) => yy
|
|
688
|
+
.example('$0 payouts reconcile', 'after a restore, a crash, or an upgrade'), run((ctx) => branch.payoutsReconcile(ctx))), () => undefined);
|
|
689
|
+
// ---------------------------------------------------------------- drive
|
|
690
|
+
cli.command('drive', 'aindrive: files & change history of this node', (y) => fail(y)
|
|
691
|
+
.command('status', 'Drive status (pairing, agent, files)', (yy) => yy.option('files', { type: 'boolean', default: false, describe: 'also list the files in the drive folder' }), run((ctx, a) => drive.driveStatus(ctx, a)))
|
|
692
|
+
.command('up', 'Start the aindrive agent for the node\'s drive folder', (yy) => yy, run((ctx) => drive.driveAction(ctx, 'up')))
|
|
693
|
+
.command('stop', 'Stop the aindrive agent', (yy) => yy, run((ctx) => drive.driveAction(ctx, 'stop')))
|
|
694
|
+
.command('sync', 'Rewrite the drive mirror from the current market state', (yy) => yy, run((ctx) => drive.driveAction(ctx, 'sync')))
|
|
695
|
+
.command('login', 'One-time browser pairing of the drive folder (interactive)', (yy) => yy.option('server', { type: 'string', describe: `aindrive server (default ${drive.DEFAULT_AINDRIVE_SERVER})` })
|
|
696
|
+
.option('name', { type: 'string', describe: 'drive name' }).option('open', { type: 'boolean', default: true, describe: 'open the browser for the pairing login (--no-open prints the link only)' }), run(async (ctx, a) => { const code = await drive.driveLogin(ctx, { server: a.server, name: a.name, noOpen: !a.open }); process.exit(code); }, true))
|
|
697
|
+
.demandCommand(1, 'Subcommand is required (status|up|stop|sync|login).'), () => undefined);
|
|
698
|
+
// ---------------------------------------------------------------- chain
|
|
699
|
+
cli.command('chain', 'Local AIN blockchain (docker) for the ain ledger', (y) => fail(y)
|
|
700
|
+
.command('up', 'Start (or attach to) a local 1-node AIN chain on :8081', (yy) => yy.option('wait', { type: 'number', default: 90, describe: 'seconds to wait for SERVING' }), run((ctx, a) => chain.chainUp(ctx, a), false, true))
|
|
701
|
+
.command('down', 'Remove the local chain container', (yy) => yy, run((ctx) => chain.chainDown(ctx), false, true))
|
|
702
|
+
.command('status', 'Chain health and last block', (yy) => yy.option('provider', { type: 'string', describe: 'AIN JSON-RPC URL to ask (default: the one in config.json)' }), run((ctx, a) => chain.chainStatus(ctx, a.provider), false, true))
|
|
703
|
+
.command('fund <address> [amount]', 'Transfer AIN from the local genesis account (local chain only)', (yy) => yy
|
|
704
|
+
.positional('address', { type: 'string', demandOption: true, describe: `the AIN address to credit (\`${PROG} keys show\`)` })
|
|
705
|
+
.positional('amount', { type: 'number', default: 1000, describe: 'how much AIN' })
|
|
706
|
+
.option('provider', { type: 'string', describe: 'AIN JSON-RPC URL to send it through (default: the one in config.json)' }), run((ctx, a) => chain.chainFund(ctx, a.address, a.amount, a.provider), false, true))
|
|
707
|
+
.command('setup', 'Register the knowledge app + market rules on-chain (funds the node identity first on a local chain)', (yy) => yy.option('fund', { type: 'number', describe: 'AIN to fund the node identity with' }), run((ctx, a) => chain.chainSetup(ctx, a), false, true))
|
|
708
|
+
.demandCommand(1, 'Subcommand is required (up|down|status|fund|setup).'), () => undefined);
|
|
709
|
+
fail(cli);
|
|
710
|
+
await cli.parseAsync();
|
|
711
|
+
//# sourceMappingURL=bin.js.map
|