lazyclaw 6.0.0 → 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.
Files changed (47) hide show
  1. package/channels-discord/index.mjs +76 -0
  2. package/channels-discord/package.json +14 -0
  3. package/channels-email/index.mjs +109 -0
  4. package/channels-email/package.json +14 -0
  5. package/channels-signal/index.mjs +69 -0
  6. package/channels-signal/package.json +9 -0
  7. package/channels-voice/index.mjs +81 -0
  8. package/channels-voice/package.json +9 -0
  9. package/channels-whatsapp/index.mjs +70 -0
  10. package/channels-whatsapp/package.json +13 -0
  11. package/commands/agents.mjs +669 -0
  12. package/commands/auth_nodes.mjs +323 -0
  13. package/commands/automation.mjs +582 -0
  14. package/commands/channels.mjs +255 -0
  15. package/commands/chat.mjs +1217 -0
  16. package/commands/config.mjs +315 -0
  17. package/commands/daemon.mjs +260 -0
  18. package/commands/misc.mjs +128 -0
  19. package/commands/providers.mjs +511 -0
  20. package/commands/sessions.mjs +343 -0
  21. package/commands/setup.mjs +741 -0
  22. package/commands/skills.mjs +218 -0
  23. package/commands/workflow.mjs +661 -0
  24. package/daemon/lib/auth.mjs +58 -0
  25. package/daemon/lib/cost.mjs +30 -0
  26. package/daemon/lib/provider.mjs +69 -0
  27. package/daemon/lib/respond.mjs +83 -0
  28. package/daemon/route_table.mjs +96 -0
  29. package/daemon/routes/_deps.mjs +36 -0
  30. package/daemon/routes/config.mjs +99 -0
  31. package/daemon/routes/conversation.mjs +371 -0
  32. package/daemon/routes/meta.mjs +239 -0
  33. package/daemon/routes/ops.mjs +185 -0
  34. package/daemon/routes/providers.mjs +211 -0
  35. package/daemon/routes/rates.mjs +90 -0
  36. package/daemon/routes/registry.mjs +223 -0
  37. package/daemon/routes/sessions.mjs +213 -0
  38. package/daemon/routes/skills.mjs +260 -0
  39. package/daemon/routes/workflows.mjs +224 -0
  40. package/dotenv_min.mjs +23 -0
  41. package/first_run.mjs +15 -0
  42. package/goals_cron.mjs +37 -0
  43. package/lib/args.mjs +216 -0
  44. package/lib/config.mjs +113 -0
  45. package/lib/registry_boot.mjs +55 -0
  46. package/package.json +14 -1
  47. package/secure_write.mjs +46 -0
@@ -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
+ }