lazyclaw 6.0.0 → 6.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/README.ko.md +88 -25
- package/README.md +121 -190
- package/channels/handoff.mjs +18 -5
- package/channels/matrix.mjs +23 -5
- package/channels/slack.mjs +83 -50
- package/channels/slack_env.mjs +45 -0
- package/channels/telegram.mjs +49 -6
- 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 +10 -21
- package/commands/agents.mjs +669 -0
- package/commands/auth_nodes.mjs +323 -0
- package/commands/automation.mjs +582 -0
- package/commands/channels.mjs +254 -0
- package/commands/chat.mjs +1253 -0
- package/commands/config.mjs +315 -0
- package/commands/daemon.mjs +275 -0
- package/commands/gateway.mjs +194 -0
- package/commands/misc.mjs +128 -0
- package/commands/providers.mjs +490 -0
- package/commands/service.mjs +113 -0
- package/commands/sessions.mjs +343 -0
- package/commands/setup.mjs +742 -0
- package/commands/setup_channels.mjs +207 -0
- package/commands/skills.mjs +218 -0
- package/commands/workflow.mjs +661 -0
- package/config_features.mjs +106 -0
- package/daemon/lib/auth.mjs +58 -0
- package/daemon/lib/cost.mjs +30 -0
- package/daemon/lib/inbound_dedup.mjs +108 -0
- package/daemon/lib/learn_queue.mjs +46 -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 +42 -0
- package/daemon/routes/config.mjs +99 -0
- package/daemon/routes/conversation.mjs +435 -0
- package/daemon/routes/meta.mjs +239 -0
- package/daemon/routes/ops.mjs +165 -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/dotenv_min.mjs +51 -0
- package/first_run.mjs +24 -0
- package/goals_cron.mjs +37 -0
- package/lib/args.mjs +216 -0
- package/lib/config.mjs +113 -0
- package/lib/gateway_guard.mjs +100 -0
- package/lib/inbound_client.mjs +108 -0
- package/lib/registry_boot.mjs +55 -0
- package/lib/service_install.mjs +207 -0
- package/package.json +15 -3
- package/providers/probe.mjs +28 -0
- package/secure_write.mjs +46 -0
- package/tui/editor.mjs +18 -2
- package/tui/repl.mjs +25 -4
- package/tui/slash_commands.mjs +4 -0
- package/tui/slash_dispatcher.mjs +118 -0
- package/tui/splash_props.mjs +52 -0
- package/web/dashboard.css +6 -12
- package/web/dashboard.html +2 -34
- package/web/dashboard.js +3 -3
|
@@ -0,0 +1,490 @@
|
|
|
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
|
+
import { probeProvider } from '../providers/probe.mjs';
|
|
7
|
+
|
|
8
|
+
export async function cmdRates(sub, positional, flags = {}) {
|
|
9
|
+
// Manage cfg.rates without hand-editing JSON. Same shape as
|
|
10
|
+
// RATE_CARD_SHAPE in providers/rates.mjs:
|
|
11
|
+
// { 'provider/model': { inputPer1M, outputPer1M, cacheReadPer1M?, cacheCreatePer1M?, currency? } }
|
|
12
|
+
switch (sub) {
|
|
13
|
+
case undefined:
|
|
14
|
+
case 'list': {
|
|
15
|
+
const cfg = readConfig();
|
|
16
|
+
const rates = cfg.rates && typeof cfg.rates === 'object' ? cfg.rates : {};
|
|
17
|
+
// Same --filter / --limit pattern as v3.33-v3.36 across
|
|
18
|
+
// sessions/skills/workflows. Filter on key (provider/model)
|
|
19
|
+
// case-insensitive, then post-filter cap.
|
|
20
|
+
let entries = Object.entries(rates);
|
|
21
|
+
if (flags.filter) {
|
|
22
|
+
const f = String(flags.filter).toLowerCase();
|
|
23
|
+
entries = entries.filter(([key]) => key.toLowerCase().includes(f));
|
|
24
|
+
}
|
|
25
|
+
if (flags.limit !== undefined) {
|
|
26
|
+
const n = parseInt(flags.limit, 10);
|
|
27
|
+
if (Number.isFinite(n) && n > 0) entries = entries.slice(0, n);
|
|
28
|
+
}
|
|
29
|
+
console.log(JSON.stringify(Object.fromEntries(entries), null, 2));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
case 'set': {
|
|
33
|
+
const key = positional[0];
|
|
34
|
+
if (!key || !key.includes('/')) {
|
|
35
|
+
console.error('Usage: lazyclaw rates set <provider/model> --input <N> --output <N> [--cache-read <N>] [--cache-create <N>] [--currency USD]');
|
|
36
|
+
process.exit(2);
|
|
37
|
+
}
|
|
38
|
+
const inputPer1M = flags.input !== undefined ? Number(flags.input) : null;
|
|
39
|
+
const outputPer1M = flags.output !== undefined ? Number(flags.output) : null;
|
|
40
|
+
if (!Number.isFinite(inputPer1M) || !Number.isFinite(outputPer1M) || inputPer1M < 0 || outputPer1M < 0) {
|
|
41
|
+
console.error('rates set: --input and --output must be non-negative numbers (per million tokens)');
|
|
42
|
+
process.exit(2);
|
|
43
|
+
}
|
|
44
|
+
const card = { inputPer1M, outputPer1M };
|
|
45
|
+
if (flags['cache-read'] !== undefined) card.cacheReadPer1M = Number(flags['cache-read']);
|
|
46
|
+
if (flags['cache-create'] !== undefined) card.cacheCreatePer1M = Number(flags['cache-create']);
|
|
47
|
+
if (flags.currency) card.currency = String(flags.currency);
|
|
48
|
+
else card.currency = 'USD';
|
|
49
|
+
const cfg = readConfig();
|
|
50
|
+
cfg.rates = cfg.rates || {};
|
|
51
|
+
cfg.rates[key] = card;
|
|
52
|
+
writeConfig(cfg);
|
|
53
|
+
console.log(JSON.stringify({ ok: true, key, card }));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
case 'delete':
|
|
57
|
+
case 'unset': {
|
|
58
|
+
const key = positional[0];
|
|
59
|
+
if (!key) { console.error('Usage: lazyclaw rates delete <provider/model>'); process.exit(2); }
|
|
60
|
+
const cfg = readConfig();
|
|
61
|
+
const had = !!(cfg.rates && cfg.rates[key]);
|
|
62
|
+
if (cfg.rates) delete cfg.rates[key];
|
|
63
|
+
writeConfig(cfg);
|
|
64
|
+
console.log(JSON.stringify({ ok: true, key, removed: had }));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
case 'shape': {
|
|
68
|
+
// Print the reference shape so users can copy-paste into config.
|
|
69
|
+
const mod = await import('../providers/rates.mjs');
|
|
70
|
+
console.log(JSON.stringify(mod.RATE_CARD_SHAPE, null, 2));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
case 'copy': {
|
|
74
|
+
// Clone a rate card from <src/model> to <dst/model>. Useful when
|
|
75
|
+
// a new model launches at the same price as a known one and you
|
|
76
|
+
// don't want to retype every field.
|
|
77
|
+
//
|
|
78
|
+
// Refuses to overwrite an existing destination unless --force is
|
|
79
|
+
// passed (a rate card is operator-curated; silent overwrite is
|
|
80
|
+
// exactly the wrong default).
|
|
81
|
+
const src = positional[0];
|
|
82
|
+
const dst = positional[1];
|
|
83
|
+
if (!src || !dst || !src.includes('/') || !dst.includes('/')) {
|
|
84
|
+
console.error('Usage: lazyclaw rates copy <src-provider/model> <dst-provider/model> [--force]');
|
|
85
|
+
process.exit(2);
|
|
86
|
+
}
|
|
87
|
+
const cfg = readConfig();
|
|
88
|
+
const rates = cfg.rates && typeof cfg.rates === 'object' ? cfg.rates : {};
|
|
89
|
+
if (!rates[src]) {
|
|
90
|
+
console.error(`rates copy: source key "${src}" not found in cfg.rates`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
if (rates[dst] && !flags.force) {
|
|
94
|
+
console.error(`rates copy: destination "${dst}" already exists (pass --force to overwrite)`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
// Deep clone (small object) so a later edit to one doesn't
|
|
98
|
+
// mutate the other.
|
|
99
|
+
cfg.rates = rates;
|
|
100
|
+
cfg.rates[dst] = JSON.parse(JSON.stringify(rates[src]));
|
|
101
|
+
writeConfig(cfg);
|
|
102
|
+
console.log(JSON.stringify({ ok: true, src, dst, card: cfg.rates[dst] }));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
case 'validate': {
|
|
106
|
+
// Shape check shared with daemon's GET /rates/validate via
|
|
107
|
+
// rates-validate.mjs. Single source of truth.
|
|
108
|
+
const cfg = readConfig();
|
|
109
|
+
await ensureRegistry();
|
|
110
|
+
const { validateRates } = await import('../rates-validate.mjs');
|
|
111
|
+
const result = validateRates(cfg.rates, getRegistry().PROVIDERS);
|
|
112
|
+
console.log(JSON.stringify(result, null, 2));
|
|
113
|
+
process.exit(result.ok ? 0 : 1);
|
|
114
|
+
}
|
|
115
|
+
default:
|
|
116
|
+
console.error('Usage: lazyclaw rates <list|set <key>|delete <key>|shape|validate>');
|
|
117
|
+
process.exit(2);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Loads on first use to avoid paying the import cost when the user
|
|
122
|
+
// only ran `lazyclaw chat` or similar; cli.mjs is already a 2700-line
|
|
123
|
+
// hot path and we don't need every helper paged in.
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
// `lazyclaw memory <show|dream|edit> [args]`
|
|
128
|
+
//
|
|
129
|
+
// show core|recent|episodic [topic] print contents to stdout
|
|
130
|
+
// dream consolidate recent into episodic
|
|
131
|
+
// edit core open $EDITOR on core.md
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
export async function cmdProviders(sub, positional, flags = {}) {
|
|
135
|
+
await ensureRegistry();
|
|
136
|
+
switch (sub) {
|
|
137
|
+
case undefined:
|
|
138
|
+
case 'list': {
|
|
139
|
+
// Defensive: if metadata is missing for a registered provider, fall back
|
|
140
|
+
// to a minimal shape so this never crashes the CLI even mid-refactor.
|
|
141
|
+
// --filter / --limit pattern matches v3.33-v3.46 across the other
|
|
142
|
+
// list surfaces. Filter on provider name, case-insensitive.
|
|
143
|
+
let out = Object.keys(getRegistry().PROVIDERS).map(name => {
|
|
144
|
+
const meta = getRegistry().PROVIDER_INFO[name] || { name, requiresApiKey: false, docs: '' };
|
|
145
|
+
return {
|
|
146
|
+
name,
|
|
147
|
+
requiresApiKey: !!meta.requiresApiKey,
|
|
148
|
+
defaultModel: meta.defaultModel || null,
|
|
149
|
+
suggestedModels: meta.suggestedModels || [],
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
if (flags.filter) {
|
|
153
|
+
const f = String(flags.filter).toLowerCase();
|
|
154
|
+
out = out.filter(p => p.name.toLowerCase().includes(f));
|
|
155
|
+
}
|
|
156
|
+
if (flags.limit !== undefined) {
|
|
157
|
+
const n = parseInt(flags.limit, 10);
|
|
158
|
+
if (Number.isFinite(n) && n > 0) out = out.slice(0, n);
|
|
159
|
+
}
|
|
160
|
+
console.log(JSON.stringify(out, null, 2));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
case 'info': {
|
|
164
|
+
const name = positional[0];
|
|
165
|
+
if (!name) { console.error('Usage: lazyclaw providers info <name>'); process.exit(2); }
|
|
166
|
+
const meta = getRegistry().PROVIDER_INFO[name];
|
|
167
|
+
if (!meta) {
|
|
168
|
+
console.error(`unknown provider: ${name} (registered: ${Object.keys(getRegistry().PROVIDERS).join(', ')})`);
|
|
169
|
+
process.exit(2);
|
|
170
|
+
}
|
|
171
|
+
console.log(JSON.stringify(meta, null, 2));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
case 'test': {
|
|
175
|
+
// Smoke-test a provider with a tiny ("ping") prompt. Useful after
|
|
176
|
+
// configuring a new API key — surfaces auth errors fast without
|
|
177
|
+
// waiting for the next real call to fail.
|
|
178
|
+
//
|
|
179
|
+
// Output:
|
|
180
|
+
// { ok: bool, provider, model, durationMs, [reply | error, code] }
|
|
181
|
+
//
|
|
182
|
+
// Exit codes:
|
|
183
|
+
// 0 — provider returned a non-empty reply
|
|
184
|
+
// 1 — provider returned an error (auth failure, rate limit, ...)
|
|
185
|
+
// 2 — invalid invocation (unknown name)
|
|
186
|
+
//
|
|
187
|
+
// No name OR --all: smoke-test every registered provider in
|
|
188
|
+
// parallel. Output is `{ ok, results: [...] }` where ok is true
|
|
189
|
+
// iff every entry passed. Exit 0 when all pass, 1 otherwise.
|
|
190
|
+
const name = positional[0];
|
|
191
|
+
const cfg = readConfig();
|
|
192
|
+
const promptIdx = positional.indexOf('--prompt');
|
|
193
|
+
const sharedPrompt = flags.prompt || (promptIdx >= 0 ? positional[promptIdx + 1] : null) || 'ping';
|
|
194
|
+
if (!name || flags.all) {
|
|
195
|
+
const apiKey = cfg['api-key'] || '';
|
|
196
|
+
const t0all = Date.now();
|
|
197
|
+
const results = await Promise.all(
|
|
198
|
+
Object.entries(getRegistry().PROVIDERS).map(async ([pid, provider]) => {
|
|
199
|
+
const meta = getRegistry().PROVIDER_INFO[pid] || {};
|
|
200
|
+
const model = flags.model || cfg.model || meta.defaultModel || 'unknown';
|
|
201
|
+
const t0 = Date.now();
|
|
202
|
+
try {
|
|
203
|
+
let reply = '';
|
|
204
|
+
const stream = provider.sendMessage([{ role: 'user', content: sharedPrompt }], { apiKey, model });
|
|
205
|
+
for await (const chunk of stream) {
|
|
206
|
+
if (typeof chunk === 'string') reply += chunk;
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
name: pid, ok: reply.length > 0, model,
|
|
210
|
+
durationMs: Date.now() - t0,
|
|
211
|
+
replyLength: reply.length,
|
|
212
|
+
};
|
|
213
|
+
} catch (err) {
|
|
214
|
+
return {
|
|
215
|
+
name: pid, ok: false, model,
|
|
216
|
+
durationMs: Date.now() - t0,
|
|
217
|
+
error: err?.message || String(err),
|
|
218
|
+
code: err?.code || null,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
223
|
+
const allOk = results.every(r => r.ok);
|
|
224
|
+
console.log(JSON.stringify({
|
|
225
|
+
ok: allOk,
|
|
226
|
+
totalDurationMs: Date.now() - t0all,
|
|
227
|
+
results,
|
|
228
|
+
}, null, 2));
|
|
229
|
+
process.exit(allOk ? 0 : 1);
|
|
230
|
+
}
|
|
231
|
+
const provider = getRegistry().PROVIDERS[name];
|
|
232
|
+
if (!provider) {
|
|
233
|
+
console.error(`unknown provider: ${name} (registered: ${Object.keys(getRegistry().PROVIDERS).join(', ')})`);
|
|
234
|
+
process.exit(2);
|
|
235
|
+
}
|
|
236
|
+
// cfg already declared above for the all-mode branch; reuse it.
|
|
237
|
+
const meta = getRegistry().PROVIDER_INFO[name] || {};
|
|
238
|
+
// --model wins over config.model wins over PROVIDER_INFO.defaultModel.
|
|
239
|
+
const model = flags.model || cfg.model || meta.defaultModel || 'unknown';
|
|
240
|
+
const prompt = flags.prompt || 'ping';
|
|
241
|
+
const apiKey = cfg['api-key'] || '';
|
|
242
|
+
// Shared, no-exit probe (providers/probe.mjs). The CLI prints JSON and
|
|
243
|
+
// exits; the setup wizard renders one line and keeps going.
|
|
244
|
+
const result = await probeProvider({ name, model, prompt, apiKey });
|
|
245
|
+
console.log(JSON.stringify(result, null, 2));
|
|
246
|
+
process.exit(result.ok ? 0 : 1);
|
|
247
|
+
}
|
|
248
|
+
case 'add': {
|
|
249
|
+
// Register an OpenAI-compatible custom endpoint non-interactively.
|
|
250
|
+
// Mirrors the picker's "+ Add custom" flow but scriptable, so users
|
|
251
|
+
// can wire NIM / OpenRouter / vLLM into config without entering the
|
|
252
|
+
// arrow-key UI.
|
|
253
|
+
// lazyclaw providers add nim \
|
|
254
|
+
// --base-url https://integrate.api.nvidia.com/v1 \
|
|
255
|
+
// --api-key nvapi-xxx \
|
|
256
|
+
// [--default-model meta/llama-3.1-70b] \
|
|
257
|
+
// [--no-probe]
|
|
258
|
+
const name = positional[0];
|
|
259
|
+
const baseUrl = flags['base-url'] || flags.baseUrl;
|
|
260
|
+
const apiKey = flags['api-key'] || flags.apiKey || '';
|
|
261
|
+
if (!name || !baseUrl) {
|
|
262
|
+
console.error('Usage: lazyclaw providers add <name> --base-url <url> [--api-key <key>] [--default-model <id>] [--no-probe]');
|
|
263
|
+
process.exit(2);
|
|
264
|
+
}
|
|
265
|
+
let validName;
|
|
266
|
+
try { validName = getRegistry().validateCustomProviderName(name); }
|
|
267
|
+
catch (e) { console.error(e.message); process.exit(2); }
|
|
268
|
+
if (!/^https?:\/\//i.test(String(baseUrl))) {
|
|
269
|
+
console.error('--base-url must start with http:// or https://');
|
|
270
|
+
process.exit(2);
|
|
271
|
+
}
|
|
272
|
+
const cfg = readConfig();
|
|
273
|
+
cfg.customProviders = Array.isArray(cfg.customProviders) ? cfg.customProviders : [];
|
|
274
|
+
const idx = cfg.customProviders.findIndex((p) => p && p.name === validName);
|
|
275
|
+
const entry = {
|
|
276
|
+
name: validName,
|
|
277
|
+
baseUrl: String(baseUrl).replace(/\/+$/, ''),
|
|
278
|
+
apiKey: apiKey || undefined,
|
|
279
|
+
};
|
|
280
|
+
if (flags['default-model']) entry.defaultModel = flags['default-model'];
|
|
281
|
+
if (idx >= 0) cfg.customProviders[idx] = { ...cfg.customProviders[idx], ...entry };
|
|
282
|
+
else cfg.customProviders.push(entry);
|
|
283
|
+
writeConfig(cfg);
|
|
284
|
+
getRegistry().registerCustomProviders(cfg);
|
|
285
|
+
|
|
286
|
+
let probe = null;
|
|
287
|
+
if (!flags['no-probe']) {
|
|
288
|
+
try {
|
|
289
|
+
const list = await getRegistry().fetchOpenAICompatModels({
|
|
290
|
+
baseUrl: entry.baseUrl, apiKey: entry.apiKey || '',
|
|
291
|
+
});
|
|
292
|
+
probe = { ok: true, modelCount: list.length, sample: list.slice(0, 8) };
|
|
293
|
+
if (list.length) {
|
|
294
|
+
const updated = readConfig();
|
|
295
|
+
const i = (updated.customProviders || []).findIndex((p) => p && p.name === validName);
|
|
296
|
+
if (i >= 0) {
|
|
297
|
+
updated.customProviders[i].suggestedModels = list.slice(0, 50);
|
|
298
|
+
if (!updated.customProviders[i].defaultModel) updated.customProviders[i].defaultModel = list[0];
|
|
299
|
+
writeConfig(updated);
|
|
300
|
+
getRegistry().registerCustomProviders(updated);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} catch (e) {
|
|
304
|
+
probe = { ok: false, error: e?.message || String(e) };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
console.log(JSON.stringify({
|
|
308
|
+
ok: true, added: validName, baseUrl: entry.baseUrl, hasApiKey: !!entry.apiKey, probe,
|
|
309
|
+
}, null, 2));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
case 'remove': {
|
|
313
|
+
const name = positional[0];
|
|
314
|
+
if (!name) { console.error('Usage: lazyclaw providers remove <name>'); process.exit(2); }
|
|
315
|
+
const cfg = readConfig();
|
|
316
|
+
const list = Array.isArray(cfg.customProviders) ? cfg.customProviders : [];
|
|
317
|
+
const before = list.length;
|
|
318
|
+
cfg.customProviders = list.filter((p) => !(p && p.name === name));
|
|
319
|
+
if (cfg.customProviders.length === before) {
|
|
320
|
+
console.error(`no custom provider named "${name}" — registered: ${list.map((p) => p.name).join(', ') || '(none)'}`);
|
|
321
|
+
process.exit(2);
|
|
322
|
+
}
|
|
323
|
+
writeConfig(cfg);
|
|
324
|
+
// The in-memory PROVIDERS map keeps the dropped entry until process
|
|
325
|
+
// restart — fine for the CLI (each invocation re-registers from
|
|
326
|
+
// disk). We don't try to mutate it here.
|
|
327
|
+
console.log(JSON.stringify({ ok: true, removed: name }, null, 2));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
case 'models': {
|
|
331
|
+
// Fetch + print the live model list from a provider's /v1/models.
|
|
332
|
+
// Works for any registered OpenAI-compatible endpoint (custom +
|
|
333
|
+
// openai + ollama). Used by the picker but useful standalone too:
|
|
334
|
+
// lazyclaw providers models nim
|
|
335
|
+
// lazyclaw providers models openai --filter gpt-4
|
|
336
|
+
const name = positional[0];
|
|
337
|
+
if (!name) { console.error('Usage: lazyclaw providers models <name> [--filter <substr>]'); process.exit(2); }
|
|
338
|
+
if (!getRegistry().PROVIDERS[name]) {
|
|
339
|
+
console.error(`unknown provider: ${name}`);
|
|
340
|
+
process.exit(2);
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const list = await _fetchModelsForProvider(name);
|
|
344
|
+
let out = list;
|
|
345
|
+
if (flags.filter) {
|
|
346
|
+
const f = String(flags.filter).toLowerCase();
|
|
347
|
+
out = out.filter((m) => m.toLowerCase().includes(f));
|
|
348
|
+
}
|
|
349
|
+
console.log(JSON.stringify({ ok: true, provider: name, count: out.length, models: out }, null, 2));
|
|
350
|
+
return;
|
|
351
|
+
} catch (e) {
|
|
352
|
+
console.log(JSON.stringify({ ok: false, provider: name, error: e?.message || String(e) }, null, 2));
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
default:
|
|
357
|
+
console.error('Usage: lazyclaw providers <list|info <name>|test <name>|add <name> --base-url <url> [--api-key <k>]|remove <name>|models <name>>');
|
|
358
|
+
process.exit(2);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// `lazyclaw orchestrator` — read/write the cfg.orchestrator section
|
|
363
|
+
// without editing config.json by hand. Mirrors the shape `lazyclaw
|
|
364
|
+
// providers` / `lazyclaw rates` already use.
|
|
365
|
+
//
|
|
366
|
+
// Subcommands:
|
|
367
|
+
// status Print current planner / workers / maxSubtasks as JSON.
|
|
368
|
+
// set-planner <provider[:model]> Replace the planner spec.
|
|
369
|
+
// workers add <provider[:model]> Append a worker (idempotent — duplicates skipped).
|
|
370
|
+
// workers remove <provider[:model]> Drop a worker by exact match. Idempotent.
|
|
371
|
+
// workers clear Empty the workers list.
|
|
372
|
+
// workers set <provider[:model],...> Replace the whole list (comma-separated).
|
|
373
|
+
// set-max-subtasks <N> Cap the number of subtasks (clamped 1..10).
|
|
374
|
+
// clear Delete the entire cfg.orchestrator block.
|
|
375
|
+
export async function cmdOrchestrator(sub, positional, _flags = {}) {
|
|
376
|
+
await ensureRegistry();
|
|
377
|
+
const cfg = readConfig();
|
|
378
|
+
const orch = cfg.orchestrator && typeof cfg.orchestrator === 'object' ? cfg.orchestrator : {};
|
|
379
|
+
const known = Object.keys(getRegistry().PROVIDERS);
|
|
380
|
+
const validateSpec = (spec) => {
|
|
381
|
+
if (!spec) throw new Error('provider spec required (e.g. "claude-cli" or "openai:gpt-4o")');
|
|
382
|
+
const colon = spec.indexOf(':');
|
|
383
|
+
const provName = colon > 0 ? spec.slice(0, colon) : spec;
|
|
384
|
+
if (provName === 'orchestrator') throw new Error('"orchestrator" cannot reference itself — pick a real provider');
|
|
385
|
+
if (!known.includes(provName)) {
|
|
386
|
+
throw new Error(`unknown provider "${provName}" — registered: ${known.join(', ')}`);
|
|
387
|
+
}
|
|
388
|
+
return spec;
|
|
389
|
+
};
|
|
390
|
+
const saveAndPrint = (next) => {
|
|
391
|
+
if (next === null) delete cfg.orchestrator;
|
|
392
|
+
else cfg.orchestrator = next;
|
|
393
|
+
writeConfig(cfg);
|
|
394
|
+
console.log(JSON.stringify(cfg.orchestrator || null, null, 2));
|
|
395
|
+
};
|
|
396
|
+
switch (sub) {
|
|
397
|
+
case undefined:
|
|
398
|
+
case 'status': {
|
|
399
|
+
console.log(JSON.stringify({
|
|
400
|
+
ok: true,
|
|
401
|
+
configured: !!cfg.orchestrator,
|
|
402
|
+
planner: orch.planner || null,
|
|
403
|
+
workers: Array.isArray(orch.workers) ? orch.workers : [],
|
|
404
|
+
maxSubtasks: Number.isFinite(orch.maxSubtasks) ? orch.maxSubtasks : null,
|
|
405
|
+
knownProviders: known,
|
|
406
|
+
}, null, 2));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
case 'on':
|
|
410
|
+
case 'off': {
|
|
411
|
+
// Route cfg.provider to/from 'orchestrator' (shared with /orchestrator).
|
|
412
|
+
const cf = await import('../config_features.mjs');
|
|
413
|
+
cf.orchestratorEnable(cfg, sub === 'on');
|
|
414
|
+
writeConfig(cfg);
|
|
415
|
+
const w = Array.isArray(orch.workers) ? orch.workers.length : 0;
|
|
416
|
+
console.log(JSON.stringify({ ok: true, enabled: sub === 'on', provider: cfg.provider, ...(sub === 'on' && w === 0 ? { warning: 'no workers configured — add one: lazyclaw orchestrator workers add <provider[:model]>' } : {}) }, null, 2));
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
case 'set-planner': {
|
|
420
|
+
try {
|
|
421
|
+
const spec = validateSpec(positional[0]);
|
|
422
|
+
saveAndPrint({ ...orch, planner: spec });
|
|
423
|
+
} catch (e) { console.error(`orchestrator: ${e.message}`); process.exit(2); }
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
case 'workers': {
|
|
427
|
+
const wsub = positional[0];
|
|
428
|
+
const workers = Array.isArray(orch.workers) ? orch.workers.slice() : [];
|
|
429
|
+
switch (wsub) {
|
|
430
|
+
case 'add': {
|
|
431
|
+
try {
|
|
432
|
+
const spec = validateSpec(positional[1]);
|
|
433
|
+
if (!workers.includes(spec)) workers.push(spec);
|
|
434
|
+
saveAndPrint({ ...orch, workers });
|
|
435
|
+
} catch (e) { console.error(`orchestrator: ${e.message}`); process.exit(2); }
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
case 'remove': {
|
|
439
|
+
const spec = positional[1];
|
|
440
|
+
if (!spec) { console.error('orchestrator: workers remove <provider[:model]>'); process.exit(2); }
|
|
441
|
+
const idx = workers.indexOf(spec);
|
|
442
|
+
if (idx >= 0) workers.splice(idx, 1);
|
|
443
|
+
saveAndPrint({ ...orch, workers });
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
case 'clear': {
|
|
447
|
+
saveAndPrint({ ...orch, workers: [] });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
case 'set': {
|
|
451
|
+
const raw = positional[1] || '';
|
|
452
|
+
const specs = raw.split(',').map((s) => s.trim()).filter(Boolean);
|
|
453
|
+
try {
|
|
454
|
+
specs.forEach(validateSpec);
|
|
455
|
+
saveAndPrint({ ...orch, workers: specs });
|
|
456
|
+
} catch (e) { console.error(`orchestrator: ${e.message}`); process.exit(2); }
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
default: {
|
|
460
|
+
console.error('Usage: lazyclaw orchestrator workers <add <spec> | remove <spec> | clear | set <spec,spec,...>>');
|
|
461
|
+
process.exit(2);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
case 'set-max-subtasks': {
|
|
466
|
+
const n = parseInt(positional[0], 10);
|
|
467
|
+
if (!Number.isFinite(n) || n < 1) { console.error('orchestrator: set-max-subtasks <N> (1..10)'); process.exit(2); }
|
|
468
|
+
saveAndPrint({ ...orch, maxSubtasks: Math.min(10, Math.max(1, n)) });
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
case 'clear': {
|
|
472
|
+
saveAndPrint(null);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
default: {
|
|
476
|
+
console.error(
|
|
477
|
+
'Usage:\n' +
|
|
478
|
+
' lazyclaw orchestrator status\n' +
|
|
479
|
+
' lazyclaw orchestrator set-planner <provider[:model]>\n' +
|
|
480
|
+
' lazyclaw orchestrator workers add <provider[:model]>\n' +
|
|
481
|
+
' lazyclaw orchestrator workers remove <provider[:model]>\n' +
|
|
482
|
+
' lazyclaw orchestrator workers set <provider[:model],...>\n' +
|
|
483
|
+
' lazyclaw orchestrator workers clear\n' +
|
|
484
|
+
' lazyclaw orchestrator set-max-subtasks <N>\n' +
|
|
485
|
+
' lazyclaw orchestrator clear'
|
|
486
|
+
);
|
|
487
|
+
process.exit(2);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// commands/service.mjs — `lazyclaw service <install|uninstall|status|restart>`.
|
|
2
|
+
// Wraps a lazyclaw long-running command as an always-on OS service via
|
|
3
|
+
// lib/service_install.mjs (launchd / systemd / detached-fallback).
|
|
4
|
+
//
|
|
5
|
+
// v1 manages the DAEMON — the single always-on agent core. Once the channel
|
|
6
|
+
// listeners forward into the daemon's /inbound (next phase), the daemon is
|
|
7
|
+
// the one process that has to stay up; channel surfaces graduate to their own
|
|
8
|
+
// service targets in a later phase.
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import { configPath, readConfig } from '../lib/config.mjs';
|
|
13
|
+
import { assertUnattendedSafe, assertServicePairing } from '../lib/gateway_guard.mjs';
|
|
14
|
+
import { detectBackend, installService, uninstallService, serviceStatus } from '../lib/service_install.mjs';
|
|
15
|
+
|
|
16
|
+
const SUPPORTED_SURFACES = new Set(['daemon', 'gateway']);
|
|
17
|
+
|
|
18
|
+
function hasSystemctl() {
|
|
19
|
+
try {
|
|
20
|
+
const r = spawnSync('systemctl', ['--version'], { stdio: 'ignore' });
|
|
21
|
+
return !r.error && r.status === 0;
|
|
22
|
+
} catch { return false; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function cliPath() {
|
|
26
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'cli.mjs');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Exported for tests: builds the service spec (wrapped argv + backend) without
|
|
30
|
+
// any side effects.
|
|
31
|
+
export function _buildSpec(surface, flags = {}, cfgDir = '', deps = {}) {
|
|
32
|
+
const hasSystemd = typeof deps.hasSystemctl === 'function' ? deps.hasSystemctl() : hasSystemctl();
|
|
33
|
+
const args = [deps.cliPath ? deps.cliPath() : cliPath(), surface];
|
|
34
|
+
// Default the service to the well-known port the channel listeners dial
|
|
35
|
+
// (the LAZYCLAW_DAEMON_URL default), so `service install` + `* listen`
|
|
36
|
+
// work together out of the box. Bare `lazyclaw daemon` still defaults to a
|
|
37
|
+
// random port for ad-hoc / scripted use.
|
|
38
|
+
args.push('--port', flags.port !== undefined ? String(flags.port) : '19600');
|
|
39
|
+
if (flags['auth-token']) args.push('--auth-token', String(flags['auth-token']));
|
|
40
|
+
if (flags.log) args.push('--log', String(flags.log));
|
|
41
|
+
if (flags.channels) args.push('--channels', String(flags.channels));
|
|
42
|
+
return {
|
|
43
|
+
name: surface,
|
|
44
|
+
surface,
|
|
45
|
+
execPath: process.execPath,
|
|
46
|
+
args,
|
|
47
|
+
workingDir: deps.cwd || process.cwd(),
|
|
48
|
+
configDir: cfgDir,
|
|
49
|
+
description: `lazyclaw always-on ${surface}`,
|
|
50
|
+
env: { LAZYCLAW_CONFIG_DIR: cfgDir },
|
|
51
|
+
backend: detectBackend({ override: flags.backend || null, hasSystemctl: hasSystemd }),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function cmdService(sub, positional = [], flags = {}) {
|
|
56
|
+
const surface = (positional[0] || 'daemon').toLowerCase();
|
|
57
|
+
if (!SUPPORTED_SURFACES.has(surface)) {
|
|
58
|
+
console.error(`service: only '${[...SUPPORTED_SURFACES].join("', '")}' is supported in this version (channel gateway lands in a later phase). Got: ${surface}`);
|
|
59
|
+
process.exit(2);
|
|
60
|
+
}
|
|
61
|
+
const cfg = readConfig();
|
|
62
|
+
const cfgDir = path.dirname(configPath());
|
|
63
|
+
|
|
64
|
+
// Fail closed before installing: never wrap a remote surface as a service
|
|
65
|
+
// while the global unattended-sensitive tool override is on (RCE). Channel
|
|
66
|
+
// surfaces additionally require a non-empty pairing allowlist.
|
|
67
|
+
try {
|
|
68
|
+
assertUnattendedSafe(cfg, { surface });
|
|
69
|
+
if (surface !== 'daemon') assertServicePairing(cfg, { service: true, surface });
|
|
70
|
+
} catch (e) { console.error(e.message); process.exit(2); }
|
|
71
|
+
|
|
72
|
+
const spec = _buildSpec(surface, flags, cfgDir);
|
|
73
|
+
const emit = (obj) => process.stdout.write(JSON.stringify(obj) + '\n');
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
if (sub === 'install') {
|
|
77
|
+
const r = installService(spec);
|
|
78
|
+
emit({ ok: true, action: 'install', ...r });
|
|
79
|
+
if (r.backend === 'fallback') {
|
|
80
|
+
process.stderr.write('note: no service manager detected — running detached with a pidfile. This survives the terminal but NOT a reboot. Re-run `lazyclaw service install` after reboot, or install launchd/systemd.\n');
|
|
81
|
+
// The detached child's fate is invisible (stdio ignored), so verify it
|
|
82
|
+
// actually stayed up — a daemon that hit EADDRINUSE (port already in
|
|
83
|
+
// use) exits within ~200ms. Surface that instead of a false "ok".
|
|
84
|
+
await new Promise((s) => setTimeout(s, 800));
|
|
85
|
+
const st = serviceStatus(spec);
|
|
86
|
+
if (!st.running) {
|
|
87
|
+
const port = flags.port !== undefined ? String(flags.port) : '19600';
|
|
88
|
+
process.stderr.write(`warning: the daemon did not stay up — port ${port} may already be in use (a daemon is already running?). Check: lsof -ti tcp:${port}. Free it or pass --port, then re-run.\n`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (sub === 'uninstall' || sub === 'remove') {
|
|
94
|
+
emit({ ok: true, action: 'uninstall', ...uninstallService(spec) });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (sub === 'status') {
|
|
98
|
+
emit({ ok: true, action: 'status', ...serviceStatus(spec) });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (sub === 'restart') {
|
|
102
|
+
try { uninstallService(spec); } catch { /* not installed yet */ }
|
|
103
|
+
emit({ ok: true, action: 'restart', ...installService(spec) });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
} catch (e) {
|
|
107
|
+
console.error(`service: ${e.message}`);
|
|
108
|
+
process.exit(2);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.error('Usage: lazyclaw service <install|uninstall|status|restart> [daemon|gateway] [--port N] [--auth-token T] [--log LEVEL] [--channels a,b] [--backend launchd|systemd|fallback]');
|
|
112
|
+
process.exit(2);
|
|
113
|
+
}
|