lazyclaw 5.4.4 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/channels/handoff.mjs +36 -0
- package/channels-discord/index.mjs +76 -0
- package/channels-discord/package.json +14 -0
- package/channels-email/index.mjs +109 -0
- package/channels-email/package.json +14 -0
- package/channels-signal/index.mjs +69 -0
- package/channels-signal/package.json +9 -0
- package/channels-voice/index.mjs +81 -0
- package/channels-voice/package.json +9 -0
- package/channels-whatsapp/index.mjs +70 -0
- package/channels-whatsapp/package.json +13 -0
- package/cli.mjs +73 -7399
- package/commands/agents.mjs +669 -0
- package/commands/auth_nodes.mjs +323 -0
- package/commands/automation.mjs +582 -0
- package/commands/channels.mjs +255 -0
- package/commands/chat.mjs +1217 -0
- package/commands/config.mjs +315 -0
- package/commands/daemon.mjs +260 -0
- package/commands/misc.mjs +128 -0
- package/commands/providers.mjs +511 -0
- package/commands/sessions.mjs +343 -0
- package/commands/setup.mjs +741 -0
- package/commands/skills.mjs +218 -0
- package/commands/workflow.mjs +661 -0
- package/daemon/lib/auth.mjs +58 -0
- package/daemon/lib/cost.mjs +30 -0
- package/daemon/lib/provider.mjs +69 -0
- package/daemon/lib/respond.mjs +83 -0
- package/daemon/route_table.mjs +96 -0
- package/daemon/routes/_deps.mjs +36 -0
- package/daemon/routes/config.mjs +99 -0
- package/daemon/routes/conversation.mjs +371 -0
- package/daemon/routes/meta.mjs +239 -0
- package/daemon/routes/ops.mjs +185 -0
- package/daemon/routes/providers.mjs +211 -0
- package/daemon/routes/rates.mjs +90 -0
- package/daemon/routes/registry.mjs +223 -0
- package/daemon/routes/sessions.mjs +213 -0
- package/daemon/routes/skills.mjs +260 -0
- package/daemon/routes/workflows.mjs +224 -0
- package/daemon.mjs +23 -2085
- package/dotenv_min.mjs +23 -0
- package/first_run.mjs +15 -0
- package/goals_cron.mjs +37 -0
- package/lib/args.mjs +216 -0
- package/lib/config.mjs +113 -0
- package/lib/registry_boot.mjs +55 -0
- package/mas/agent_turn.mjs +2 -1
- package/mas/index_db.mjs +82 -0
- package/mas/learning.mjs +17 -1
- package/mas/mention_router.mjs +38 -10
- package/mas/provider_adapters.mjs +28 -4
- package/mas/scrub_env.mjs +34 -0
- package/mas/tool_runner.mjs +23 -7
- package/mas/tools/bash.mjs +10 -5
- package/mas/tools/browser.mjs +18 -0
- package/mas/tools/learning.mjs +24 -14
- package/mas/tools/recall.mjs +5 -1
- package/mas/tools/web.mjs +47 -11
- package/mas/trajectory_store.mjs +7 -4
- package/package.json +16 -2
- package/providers/auth_store.mjs +22 -0
- package/providers/claude_cli.mjs +28 -2
- package/providers/claude_cli_detect.mjs +46 -0
- package/providers/custom_provider.mjs +70 -0
- package/providers/model_catalogue.mjs +86 -0
- package/providers/orchestrator.mjs +30 -9
- package/providers/rates.mjs +12 -2
- package/providers/registry.mjs +10 -7
- package/sandbox/confiners/landlock.mjs +14 -8
- package/sandbox/confiners/seatbelt.mjs +18 -2
- package/scripts/loop-worker.mjs +18 -7
- package/scripts/migrate-v5.mjs +5 -61
- package/secure_write.mjs +46 -0
- package/sessions.mjs +0 -0
- package/tui/modal_filter.mjs +59 -0
- package/tui/modal_picker.mjs +12 -37
- package/tui/pickers.mjs +917 -0
- package/tui/provider_families.mjs +41 -0
- package/tui/repl.mjs +67 -36
- package/tui/slash_commands.mjs +2 -1
- package/tui/slash_dispatcher.mjs +717 -58
- package/tui/splash.mjs +5 -12
- package/tui/subcommands.mjs +17 -0
- package/tui/terminal_approve.mjs +37 -0
- package/web/dashboard.css +275 -0
- package/web/dashboard.html +2 -1685
- package/web/dashboard.js +1406 -0
- package/workflow/persistent.mjs +13 -6
- package/mas/tools/skill_view.mjs +0 -43
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Misc commands: cmdBrowse (headless URL fetch) and the sandbox backend
|
|
2
|
+
// subcommands (list/test/add/use), extracted from cli.mjs (Phase D3).
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import { configPath } from '../lib/config.mjs';
|
|
6
|
+
import { resolveSandbox, listBackends } from '../sandbox/index.mjs';
|
|
7
|
+
|
|
8
|
+
export async function cmdBrowse(url, flags = {}) {
|
|
9
|
+
if (!url) { console.error('Usage: lazyclaw browse <url> [--max-bytes <N>] [--timeout-ms <N>] [--meta]'); process.exit(2); }
|
|
10
|
+
const { browse } = await import('../browse.mjs');
|
|
11
|
+
const opts = {};
|
|
12
|
+
if (flags['max-bytes'] !== undefined) opts.maxBytes = parseInt(flags['max-bytes'], 10);
|
|
13
|
+
if (flags['timeout-ms'] !== undefined) opts.timeoutMs = parseInt(flags['timeout-ms'], 10);
|
|
14
|
+
if (flags['user-agent']) opts.userAgent = flags['user-agent'];
|
|
15
|
+
try {
|
|
16
|
+
const r = await browse(url, opts);
|
|
17
|
+
if (flags.meta) {
|
|
18
|
+
process.stderr.write(JSON.stringify({
|
|
19
|
+
url: r.url, title: r.title, bytes: r.bytes, truncated: r.truncated,
|
|
20
|
+
}) + '\n');
|
|
21
|
+
}
|
|
22
|
+
process.stdout.write(r.markdown);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
console.error(`error: ${e?.message || e}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function cmdSandbox(args, flags = {}) {
|
|
29
|
+
const sub = args[0];
|
|
30
|
+
|
|
31
|
+
if (!sub || sub === 'list') {
|
|
32
|
+
for (const kind of listBackends()) process.stdout.write(`${kind}\n`);
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (sub === 'test') {
|
|
37
|
+
const name = args[1];
|
|
38
|
+
if (!name) { process.stderr.write('usage: lazyclaw sandbox test <kind|profile>\n'); return 2; }
|
|
39
|
+
const cfg = _sandboxLoadConfigOrEmpty();
|
|
40
|
+
// If `name` looks like a known kind, route to that kind. If it
|
|
41
|
+
// is not a known kind AND not a profile in cfg, treat as an
|
|
42
|
+
// unknown identifier and report SANDBOX_BAD_KIND.
|
|
43
|
+
const isKind = listBackends().includes(name);
|
|
44
|
+
const profile = cfg.sandbox && cfg.sandbox.profiles && cfg.sandbox.profiles[name];
|
|
45
|
+
if (!isKind && !profile) {
|
|
46
|
+
process.stderr.write(`SANDBOX_BAD_KIND: unknown sandbox kind or profile "${name}"\n`);
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
let sb;
|
|
50
|
+
try {
|
|
51
|
+
const synthCfg = isKind
|
|
52
|
+
? { sandbox: { default: name, ...cfg.sandbox } }
|
|
53
|
+
: cfg;
|
|
54
|
+
sb = resolveSandbox(synthCfg);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
process.stderr.write(`${e.code || 'SANDBOX_ERR'}: ${e.message}\n`); return 1;
|
|
57
|
+
}
|
|
58
|
+
if (sb.spec.kind !== 'local' && sb.spec.kind !== 'docker') {
|
|
59
|
+
// Remote/serverless backends just construct argv in unit tests;
|
|
60
|
+
// we report "shape-ok" without actually executing.
|
|
61
|
+
process.stdout.write(`ok ${sb.spec.kind} (argv-shape)\n`);
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
const sess = await sb.open();
|
|
65
|
+
try {
|
|
66
|
+
const r = await sess.exec(['echo', 'lazyclaw-sandbox-test']);
|
|
67
|
+
if (r.code !== 0 || !/lazyclaw-sandbox-test/.test(r.stdout)) {
|
|
68
|
+
process.stderr.write(`fail ${name}: exit=${r.code} stdout=${r.stdout}\n`); return 1;
|
|
69
|
+
}
|
|
70
|
+
process.stdout.write(`ok ${name}\n`);
|
|
71
|
+
return 0;
|
|
72
|
+
} finally { await sess.close(); }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (sub === 'add') {
|
|
76
|
+
const name = args[1];
|
|
77
|
+
if (!name) { process.stderr.write('usage: lazyclaw sandbox add <name> --kind <kind> [...]\n'); return 2; }
|
|
78
|
+
const opts = {};
|
|
79
|
+
if (flags.kind) opts.kind = flags.kind;
|
|
80
|
+
if (flags.image) opts.image = flags.image;
|
|
81
|
+
if (flags.host) opts.host = flags.host;
|
|
82
|
+
if (flags.user) opts.user = flags.user;
|
|
83
|
+
if (flags.workspace) opts.workspace = flags.workspace;
|
|
84
|
+
if (flags.app) opts.app = flags.app;
|
|
85
|
+
if (flags.confiner) opts.confiner = flags.confiner;
|
|
86
|
+
if (!listBackends().includes(opts.kind)) {
|
|
87
|
+
process.stderr.write(`unknown kind "${opts.kind}"\n`); return 1;
|
|
88
|
+
}
|
|
89
|
+
const cfg = _sandboxLoadConfigOrEmpty();
|
|
90
|
+
cfg.sandbox = cfg.sandbox || {};
|
|
91
|
+
cfg.sandbox.profiles = cfg.sandbox.profiles || {};
|
|
92
|
+
cfg.sandbox.profiles[name] = opts;
|
|
93
|
+
_sandboxSaveConfig(cfg);
|
|
94
|
+
process.stdout.write(`added profile ${name} (${opts.kind})\n`);
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (sub === 'use') {
|
|
99
|
+
const name = args[1];
|
|
100
|
+
if (!name) { process.stderr.write('usage: lazyclaw sandbox use <profile>\n'); return 2; }
|
|
101
|
+
const cfg = _sandboxLoadConfigOrEmpty();
|
|
102
|
+
const prof = cfg.sandbox && cfg.sandbox.profiles && cfg.sandbox.profiles[name];
|
|
103
|
+
if (!prof) { process.stderr.write(`no profile "${name}"\n`); return 1; }
|
|
104
|
+
cfg.sandbox = cfg.sandbox || {};
|
|
105
|
+
cfg.sandbox.default = prof.kind;
|
|
106
|
+
cfg.sandbox[prof.kind] = { ...(cfg.sandbox[prof.kind] || {}), ...prof, kind: undefined };
|
|
107
|
+
delete cfg.sandbox[prof.kind].kind;
|
|
108
|
+
_sandboxSaveConfig(cfg);
|
|
109
|
+
process.stdout.write(`using profile ${name} (${prof.kind})\n`);
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
process.stderr.write(`unknown subcommand "${sub}". Try: list | test | add | use\n`);
|
|
114
|
+
return 2;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function _sandboxLoadConfigOrEmpty() {
|
|
118
|
+
const p = process.env.LAZYCLAW_CONFIG || configPath();
|
|
119
|
+
try {
|
|
120
|
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
121
|
+
} catch { return {}; }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function _sandboxSaveConfig(cfg) {
|
|
125
|
+
const p = process.env.LAZYCLAW_CONFIG || configPath();
|
|
126
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
127
|
+
fs.writeFileSync(p, JSON.stringify(cfg, null, 2));
|
|
128
|
+
}
|
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
// Provider, rate-card, and orchestrator-config commands, extracted from
|
|
2
|
+
// cli.mjs (Phase D3, picker-dependent batch — uses _fetchModelsForProvider).
|
|
3
|
+
import { readConfig, writeConfig } from '../lib/config.mjs';
|
|
4
|
+
import { ensureRegistry, getRegistry } from '../lib/registry_boot.mjs';
|
|
5
|
+
import { _fetchModelsForProvider } from '../tui/pickers.mjs';
|
|
6
|
+
|
|
7
|
+
export async function cmdRates(sub, positional, flags = {}) {
|
|
8
|
+
// Manage cfg.rates without hand-editing JSON. Same shape as
|
|
9
|
+
// RATE_CARD_SHAPE in providers/rates.mjs:
|
|
10
|
+
// { 'provider/model': { inputPer1M, outputPer1M, cacheReadPer1M?, cacheCreatePer1M?, currency? } }
|
|
11
|
+
switch (sub) {
|
|
12
|
+
case undefined:
|
|
13
|
+
case 'list': {
|
|
14
|
+
const cfg = readConfig();
|
|
15
|
+
const rates = cfg.rates && typeof cfg.rates === 'object' ? cfg.rates : {};
|
|
16
|
+
// Same --filter / --limit pattern as v3.33-v3.36 across
|
|
17
|
+
// sessions/skills/workflows. Filter on key (provider/model)
|
|
18
|
+
// case-insensitive, then post-filter cap.
|
|
19
|
+
let entries = Object.entries(rates);
|
|
20
|
+
if (flags.filter) {
|
|
21
|
+
const f = String(flags.filter).toLowerCase();
|
|
22
|
+
entries = entries.filter(([key]) => key.toLowerCase().includes(f));
|
|
23
|
+
}
|
|
24
|
+
if (flags.limit !== undefined) {
|
|
25
|
+
const n = parseInt(flags.limit, 10);
|
|
26
|
+
if (Number.isFinite(n) && n > 0) entries = entries.slice(0, n);
|
|
27
|
+
}
|
|
28
|
+
console.log(JSON.stringify(Object.fromEntries(entries), null, 2));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
case 'set': {
|
|
32
|
+
const key = positional[0];
|
|
33
|
+
if (!key || !key.includes('/')) {
|
|
34
|
+
console.error('Usage: lazyclaw rates set <provider/model> --input <N> --output <N> [--cache-read <N>] [--cache-create <N>] [--currency USD]');
|
|
35
|
+
process.exit(2);
|
|
36
|
+
}
|
|
37
|
+
const inputPer1M = flags.input !== undefined ? Number(flags.input) : null;
|
|
38
|
+
const outputPer1M = flags.output !== undefined ? Number(flags.output) : null;
|
|
39
|
+
if (!Number.isFinite(inputPer1M) || !Number.isFinite(outputPer1M) || inputPer1M < 0 || outputPer1M < 0) {
|
|
40
|
+
console.error('rates set: --input and --output must be non-negative numbers (per million tokens)');
|
|
41
|
+
process.exit(2);
|
|
42
|
+
}
|
|
43
|
+
const card = { inputPer1M, outputPer1M };
|
|
44
|
+
if (flags['cache-read'] !== undefined) card.cacheReadPer1M = Number(flags['cache-read']);
|
|
45
|
+
if (flags['cache-create'] !== undefined) card.cacheCreatePer1M = Number(flags['cache-create']);
|
|
46
|
+
if (flags.currency) card.currency = String(flags.currency);
|
|
47
|
+
else card.currency = 'USD';
|
|
48
|
+
const cfg = readConfig();
|
|
49
|
+
cfg.rates = cfg.rates || {};
|
|
50
|
+
cfg.rates[key] = card;
|
|
51
|
+
writeConfig(cfg);
|
|
52
|
+
console.log(JSON.stringify({ ok: true, key, card }));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
case 'delete':
|
|
56
|
+
case 'unset': {
|
|
57
|
+
const key = positional[0];
|
|
58
|
+
if (!key) { console.error('Usage: lazyclaw rates delete <provider/model>'); process.exit(2); }
|
|
59
|
+
const cfg = readConfig();
|
|
60
|
+
const had = !!(cfg.rates && cfg.rates[key]);
|
|
61
|
+
if (cfg.rates) delete cfg.rates[key];
|
|
62
|
+
writeConfig(cfg);
|
|
63
|
+
console.log(JSON.stringify({ ok: true, key, removed: had }));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
case 'shape': {
|
|
67
|
+
// Print the reference shape so users can copy-paste into config.
|
|
68
|
+
const mod = await import('../providers/rates.mjs');
|
|
69
|
+
console.log(JSON.stringify(mod.RATE_CARD_SHAPE, null, 2));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
case 'copy': {
|
|
73
|
+
// Clone a rate card from <src/model> to <dst/model>. Useful when
|
|
74
|
+
// a new model launches at the same price as a known one and you
|
|
75
|
+
// don't want to retype every field.
|
|
76
|
+
//
|
|
77
|
+
// Refuses to overwrite an existing destination unless --force is
|
|
78
|
+
// passed (a rate card is operator-curated; silent overwrite is
|
|
79
|
+
// exactly the wrong default).
|
|
80
|
+
const src = positional[0];
|
|
81
|
+
const dst = positional[1];
|
|
82
|
+
if (!src || !dst || !src.includes('/') || !dst.includes('/')) {
|
|
83
|
+
console.error('Usage: lazyclaw rates copy <src-provider/model> <dst-provider/model> [--force]');
|
|
84
|
+
process.exit(2);
|
|
85
|
+
}
|
|
86
|
+
const cfg = readConfig();
|
|
87
|
+
const rates = cfg.rates && typeof cfg.rates === 'object' ? cfg.rates : {};
|
|
88
|
+
if (!rates[src]) {
|
|
89
|
+
console.error(`rates copy: source key "${src}" not found in cfg.rates`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
if (rates[dst] && !flags.force) {
|
|
93
|
+
console.error(`rates copy: destination "${dst}" already exists (pass --force to overwrite)`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
// Deep clone (small object) so a later edit to one doesn't
|
|
97
|
+
// mutate the other.
|
|
98
|
+
cfg.rates = rates;
|
|
99
|
+
cfg.rates[dst] = JSON.parse(JSON.stringify(rates[src]));
|
|
100
|
+
writeConfig(cfg);
|
|
101
|
+
console.log(JSON.stringify({ ok: true, src, dst, card: cfg.rates[dst] }));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
case 'validate': {
|
|
105
|
+
// Shape check shared with daemon's GET /rates/validate via
|
|
106
|
+
// rates-validate.mjs. Single source of truth.
|
|
107
|
+
const cfg = readConfig();
|
|
108
|
+
await ensureRegistry();
|
|
109
|
+
const { validateRates } = await import('../rates-validate.mjs');
|
|
110
|
+
const result = validateRates(cfg.rates, getRegistry().PROVIDERS);
|
|
111
|
+
console.log(JSON.stringify(result, null, 2));
|
|
112
|
+
process.exit(result.ok ? 0 : 1);
|
|
113
|
+
}
|
|
114
|
+
default:
|
|
115
|
+
console.error('Usage: lazyclaw rates <list|set <key>|delete <key>|shape|validate>');
|
|
116
|
+
process.exit(2);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Loads on first use to avoid paying the import cost when the user
|
|
121
|
+
// only ran `lazyclaw chat` or similar; cli.mjs is already a 2700-line
|
|
122
|
+
// hot path and we don't need every helper paged in.
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
// `lazyclaw memory <show|dream|edit> [args]`
|
|
127
|
+
//
|
|
128
|
+
// show core|recent|episodic [topic] print contents to stdout
|
|
129
|
+
// dream consolidate recent into episodic
|
|
130
|
+
// edit core open $EDITOR on core.md
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
export async function cmdProviders(sub, positional, flags = {}) {
|
|
134
|
+
await ensureRegistry();
|
|
135
|
+
switch (sub) {
|
|
136
|
+
case undefined:
|
|
137
|
+
case 'list': {
|
|
138
|
+
// Defensive: if metadata is missing for a registered provider, fall back
|
|
139
|
+
// to a minimal shape so this never crashes the CLI even mid-refactor.
|
|
140
|
+
// --filter / --limit pattern matches v3.33-v3.46 across the other
|
|
141
|
+
// list surfaces. Filter on provider name, case-insensitive.
|
|
142
|
+
let out = Object.keys(getRegistry().PROVIDERS).map(name => {
|
|
143
|
+
const meta = getRegistry().PROVIDER_INFO[name] || { name, requiresApiKey: false, docs: '' };
|
|
144
|
+
return {
|
|
145
|
+
name,
|
|
146
|
+
requiresApiKey: !!meta.requiresApiKey,
|
|
147
|
+
defaultModel: meta.defaultModel || null,
|
|
148
|
+
suggestedModels: meta.suggestedModels || [],
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
if (flags.filter) {
|
|
152
|
+
const f = String(flags.filter).toLowerCase();
|
|
153
|
+
out = out.filter(p => p.name.toLowerCase().includes(f));
|
|
154
|
+
}
|
|
155
|
+
if (flags.limit !== undefined) {
|
|
156
|
+
const n = parseInt(flags.limit, 10);
|
|
157
|
+
if (Number.isFinite(n) && n > 0) out = out.slice(0, n);
|
|
158
|
+
}
|
|
159
|
+
console.log(JSON.stringify(out, null, 2));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
case 'info': {
|
|
163
|
+
const name = positional[0];
|
|
164
|
+
if (!name) { console.error('Usage: lazyclaw providers info <name>'); process.exit(2); }
|
|
165
|
+
const meta = getRegistry().PROVIDER_INFO[name];
|
|
166
|
+
if (!meta) {
|
|
167
|
+
console.error(`unknown provider: ${name} (registered: ${Object.keys(getRegistry().PROVIDERS).join(', ')})`);
|
|
168
|
+
process.exit(2);
|
|
169
|
+
}
|
|
170
|
+
console.log(JSON.stringify(meta, null, 2));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
case 'test': {
|
|
174
|
+
// Smoke-test a provider with a tiny ("ping") prompt. Useful after
|
|
175
|
+
// configuring a new API key — surfaces auth errors fast without
|
|
176
|
+
// waiting for the next real call to fail.
|
|
177
|
+
//
|
|
178
|
+
// Output:
|
|
179
|
+
// { ok: bool, provider, model, durationMs, [reply | error, code] }
|
|
180
|
+
//
|
|
181
|
+
// Exit codes:
|
|
182
|
+
// 0 — provider returned a non-empty reply
|
|
183
|
+
// 1 — provider returned an error (auth failure, rate limit, ...)
|
|
184
|
+
// 2 — invalid invocation (unknown name)
|
|
185
|
+
//
|
|
186
|
+
// No name OR --all: smoke-test every registered provider in
|
|
187
|
+
// parallel. Output is `{ ok, results: [...] }` where ok is true
|
|
188
|
+
// iff every entry passed. Exit 0 when all pass, 1 otherwise.
|
|
189
|
+
const name = positional[0];
|
|
190
|
+
const cfg = readConfig();
|
|
191
|
+
const promptIdx = positional.indexOf('--prompt');
|
|
192
|
+
const sharedPrompt = flags.prompt || (promptIdx >= 0 ? positional[promptIdx + 1] : null) || 'ping';
|
|
193
|
+
if (!name || flags.all) {
|
|
194
|
+
const apiKey = cfg['api-key'] || '';
|
|
195
|
+
const t0all = Date.now();
|
|
196
|
+
const results = await Promise.all(
|
|
197
|
+
Object.entries(getRegistry().PROVIDERS).map(async ([pid, provider]) => {
|
|
198
|
+
const meta = getRegistry().PROVIDER_INFO[pid] || {};
|
|
199
|
+
const model = flags.model || cfg.model || meta.defaultModel || 'unknown';
|
|
200
|
+
const t0 = Date.now();
|
|
201
|
+
try {
|
|
202
|
+
let reply = '';
|
|
203
|
+
const stream = provider.sendMessage([{ role: 'user', content: sharedPrompt }], { apiKey, model });
|
|
204
|
+
for await (const chunk of stream) {
|
|
205
|
+
if (typeof chunk === 'string') reply += chunk;
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
name: pid, ok: reply.length > 0, model,
|
|
209
|
+
durationMs: Date.now() - t0,
|
|
210
|
+
replyLength: reply.length,
|
|
211
|
+
};
|
|
212
|
+
} catch (err) {
|
|
213
|
+
return {
|
|
214
|
+
name: pid, ok: false, model,
|
|
215
|
+
durationMs: Date.now() - t0,
|
|
216
|
+
error: err?.message || String(err),
|
|
217
|
+
code: err?.code || null,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
const allOk = results.every(r => r.ok);
|
|
223
|
+
console.log(JSON.stringify({
|
|
224
|
+
ok: allOk,
|
|
225
|
+
totalDurationMs: Date.now() - t0all,
|
|
226
|
+
results,
|
|
227
|
+
}, null, 2));
|
|
228
|
+
process.exit(allOk ? 0 : 1);
|
|
229
|
+
}
|
|
230
|
+
const provider = getRegistry().PROVIDERS[name];
|
|
231
|
+
if (!provider) {
|
|
232
|
+
console.error(`unknown provider: ${name} (registered: ${Object.keys(getRegistry().PROVIDERS).join(', ')})`);
|
|
233
|
+
process.exit(2);
|
|
234
|
+
}
|
|
235
|
+
// cfg already declared above for the all-mode branch; reuse it.
|
|
236
|
+
const meta = getRegistry().PROVIDER_INFO[name] || {};
|
|
237
|
+
// --model / --prompt come in via the parsed flags map (parseArgs
|
|
238
|
+
// lifted them out of positional). --model wins over config.model
|
|
239
|
+
// wins over PROVIDER_INFO.defaultModel.
|
|
240
|
+
const model = flags.model || cfg.model || meta.defaultModel || 'unknown';
|
|
241
|
+
const prompt = flags.prompt || 'ping';
|
|
242
|
+
const apiKey = cfg['api-key'] || '';
|
|
243
|
+
const t0 = Date.now();
|
|
244
|
+
try {
|
|
245
|
+
// Drain the streaming response (every provider yields chunks of
|
|
246
|
+
// string). For mock this is instant; for real providers it's
|
|
247
|
+
// bounded by the prompt length and provider latency. We don't
|
|
248
|
+
// support a timeout flag here — the user can SIGINT if a
|
|
249
|
+
// provider hangs.
|
|
250
|
+
let reply = '';
|
|
251
|
+
const stream = provider.sendMessage([{ role: 'user', content: prompt }], { apiKey, model });
|
|
252
|
+
for await (const chunk of stream) {
|
|
253
|
+
if (typeof chunk === 'string') reply += chunk;
|
|
254
|
+
}
|
|
255
|
+
const durationMs = Date.now() - t0;
|
|
256
|
+
const ok = reply.length > 0;
|
|
257
|
+
console.log(JSON.stringify({
|
|
258
|
+
ok,
|
|
259
|
+
provider: name,
|
|
260
|
+
model,
|
|
261
|
+
durationMs,
|
|
262
|
+
replyLength: reply.length,
|
|
263
|
+
reply: reply.slice(0, 200) + (reply.length > 200 ? '…' : ''),
|
|
264
|
+
}, null, 2));
|
|
265
|
+
process.exit(ok ? 0 : 1);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
const durationMs = Date.now() - t0;
|
|
268
|
+
console.log(JSON.stringify({
|
|
269
|
+
ok: false,
|
|
270
|
+
provider: name,
|
|
271
|
+
model,
|
|
272
|
+
durationMs,
|
|
273
|
+
error: err?.message || String(err),
|
|
274
|
+
code: err?.code || null,
|
|
275
|
+
}, null, 2));
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
case 'add': {
|
|
280
|
+
// Register an OpenAI-compatible custom endpoint non-interactively.
|
|
281
|
+
// Mirrors the picker's "+ Add custom" flow but scriptable, so users
|
|
282
|
+
// can wire NIM / OpenRouter / vLLM into config without entering the
|
|
283
|
+
// arrow-key UI.
|
|
284
|
+
// lazyclaw providers add nim \
|
|
285
|
+
// --base-url https://integrate.api.nvidia.com/v1 \
|
|
286
|
+
// --api-key nvapi-xxx \
|
|
287
|
+
// [--default-model meta/llama-3.1-70b] \
|
|
288
|
+
// [--no-probe]
|
|
289
|
+
const name = positional[0];
|
|
290
|
+
const baseUrl = flags['base-url'] || flags.baseUrl;
|
|
291
|
+
const apiKey = flags['api-key'] || flags.apiKey || '';
|
|
292
|
+
if (!name || !baseUrl) {
|
|
293
|
+
console.error('Usage: lazyclaw providers add <name> --base-url <url> [--api-key <key>] [--default-model <id>] [--no-probe]');
|
|
294
|
+
process.exit(2);
|
|
295
|
+
}
|
|
296
|
+
let validName;
|
|
297
|
+
try { validName = getRegistry().validateCustomProviderName(name); }
|
|
298
|
+
catch (e) { console.error(e.message); process.exit(2); }
|
|
299
|
+
if (!/^https?:\/\//i.test(String(baseUrl))) {
|
|
300
|
+
console.error('--base-url must start with http:// or https://');
|
|
301
|
+
process.exit(2);
|
|
302
|
+
}
|
|
303
|
+
const cfg = readConfig();
|
|
304
|
+
cfg.customProviders = Array.isArray(cfg.customProviders) ? cfg.customProviders : [];
|
|
305
|
+
const idx = cfg.customProviders.findIndex((p) => p && p.name === validName);
|
|
306
|
+
const entry = {
|
|
307
|
+
name: validName,
|
|
308
|
+
baseUrl: String(baseUrl).replace(/\/+$/, ''),
|
|
309
|
+
apiKey: apiKey || undefined,
|
|
310
|
+
};
|
|
311
|
+
if (flags['default-model']) entry.defaultModel = flags['default-model'];
|
|
312
|
+
if (idx >= 0) cfg.customProviders[idx] = { ...cfg.customProviders[idx], ...entry };
|
|
313
|
+
else cfg.customProviders.push(entry);
|
|
314
|
+
writeConfig(cfg);
|
|
315
|
+
getRegistry().registerCustomProviders(cfg);
|
|
316
|
+
|
|
317
|
+
let probe = null;
|
|
318
|
+
if (!flags['no-probe']) {
|
|
319
|
+
try {
|
|
320
|
+
const list = await getRegistry().fetchOpenAICompatModels({
|
|
321
|
+
baseUrl: entry.baseUrl, apiKey: entry.apiKey || '',
|
|
322
|
+
});
|
|
323
|
+
probe = { ok: true, modelCount: list.length, sample: list.slice(0, 8) };
|
|
324
|
+
if (list.length) {
|
|
325
|
+
const updated = readConfig();
|
|
326
|
+
const i = (updated.customProviders || []).findIndex((p) => p && p.name === validName);
|
|
327
|
+
if (i >= 0) {
|
|
328
|
+
updated.customProviders[i].suggestedModels = list.slice(0, 50);
|
|
329
|
+
if (!updated.customProviders[i].defaultModel) updated.customProviders[i].defaultModel = list[0];
|
|
330
|
+
writeConfig(updated);
|
|
331
|
+
getRegistry().registerCustomProviders(updated);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
} catch (e) {
|
|
335
|
+
probe = { ok: false, error: e?.message || String(e) };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
console.log(JSON.stringify({
|
|
339
|
+
ok: true, added: validName, baseUrl: entry.baseUrl, hasApiKey: !!entry.apiKey, probe,
|
|
340
|
+
}, null, 2));
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
case 'remove': {
|
|
344
|
+
const name = positional[0];
|
|
345
|
+
if (!name) { console.error('Usage: lazyclaw providers remove <name>'); process.exit(2); }
|
|
346
|
+
const cfg = readConfig();
|
|
347
|
+
const list = Array.isArray(cfg.customProviders) ? cfg.customProviders : [];
|
|
348
|
+
const before = list.length;
|
|
349
|
+
cfg.customProviders = list.filter((p) => !(p && p.name === name));
|
|
350
|
+
if (cfg.customProviders.length === before) {
|
|
351
|
+
console.error(`no custom provider named "${name}" — registered: ${list.map((p) => p.name).join(', ') || '(none)'}`);
|
|
352
|
+
process.exit(2);
|
|
353
|
+
}
|
|
354
|
+
writeConfig(cfg);
|
|
355
|
+
// The in-memory PROVIDERS map keeps the dropped entry until process
|
|
356
|
+
// restart — fine for the CLI (each invocation re-registers from
|
|
357
|
+
// disk). We don't try to mutate it here.
|
|
358
|
+
console.log(JSON.stringify({ ok: true, removed: name }, null, 2));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
case 'models': {
|
|
362
|
+
// Fetch + print the live model list from a provider's /v1/models.
|
|
363
|
+
// Works for any registered OpenAI-compatible endpoint (custom +
|
|
364
|
+
// openai + ollama). Used by the picker but useful standalone too:
|
|
365
|
+
// lazyclaw providers models nim
|
|
366
|
+
// lazyclaw providers models openai --filter gpt-4
|
|
367
|
+
const name = positional[0];
|
|
368
|
+
if (!name) { console.error('Usage: lazyclaw providers models <name> [--filter <substr>]'); process.exit(2); }
|
|
369
|
+
if (!getRegistry().PROVIDERS[name]) {
|
|
370
|
+
console.error(`unknown provider: ${name}`);
|
|
371
|
+
process.exit(2);
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
const list = await _fetchModelsForProvider(name);
|
|
375
|
+
let out = list;
|
|
376
|
+
if (flags.filter) {
|
|
377
|
+
const f = String(flags.filter).toLowerCase();
|
|
378
|
+
out = out.filter((m) => m.toLowerCase().includes(f));
|
|
379
|
+
}
|
|
380
|
+
console.log(JSON.stringify({ ok: true, provider: name, count: out.length, models: out }, null, 2));
|
|
381
|
+
return;
|
|
382
|
+
} catch (e) {
|
|
383
|
+
console.log(JSON.stringify({ ok: false, provider: name, error: e?.message || String(e) }, null, 2));
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
default:
|
|
388
|
+
console.error('Usage: lazyclaw providers <list|info <name>|test <name>|add <name> --base-url <url> [--api-key <k>]|remove <name>|models <name>>');
|
|
389
|
+
process.exit(2);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// `lazyclaw orchestrator` — read/write the cfg.orchestrator section
|
|
394
|
+
// without editing config.json by hand. Mirrors the shape `lazyclaw
|
|
395
|
+
// providers` / `lazyclaw rates` already use.
|
|
396
|
+
//
|
|
397
|
+
// Subcommands:
|
|
398
|
+
// status Print current planner / workers / maxSubtasks as JSON.
|
|
399
|
+
// set-planner <provider[:model]> Replace the planner spec.
|
|
400
|
+
// workers add <provider[:model]> Append a worker (idempotent — duplicates skipped).
|
|
401
|
+
// workers remove <provider[:model]> Drop a worker by exact match. Idempotent.
|
|
402
|
+
// workers clear Empty the workers list.
|
|
403
|
+
// workers set <provider[:model],...> Replace the whole list (comma-separated).
|
|
404
|
+
// set-max-subtasks <N> Cap the number of subtasks (clamped 1..10).
|
|
405
|
+
// clear Delete the entire cfg.orchestrator block.
|
|
406
|
+
export async function cmdOrchestrator(sub, positional, _flags = {}) {
|
|
407
|
+
await ensureRegistry();
|
|
408
|
+
const cfg = readConfig();
|
|
409
|
+
const orch = cfg.orchestrator && typeof cfg.orchestrator === 'object' ? cfg.orchestrator : {};
|
|
410
|
+
const known = Object.keys(getRegistry().PROVIDERS);
|
|
411
|
+
const validateSpec = (spec) => {
|
|
412
|
+
if (!spec) throw new Error('provider spec required (e.g. "claude-cli" or "openai:gpt-4o")');
|
|
413
|
+
const colon = spec.indexOf(':');
|
|
414
|
+
const provName = colon > 0 ? spec.slice(0, colon) : spec;
|
|
415
|
+
if (provName === 'orchestrator') throw new Error('"orchestrator" cannot reference itself — pick a real provider');
|
|
416
|
+
if (!known.includes(provName)) {
|
|
417
|
+
throw new Error(`unknown provider "${provName}" — registered: ${known.join(', ')}`);
|
|
418
|
+
}
|
|
419
|
+
return spec;
|
|
420
|
+
};
|
|
421
|
+
const saveAndPrint = (next) => {
|
|
422
|
+
if (next === null) delete cfg.orchestrator;
|
|
423
|
+
else cfg.orchestrator = next;
|
|
424
|
+
writeConfig(cfg);
|
|
425
|
+
console.log(JSON.stringify(cfg.orchestrator || null, null, 2));
|
|
426
|
+
};
|
|
427
|
+
switch (sub) {
|
|
428
|
+
case undefined:
|
|
429
|
+
case 'status': {
|
|
430
|
+
console.log(JSON.stringify({
|
|
431
|
+
ok: true,
|
|
432
|
+
configured: !!cfg.orchestrator,
|
|
433
|
+
planner: orch.planner || null,
|
|
434
|
+
workers: Array.isArray(orch.workers) ? orch.workers : [],
|
|
435
|
+
maxSubtasks: Number.isFinite(orch.maxSubtasks) ? orch.maxSubtasks : null,
|
|
436
|
+
knownProviders: known,
|
|
437
|
+
}, null, 2));
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
case 'set-planner': {
|
|
441
|
+
try {
|
|
442
|
+
const spec = validateSpec(positional[0]);
|
|
443
|
+
saveAndPrint({ ...orch, planner: spec });
|
|
444
|
+
} catch (e) { console.error(`orchestrator: ${e.message}`); process.exit(2); }
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
case 'workers': {
|
|
448
|
+
const wsub = positional[0];
|
|
449
|
+
const workers = Array.isArray(orch.workers) ? orch.workers.slice() : [];
|
|
450
|
+
switch (wsub) {
|
|
451
|
+
case 'add': {
|
|
452
|
+
try {
|
|
453
|
+
const spec = validateSpec(positional[1]);
|
|
454
|
+
if (!workers.includes(spec)) workers.push(spec);
|
|
455
|
+
saveAndPrint({ ...orch, workers });
|
|
456
|
+
} catch (e) { console.error(`orchestrator: ${e.message}`); process.exit(2); }
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
case 'remove': {
|
|
460
|
+
const spec = positional[1];
|
|
461
|
+
if (!spec) { console.error('orchestrator: workers remove <provider[:model]>'); process.exit(2); }
|
|
462
|
+
const idx = workers.indexOf(spec);
|
|
463
|
+
if (idx >= 0) workers.splice(idx, 1);
|
|
464
|
+
saveAndPrint({ ...orch, workers });
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
case 'clear': {
|
|
468
|
+
saveAndPrint({ ...orch, workers: [] });
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
case 'set': {
|
|
472
|
+
const raw = positional[1] || '';
|
|
473
|
+
const specs = raw.split(',').map((s) => s.trim()).filter(Boolean);
|
|
474
|
+
try {
|
|
475
|
+
specs.forEach(validateSpec);
|
|
476
|
+
saveAndPrint({ ...orch, workers: specs });
|
|
477
|
+
} catch (e) { console.error(`orchestrator: ${e.message}`); process.exit(2); }
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
default: {
|
|
481
|
+
console.error('Usage: lazyclaw orchestrator workers <add <spec> | remove <spec> | clear | set <spec,spec,...>>');
|
|
482
|
+
process.exit(2);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
case 'set-max-subtasks': {
|
|
487
|
+
const n = parseInt(positional[0], 10);
|
|
488
|
+
if (!Number.isFinite(n) || n < 1) { console.error('orchestrator: set-max-subtasks <N> (1..10)'); process.exit(2); }
|
|
489
|
+
saveAndPrint({ ...orch, maxSubtasks: Math.min(10, Math.max(1, n)) });
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
case 'clear': {
|
|
493
|
+
saveAndPrint(null);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
default: {
|
|
497
|
+
console.error(
|
|
498
|
+
'Usage:\n' +
|
|
499
|
+
' lazyclaw orchestrator status\n' +
|
|
500
|
+
' lazyclaw orchestrator set-planner <provider[:model]>\n' +
|
|
501
|
+
' lazyclaw orchestrator workers add <provider[:model]>\n' +
|
|
502
|
+
' lazyclaw orchestrator workers remove <provider[:model]>\n' +
|
|
503
|
+
' lazyclaw orchestrator workers set <provider[:model],...>\n' +
|
|
504
|
+
' lazyclaw orchestrator workers clear\n' +
|
|
505
|
+
' lazyclaw orchestrator set-max-subtasks <N>\n' +
|
|
506
|
+
' lazyclaw orchestrator clear'
|
|
507
|
+
);
|
|
508
|
+
process.exit(2);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|