lazyclaw 6.3.1 → 6.4.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 +5 -1
- package/README.md +17 -13
- package/agents.mjs +54 -4
- package/channels-discord/index.mjs +5 -1
- package/channels-email/index.mjs +4 -3
- package/channels-whatsapp/index.mjs +3 -2
- package/chat_window.mjs +11 -2
- package/cli.mjs +103 -3
- package/commands/agents.mjs +6 -1
- package/commands/auth_nodes.mjs +19 -1
- package/commands/automation.mjs +20 -5
- package/commands/channels.mjs +46 -3
- package/commands/chat.mjs +90 -33
- package/commands/chat_hardening.mjs +23 -0
- package/commands/daemon.mjs +99 -1
- package/commands/gateway.mjs +123 -4
- package/commands/mcp.mjs +150 -0
- package/commands/misc.mjs +20 -0
- package/commands/sessions.mjs +68 -8
- package/commands/setup.mjs +44 -8
- package/commands/setup_channels.mjs +219 -47
- package/commands/skills.mjs +19 -1
- package/commands/workflow_named.mjs +137 -0
- package/config-validate.mjs +10 -1
- package/config_features.mjs +45 -0
- package/cron.mjs +19 -8
- package/daemon/lib/auth.mjs +25 -0
- package/daemon/lib/cost.mjs +36 -0
- package/daemon/lib/respond.mjs +20 -1
- package/daemon/lib/team_inbound.mjs +69 -0
- package/daemon/route_table.mjs +5 -1
- package/daemon/routes/_deps.mjs +2 -2
- package/daemon/routes/config.mjs +11 -6
- package/daemon/routes/conversation.mjs +103 -40
- package/daemon/routes/events.mjs +45 -0
- package/daemon/routes/meta.mjs +38 -7
- package/daemon/routes/providers.mjs +26 -6
- package/daemon/routes/workflows.mjs +30 -0
- package/daemon.mjs +26 -2
- package/dotenv_min.mjs +15 -3
- package/gateway/challenge_registry.mjs +90 -0
- package/gateway/device_auth.mjs +71 -97
- package/gateway/http_gateway.mjs +29 -3
- package/lib/args.mjs +69 -3
- package/lib/config.mjs +85 -3
- package/lib/render.mjs +43 -0
- package/lib/service_install.mjs +15 -1
- package/mas/agent_turn.mjs +61 -12
- package/mas/confidence.mjs +20 -1
- package/mas/embedder.mjs +100 -0
- package/mas/events.mjs +57 -0
- package/mas/index_db.mjs +103 -10
- package/mas/learning.mjs +96 -72
- package/mas/mention_router.mjs +48 -90
- package/mas/prompt_stack.mjs +60 -3
- package/mas/recall_blend.mjs +56 -0
- package/mas/redact.mjs +55 -0
- package/mas/router_posting.mjs +96 -0
- package/mas/router_termination.mjs +63 -0
- package/mas/scrub_env.mjs +21 -6
- package/mas/skill_synth.mjs +7 -33
- package/mas/tool_runner.mjs +3 -2
- package/mas/tools/bash.mjs +9 -2
- package/mas/tools/coding.mjs +28 -5
- package/mas/tools/delegation.mjs +34 -4
- package/mas/tools/git.mjs +19 -9
- package/mas/tools/ha.mjs +2 -0
- package/mas/tools/learning.mjs +55 -0
- package/mas/tools/media.mjs +21 -3
- package/mas/tools/os.mjs +70 -16
- package/mas/tools/recall.mjs +28 -2
- package/mcp/client.mjs +8 -1
- package/mcp/server_spawn.mjs +5 -1
- package/memory.mjs +24 -0
- package/package.json +3 -1
- package/providers/anthropic.mjs +101 -6
- package/providers/cache.mjs +9 -1
- package/providers/claude_cli.mjs +104 -22
- package/providers/claude_cli_session.mjs +166 -0
- package/providers/cli_error.mjs +38 -0
- package/providers/cli_login.mjs +179 -0
- package/providers/codex_cli.mjs +66 -11
- package/providers/gemini.mjs +101 -3
- package/providers/gemini_cli.mjs +62 -15
- package/providers/model_catalogue.mjs +33 -2
- package/providers/ollama.mjs +104 -3
- package/providers/openai.mjs +110 -8
- package/providers/openai_compat.mjs +97 -6
- package/providers/orchestrator.mjs +112 -12
- package/providers/registry.mjs +15 -9
- package/providers/retry.mjs +14 -2
- package/providers/tool_use/anthropic.mjs +17 -3
- package/providers/tool_use/claude_cli.mjs +72 -20
- package/providers/tool_use/gemini.mjs +18 -2
- package/providers/tool_use/openai.mjs +28 -5
- package/sandbox/confiners/seatbelt.mjs +37 -12
- package/sandbox/index.mjs +49 -0
- package/sandbox/local.mjs +7 -1
- package/sandbox/spawn.mjs +144 -0
- package/sandbox.mjs +5 -1
- package/scripts/loop-worker.mjs +25 -1
- package/sessions.mjs +0 -0
- package/skills/channel-style.md +20 -0
- package/skills/code-review.md +33 -0
- package/skills/commit-message.md +30 -0
- package/skills/concise.md +24 -0
- package/skills/debug-coach.md +25 -0
- package/skills/explain.md +24 -0
- package/skills/korean.md +25 -0
- package/skills/summarize.md +33 -0
- package/skills.mjs +24 -2
- package/skills_curator.mjs +6 -0
- package/skills_install.mjs +10 -2
- package/tasks.mjs +6 -1
- package/teams.mjs +78 -0
- package/tui/chat_mode_slash.mjs +59 -0
- package/tui/config_picker.mjs +1 -0
- package/tui/editor.mjs +178 -151
- package/tui/editor_keys.mjs +275 -0
- package/tui/hud.mjs +111 -0
- package/tui/login_flow.mjs +113 -0
- package/tui/modal_picker.mjs +10 -1
- package/tui/model_pick.mjs +287 -0
- package/tui/orchestrator_flow.mjs +164 -0
- package/tui/pickers.mjs +196 -80
- package/tui/repl.mjs +106 -44
- package/tui/repl_reset.mjs +37 -0
- package/tui/run_turn.mjs +228 -26
- package/tui/slash_args.mjs +159 -0
- package/tui/slash_commands.mjs +7 -5
- package/tui/slash_dispatcher.mjs +537 -344
- package/tui/slash_popup.mjs +5 -1
- package/tui/splash.mjs +15 -2
- package/tui/status_bar.mjs +26 -0
- package/tui/theme.mjs +28 -0
- package/web/avatars/01.png +0 -0
- package/web/avatars/02.png +0 -0
- package/web/avatars/03.png +0 -0
- package/web/avatars/04.png +0 -0
- package/web/avatars/05.png +0 -0
- package/web/avatars/06.png +0 -0
- package/web/avatars/07.png +0 -0
- package/web/avatars/08.png +0 -0
- package/web/avatars/09.png +0 -0
- package/web/avatars/10.png +0 -0
- package/web/avatars/11.png +0 -0
- package/web/avatars/12.png +0 -0
- package/web/avatars/13.png +0 -0
- package/web/avatars/14.png +0 -0
- package/web/avatars/15.png +0 -0
- package/web/avatars/16.png +0 -0
- package/web/avatars/17.png +0 -0
- package/web/avatars/18.png +0 -0
- package/web/avatars/19.png +0 -0
- package/web/avatars/20.png +0 -0
- package/web/dashboard.css +77 -0
- package/web/dashboard.html +29 -2
- package/web/dashboard.js +296 -33
- package/workflow/builtin_caps.mjs +94 -0
- package/workflow/declarative.mjs +101 -0
- package/workflow/named.mjs +71 -0
- package/workflow/named_cron.mjs +50 -0
- package/workflow/nodes.mjs +67 -0
- package/workflow/run_request.mjs +74 -0
- package/workflow/yaml_min.mjs +97 -0
package/tui/pickers.mjs
CHANGED
|
@@ -11,6 +11,40 @@ import {
|
|
|
11
11
|
fetchModelsForProvider as _fetchModelsResolve,
|
|
12
12
|
supportsLiveFetch as _supportsLiveFetch,
|
|
13
13
|
} from '../providers/model_catalogue.mjs';
|
|
14
|
+
import { paint } from './theme.mjs';
|
|
15
|
+
|
|
16
|
+
// Read exactly ONE line from a non-TTY stream (piped stdin) and hand the rest
|
|
17
|
+
// back so the next reader (the next picker / a credential prompt) still gets
|
|
18
|
+
// its input. The old fallbacks resolved the whole buffered payload at the
|
|
19
|
+
// first newline — matching no id (skip) and swallowing every later line so the
|
|
20
|
+
// following prompt hung. Pause before unshift so the buffered remainder isn't
|
|
21
|
+
// dropped while no 'data' listener is attached.
|
|
22
|
+
export function _readOneLine(stream) {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
let buf = '';
|
|
25
|
+
const done = (line) => {
|
|
26
|
+
stream.off('data', onData);
|
|
27
|
+
stream.off('end', onEnd);
|
|
28
|
+
resolve(line.replace(/\r$/, '').trim());
|
|
29
|
+
};
|
|
30
|
+
const onData = (chunk) => {
|
|
31
|
+
buf += chunk.toString();
|
|
32
|
+
const nl = buf.indexOf('\n');
|
|
33
|
+
if (nl === -1) return;
|
|
34
|
+
stream.pause();
|
|
35
|
+
const rest = buf.slice(nl + 1);
|
|
36
|
+
if (rest) stream.unshift(Buffer.from(rest, 'utf8'));
|
|
37
|
+
done(buf.slice(0, nl));
|
|
38
|
+
};
|
|
39
|
+
const onEnd = () => done(buf);
|
|
40
|
+
stream.on('data', onData);
|
|
41
|
+
stream.on('end', onEnd);
|
|
42
|
+
// A prior _readOneLine pauses the stream after unshifting its remainder.
|
|
43
|
+
// Adding a 'data' listener does NOT auto-resume an explicitly-paused
|
|
44
|
+
// stream, so resume() here or the next read would block on buffered input.
|
|
45
|
+
stream.resume();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
14
48
|
|
|
15
49
|
export function _attachGhostAutocomplete(rl) {
|
|
16
50
|
// Returns `{ dispose, suspend, resume }`. Dispose detaches the keypress +
|
|
@@ -107,36 +141,15 @@ export function _attachGhostAutocomplete(rl) {
|
|
|
107
141
|
};
|
|
108
142
|
}
|
|
109
143
|
|
|
110
|
-
// LazyClaw banner —
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
// Single source of truth for the LazyClaw banner — used by the chat
|
|
115
|
-
// REPL header, the no-arg launcher, and the first-run welcome panel.
|
|
116
|
-
// Returns an array of pre-formatted lines (with ANSI colour) so the
|
|
117
|
-
// caller can splice in additional rows without re-implementing the
|
|
118
|
-
// alignment.
|
|
119
|
-
//
|
|
120
|
-
// Width-management rule: every inner line is forced through
|
|
121
|
-
// `.padEnd(W)` so a stray width miscount can't punch the right
|
|
122
|
-
// border off the box (which is exactly the bug v3.99.5 shipped:
|
|
123
|
-
// v4.2.2 — boxed figlet "lazy" wordmark, single-colour orange. The
|
|
124
|
-
// previous mixed-colour banner (helmet-red letter art + ink-beige
|
|
125
|
-
// caption) read as "two banners glued together" because the colour
|
|
126
|
-
// changed mid-box. We use one warm orange (#F08246) for everything —
|
|
127
|
-
// border, letter art, caption — so the eye reads it as one badge.
|
|
128
|
-
//
|
|
129
|
-
// Letter art is figlet "standard" (6 rows) rather than the v3.99.11
|
|
130
|
-
// "small" (4 rows), because small renders as a pixel mush in most
|
|
131
|
-
// terminal fonts. Standard's strokes are wide enough that the
|
|
132
|
-
// letters read as `l a z y` even at small terminal sizes.
|
|
133
|
-
//
|
|
134
|
-
// Layout invariant: every inner row is exactly INNER_W visible cells
|
|
135
|
-
// (no double-width glyphs, all chars are 1 cell in any monospace
|
|
136
|
-
// font), so the right edge `│` always lands in the same column.
|
|
144
|
+
// LazyClaw banner — single source of truth (chat REPL header, no-arg
|
|
145
|
+
// launcher, first-run welcome). Printed once so users see the active
|
|
146
|
+
// provider/model; plain ANSI, auto-skipped when stdout isn't a TTY.
|
|
147
|
+
// Returns an array of pre-formatted lines the caller can splice rows into.
|
|
137
148
|
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
149
|
+
// Layout invariants: every inner row is forced through `.padEnd(INNER_W)`
|
|
150
|
+
// and is exactly INNER_W single-cell glyphs, so the right border `│` always
|
|
151
|
+
// lands in the same column. _renderMascot / _renderMascotTiny are stubs kept
|
|
152
|
+
// so any leftover caller doesn't crash.
|
|
140
153
|
|
|
141
154
|
const _ORANGE_RGB = '241;130;70'; // #F18246
|
|
142
155
|
export function _orange(s) { return `\x1b[38;2;${_ORANGE_RGB}m${s}\x1b[0m`; }
|
|
@@ -261,14 +274,7 @@ export async function _arrowMenu({ title, subtitle, footer, items, defaultIdx =
|
|
|
261
274
|
process.stderr.write(`${title}\n`);
|
|
262
275
|
items.forEach((it, i) => process.stderr.write(` ${i + 1}. ${it.label}${it.desc ? ' — ' + it.desc : ''}\n`));
|
|
263
276
|
process.stderr.write('pick (number / id, blank for first): ');
|
|
264
|
-
const ans = await
|
|
265
|
-
let buf = '';
|
|
266
|
-
const onData = (chunk) => {
|
|
267
|
-
buf += chunk.toString();
|
|
268
|
-
if (buf.includes('\n')) { process.stdin.off('data', onData); resolve(buf.trim()); }
|
|
269
|
-
};
|
|
270
|
-
process.stdin.on('data', onData);
|
|
271
|
-
});
|
|
277
|
+
const ans = await _readOneLine(process.stdin);
|
|
272
278
|
if (!ans) return items[0];
|
|
273
279
|
const byNum = parseInt(ans, 10);
|
|
274
280
|
if (Number.isFinite(byNum) && byNum >= 1 && byNum <= items.length) return items[byNum - 1];
|
|
@@ -286,9 +292,18 @@ export async function _arrowMenu({ title, subtitle, footer, items, defaultIdx =
|
|
|
286
292
|
// before drawing so the picker always receives the first keypress.
|
|
287
293
|
process.stdin.resume();
|
|
288
294
|
if (process.stdin.ref) process.stdin.ref();
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
295
|
+
// Render on the ALTERNATE screen buffer (the same trick vim / less / fzf
|
|
296
|
+
// use). Without it, the menu's full-screen \x1b[2J clears land on the main
|
|
297
|
+
// buffer, interleave with the readline output from prior wizard steps, and
|
|
298
|
+
// every step pushes a screenful into scrollback — the "화면이 밀린다" bug.
|
|
299
|
+
// On the alt buffer the menu draws in isolation; leaving it (cleanup) restores
|
|
300
|
+
// the main buffer (and the wizard text on it) verbatim, with nothing pushed.
|
|
301
|
+
const altScreen = !!(process.stdout.isTTY);
|
|
302
|
+
if (altScreen) process.stdout.write('\x1b[?1049h');
|
|
303
|
+
// Menu chrome via the theme gate: plain text under NO_COLOR / dumb / non-TTY.
|
|
304
|
+
const accent = (s) => paint('38;5;208', s);
|
|
305
|
+
const dim = (s) => paint('2', s);
|
|
306
|
+
const bold = (s) => paint('1', s);
|
|
292
307
|
|
|
293
308
|
// Typeahead state. `query` accumulates printable chars when searchable
|
|
294
309
|
// is on; the visible item slice is recomputed on every keystroke. We
|
|
@@ -381,7 +396,10 @@ export async function _arrowMenu({ title, subtitle, footer, items, defaultIdx =
|
|
|
381
396
|
cleanup();
|
|
382
397
|
resolve(view[idx]);
|
|
383
398
|
}
|
|
384
|
-
|
|
399
|
+
// Ctrl+C mid-wizard cancels this step (resolve the CANCEL sentinel the
|
|
400
|
+
// callers already handle) instead of hard-killing the whole process —
|
|
401
|
+
// a wizard step must be abortable without taking the app down.
|
|
402
|
+
else if (key.ctrl && key.name === 'c') { cleanup(); resolve('CANCEL'); }
|
|
385
403
|
else if (key.ctrl && key.name === 'u') { if (searchable) { query = ''; recompute(); } }
|
|
386
404
|
else if (key.name === 'escape') {
|
|
387
405
|
if (searchable && query) { query = ''; recompute(); return; }
|
|
@@ -403,12 +421,50 @@ export async function _arrowMenu({ title, subtitle, footer, items, defaultIdx =
|
|
|
403
421
|
const cleanup = () => {
|
|
404
422
|
process.stdin.off('keypress', onKey);
|
|
405
423
|
if (process.stdin.setRawMode) process.stdin.setRawMode(false);
|
|
406
|
-
|
|
424
|
+
// Show the cursor, then leave the alt screen → the main buffer (with the
|
|
425
|
+
// wizard's prior output) reappears exactly as it was, nothing pushed into
|
|
426
|
+
// scrollback. Fall back to a clear+home when the alt buffer wasn't used.
|
|
427
|
+
if (altScreen) process.stdout.write('\x1b[?25h\x1b[?1049l');
|
|
428
|
+
else process.stdout.write('\x1b[?25h\x1b[2J\x1b[H');
|
|
429
|
+
// Release stdin so a one-shot CLI caller (the setup / onboard wizard) can
|
|
430
|
+
// exit. We resume()+ref() stdin on entry to receive keypresses; if it's
|
|
431
|
+
// never unref'd, the event loop stays alive after the LAST picker and the
|
|
432
|
+
// process hangs at "Setup complete" instead of returning to the shell.
|
|
433
|
+
// The chat REPL re-refs stdin (via _pauseChatForSubMenu's finally) and
|
|
434
|
+
// its own readline keeps the loop alive, so this is safe there.
|
|
435
|
+
if (process.stdin.unref) process.stdin.unref();
|
|
407
436
|
};
|
|
408
437
|
process.stdin.on('keypress', onKey);
|
|
409
438
|
});
|
|
410
439
|
}
|
|
411
440
|
|
|
441
|
+
// Arrow-key yes/no — replaces the typed `[Y/n]` prompts in the wizard so the
|
|
442
|
+
// user never types a letter. Returns a boolean. Esc / q resolve to the default.
|
|
443
|
+
// Inherits _arrowMenu's non-TTY fallback (reads a line). `pick` is injectable
|
|
444
|
+
// for tests.
|
|
445
|
+
export async function _pickYesNo(title, { subtitle, yesLabel = 'Yes', noLabel = 'No', defaultYes = true, pick = _arrowMenu } = {}) {
|
|
446
|
+
const picked = await pick({
|
|
447
|
+
title,
|
|
448
|
+
subtitle,
|
|
449
|
+
items: [
|
|
450
|
+
{ id: 'yes', label: yesLabel },
|
|
451
|
+
{ id: 'no', label: noLabel },
|
|
452
|
+
],
|
|
453
|
+
defaultIdx: defaultYes ? 0 : 1,
|
|
454
|
+
});
|
|
455
|
+
if (picked === 'BACK' || picked === 'CANCEL' || picked == null) return defaultYes;
|
|
456
|
+
const id = typeof picked === 'object' ? picked.id : picked;
|
|
457
|
+
return id === 'yes';
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Arrow-key single choice — `options` is [{ id, label, desc }]. Returns the
|
|
461
|
+
// chosen id, or `fallback` on Esc/cancel. `pick` injectable for tests.
|
|
462
|
+
export async function _pickChoice(title, options, { subtitle, defaultIdx = 0, fallback = null, pick = _arrowMenu } = {}) {
|
|
463
|
+
const picked = await pick({ title, subtitle, items: options, defaultIdx, searchable: false });
|
|
464
|
+
if (picked === 'BACK' || picked === 'CANCEL' || picked == null) return fallback;
|
|
465
|
+
return typeof picked === 'object' ? picked.id : picked;
|
|
466
|
+
}
|
|
467
|
+
|
|
412
468
|
// Bucket every registered provider into one of three auth-method
|
|
413
469
|
// families. The picker's first step asks the user which family
|
|
414
470
|
// they want before drilling into specific providers — much less
|
|
@@ -420,10 +476,10 @@ export function _providerFamilies() {
|
|
|
420
476
|
// tui/provider_families.mjs; the ANSI tags below are readline-specific.
|
|
421
477
|
const b = _bucketProviders(getRegistry());
|
|
422
478
|
return {
|
|
423
|
-
api: { label: 'API key', desc: 'paste an sk-... key during setup', tag: '
|
|
424
|
-
cli: { label: 'CLI / Local', desc: 'keyless — uses an existing CLI login or a local daemon', tag: '
|
|
425
|
-
meta: { label: 'Multi-agent', desc: 'orchestrator — fan a task out to a planner + workers (advanced)', tag: '
|
|
426
|
-
mock: { label: 'Mock', desc: 'offline echo, only useful for testing', tag: '
|
|
479
|
+
api: { label: 'API key', desc: 'paste an sk-... key during setup', tag: paint('38;5;245', '[needs key]'), members: b.api },
|
|
480
|
+
cli: { label: 'CLI / Local', desc: 'keyless — uses an existing CLI login or a local daemon', tag: paint('38;5;208', '[no key]'), members: b.cli },
|
|
481
|
+
meta: { label: 'Multi-agent', desc: 'orchestrator — fan a task out to a planner + workers (advanced)', tag: paint('38;5;245', '[meta]'), members: b.meta },
|
|
482
|
+
mock: { label: 'Mock', desc: 'offline echo, only useful for testing', tag: paint('38;5;245', '[test]'), members: b.mock },
|
|
427
483
|
};
|
|
428
484
|
}
|
|
429
485
|
|
|
@@ -446,14 +502,7 @@ export async function _pickProviderInteractive() {
|
|
|
446
502
|
// Non-TTY fallback — single-prompt picker, identical to before.
|
|
447
503
|
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
448
504
|
process.stdout.write(`provider [${providers.join('|')}]: `);
|
|
449
|
-
const ans = await
|
|
450
|
-
let buf = '';
|
|
451
|
-
const onData = (chunk) => {
|
|
452
|
-
buf += chunk.toString();
|
|
453
|
-
if (buf.includes('\n')) { process.stdin.off('data', onData); resolve(buf.trim()); }
|
|
454
|
-
};
|
|
455
|
-
process.stdin.on('data', onData);
|
|
456
|
-
});
|
|
505
|
+
const ans = await _readOneLine(process.stdin);
|
|
457
506
|
// v5.3.2 — non-TTY fallback used to be `providers[0]`, which was
|
|
458
507
|
// whatever happened to be first in the registry (currently
|
|
459
508
|
// anthropic). Pin to claude-cli to match the interactive onboard
|
|
@@ -510,8 +559,8 @@ export async function _pickProviderInteractive() {
|
|
|
510
559
|
label: name,
|
|
511
560
|
desc,
|
|
512
561
|
tag: isCustom
|
|
513
|
-
? '
|
|
514
|
-
: (meta.requiresApiKey ? '
|
|
562
|
+
? paint('38;5;213', '[custom]')
|
|
563
|
+
: (meta.requiresApiKey ? paint('38;5;245', '[api key]') : paint('38;5;208', '[no key]')),
|
|
515
564
|
};
|
|
516
565
|
});
|
|
517
566
|
// Surface a "+ Add a new custom endpoint…" entry inside the API-key
|
|
@@ -523,7 +572,7 @@ export async function _pickProviderInteractive() {
|
|
|
523
572
|
id: '__add_custom__',
|
|
524
573
|
label: '+ Add a custom OpenAI-compatible endpoint…',
|
|
525
574
|
desc: 'NVIDIA NIM · OpenRouter · Together · Groq · vLLM · LM Studio · …',
|
|
526
|
-
tag: '
|
|
575
|
+
tag: paint('38;5;213', '[new]'),
|
|
527
576
|
});
|
|
528
577
|
}
|
|
529
578
|
if (memberNames.length === 1 && family.id !== 'api') {
|
|
@@ -577,10 +626,10 @@ export async function _pickProviderInteractive() {
|
|
|
577
626
|
// workers: multi-select with a running list + add/remove/done loop.
|
|
578
627
|
// maxSubtasks: typed integer, default 5.
|
|
579
628
|
export async function _setupOrchestratorInteractive() {
|
|
580
|
-
const accent = (s) =>
|
|
581
|
-
const dim = (s) =>
|
|
582
|
-
const bold = (s) =>
|
|
583
|
-
const ok = (s) =>
|
|
629
|
+
const accent = (s) => paint('38;5;208', s);
|
|
630
|
+
const dim = (s) => paint('2', s);
|
|
631
|
+
const bold = (s) => paint('1', s);
|
|
632
|
+
const ok = (s) => paint('32', s);
|
|
584
633
|
const info = getRegistry().PROVIDER_INFO || {};
|
|
585
634
|
const eligibleNames = Object.keys(getRegistry().PROVIDERS).filter((n) => n !== 'orchestrator' && n !== 'mock');
|
|
586
635
|
if (eligibleNames.length === 0) {
|
|
@@ -735,25 +784,40 @@ export async function _pickModelInteractive(providerId, opts = {}) {
|
|
|
735
784
|
let dynamicModels = [];
|
|
736
785
|
while (true) {
|
|
737
786
|
const allModels = Array.from(new Set([...baseModels, ...dynamicModels]));
|
|
738
|
-
|
|
787
|
+
// "Use the provider's own default" — send no `-m`, the reliable path for
|
|
788
|
+
// keyless CLI providers (codex-cli / gemini-cli) whose allowed model set
|
|
789
|
+
// is fixed by the logged-in account: forcing a model the plan isn't
|
|
790
|
+
// entitled to makes the CLI reject the turn. Pre-selected when the
|
|
791
|
+
// provider declares no defaultModel.
|
|
792
|
+
const modelItems = [{
|
|
793
|
+
id: '__provider_default__',
|
|
794
|
+
label: "▷ Use the provider's own default model",
|
|
795
|
+
desc: 'no model override — the CLI/login picks (recommended for codex-cli / gemini-cli)',
|
|
796
|
+
tag: paint('38;5;208', '[default]'),
|
|
797
|
+
}];
|
|
739
798
|
if (supportsLiveFetch) {
|
|
740
|
-
modelItems.
|
|
799
|
+
modelItems.push({
|
|
741
800
|
id: '__fetch_models__',
|
|
742
801
|
label: '↻ Fetch live model list from /v1/models',
|
|
743
802
|
desc: isCustom || isBuiltinCompat ? `GET ${meta.baseUrl}/models` : 'pulls the up-to-date catalogue from the provider',
|
|
744
|
-
tag: '
|
|
803
|
+
tag: paint('38;5;245', '[live]'),
|
|
745
804
|
});
|
|
746
805
|
}
|
|
806
|
+
for (const m of allModels) modelItems.push({ id: m, label: m, desc: '' });
|
|
747
807
|
modelItems.push({
|
|
748
808
|
id: '__custom_model__',
|
|
749
809
|
label: '… type a custom model id',
|
|
750
810
|
desc: 'use any model id supported by this provider, even if not listed above',
|
|
751
|
-
tag: '
|
|
811
|
+
tag: paint('38;5;245', '[free]'),
|
|
752
812
|
});
|
|
753
813
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
814
|
+
// Land the cursor on the configured default model when there is one;
|
|
815
|
+
// otherwise pre-select "use the provider's own default" (index 0).
|
|
816
|
+
let defaultIdx = 0;
|
|
817
|
+
if (meta.defaultModel) {
|
|
818
|
+
const i = modelItems.findIndex((it) => it.id === meta.defaultModel);
|
|
819
|
+
if (i >= 0) defaultIdx = i;
|
|
820
|
+
}
|
|
757
821
|
const titlePrefix = opts.titlePrefix ? `${opts.titlePrefix} ` : '';
|
|
758
822
|
const picked = await _arrowMenu({
|
|
759
823
|
title: `${titlePrefix}pick a model for ${providerId}`,
|
|
@@ -764,6 +828,8 @@ export async function _pickModelInteractive(providerId, opts = {}) {
|
|
|
764
828
|
});
|
|
765
829
|
if (picked === 'CANCEL') return 'CANCEL';
|
|
766
830
|
if (picked === 'BACK') return 'BACK';
|
|
831
|
+
// Empty string = "no explicit model" (use the provider/CLI default).
|
|
832
|
+
if (picked.id === '__provider_default__') return '';
|
|
767
833
|
if (picked.id === '__custom_model__') {
|
|
768
834
|
const typed = (await _quickPrompt(` model id for ${providerId}: `)).trim();
|
|
769
835
|
if (!typed) continue;
|
|
@@ -774,7 +840,7 @@ export async function _pickModelInteractive(providerId, opts = {}) {
|
|
|
774
840
|
process.stdout.write(`\n fetching ${providerId} model list…\n`);
|
|
775
841
|
const fetched = await _fetchModelsForProvider(providerId);
|
|
776
842
|
if (!fetched.length) {
|
|
777
|
-
process.stdout.write(` ${'
|
|
843
|
+
process.stdout.write(` ${paint(33, 'no models returned')} — falling back to the suggested list.\n`);
|
|
778
844
|
await _quickPrompt(' press Enter to continue ');
|
|
779
845
|
} else {
|
|
780
846
|
dynamicModels = fetched;
|
|
@@ -782,7 +848,7 @@ export async function _pickModelInteractive(providerId, opts = {}) {
|
|
|
782
848
|
await _quickPrompt(' press Enter to pick one ');
|
|
783
849
|
}
|
|
784
850
|
} catch (e) {
|
|
785
|
-
process.stdout.write(`\n ${'
|
|
851
|
+
process.stdout.write(`\n ${paint(33, 'fetch failed:')} ${e?.message || e}\n`);
|
|
786
852
|
await _quickPrompt(' press Enter to continue ');
|
|
787
853
|
}
|
|
788
854
|
continue;
|
|
@@ -819,10 +885,10 @@ export async function _fetchModelsForProvider(providerId) {
|
|
|
819
885
|
// Persists into cfg.customProviders[] and returns { name } on success,
|
|
820
886
|
// or null when the user backs out.
|
|
821
887
|
export async function _addCustomProviderInteractive() {
|
|
822
|
-
const accent = (s) =>
|
|
823
|
-
const dim = (s) =>
|
|
824
|
-
const bold = (s) =>
|
|
825
|
-
const ok = (s) =>
|
|
888
|
+
const accent = (s) => paint('38;5;208', s);
|
|
889
|
+
const dim = (s) => paint('2', s);
|
|
890
|
+
const bold = (s) => paint('1', s);
|
|
891
|
+
const ok = (s) => paint('32', s);
|
|
826
892
|
|
|
827
893
|
process.stdout.write('\x1b[2J\x1b[H');
|
|
828
894
|
process.stdout.write(accent('Add a custom OpenAI-compatible endpoint') + '\n');
|
|
@@ -844,7 +910,7 @@ export async function _addCustomProviderInteractive() {
|
|
|
844
910
|
}
|
|
845
911
|
try { name = validateCustomProviderName(raw); }
|
|
846
912
|
catch (e) {
|
|
847
|
-
process.stdout.write(`
|
|
913
|
+
process.stdout.write(` ${paint(33, e.message)} — try again.\n`);
|
|
848
914
|
continue;
|
|
849
915
|
}
|
|
850
916
|
// OpenAI-compat builtins (nim / openrouter / groq / …) can be overridden
|
|
@@ -854,9 +920,9 @@ export async function _addCustomProviderInteractive() {
|
|
|
854
920
|
// the override so it isn't a silent surprise.
|
|
855
921
|
if (typeof isBuiltinOpenAICompatName === 'function' && isBuiltinOpenAICompatName(name)) {
|
|
856
922
|
process.stdout.write(
|
|
857
|
-
`
|
|
923
|
+
paint(2, `Note: "${name}" is a built-in OpenAI-compatible provider; ` +
|
|
858
924
|
`your custom entry will override the built-in baseUrl/api-key for this install. ` +
|
|
859
|
-
`Remove with: lazyclaw providers remove ${name}\
|
|
925
|
+
`Remove with: lazyclaw providers remove ${name}`) + '\n'
|
|
860
926
|
);
|
|
861
927
|
}
|
|
862
928
|
break;
|
|
@@ -864,7 +930,7 @@ export async function _addCustomProviderInteractive() {
|
|
|
864
930
|
const baseUrlRaw = (await _quickPrompt(` ${bold('baseUrl')} ${dim('(must end in /v1, no trailing slash needed):')} `)).trim();
|
|
865
931
|
if (!baseUrlRaw) { process.stdout.write(dim(' cancelled — baseUrl is required.\n')); return null; }
|
|
866
932
|
if (!/^https?:\/\//i.test(baseUrlRaw)) {
|
|
867
|
-
process.stdout.write(
|
|
933
|
+
process.stdout.write(` ${paint(33, 'baseUrl must start with http:// or https://')} — cancelled.\n`);
|
|
868
934
|
return null;
|
|
869
935
|
}
|
|
870
936
|
const apiKey = (await _quickPrompt(` ${bold('api-key')} ${dim('(blank if the endpoint is auth-less, e.g. local vLLM):')} `)).trim();
|
|
@@ -888,7 +954,7 @@ export async function _addCustomProviderInteractive() {
|
|
|
888
954
|
} else if (result.probe.ok) {
|
|
889
955
|
probeMsg = ` ${ok('✓')} registered — /v1/models returned no entries (will rely on free-text model id).\n`;
|
|
890
956
|
} else {
|
|
891
|
-
probeMsg = `
|
|
957
|
+
probeMsg = ` ${paint(33, '!')} registered, but /v1/models probe failed: ${result.probe.error}\n`;
|
|
892
958
|
}
|
|
893
959
|
process.stdout.write('\n');
|
|
894
960
|
process.stdout.write(` ${ok(bold('✓ custom provider saved:'))} ${entry.name} ${dim('→')} ${entry.baseUrl}\n`);
|
|
@@ -897,7 +963,15 @@ export async function _addCustomProviderInteractive() {
|
|
|
897
963
|
await _quickPrompt(' press Enter to continue ');
|
|
898
964
|
return { name };
|
|
899
965
|
}
|
|
900
|
-
|
|
966
|
+
// Single-line readline prompt. Pass { secret: true } to mask the typed value
|
|
967
|
+
// (api keys, channel tokens) — the bytes are read in raw mode and echoed as
|
|
968
|
+
// bullets so the secret never appears on screen / scrollback / a screen-share.
|
|
969
|
+
// Non-TTY input can't be masked (no raw mode); it falls back to the plain read,
|
|
970
|
+
// which is fine for piped automation where there is no screen to leak to.
|
|
971
|
+
export async function _quickPrompt(label, opts = {}) {
|
|
972
|
+
if (opts.secret && process.stdin.isTTY && process.stdin.setRawMode) {
|
|
973
|
+
return _quickPromptSecret(label);
|
|
974
|
+
}
|
|
901
975
|
const readline = await import('node:readline');
|
|
902
976
|
process.stdout.write('\n');
|
|
903
977
|
// Make sure stdin is in cooked / line-buffered mode for the
|
|
@@ -914,3 +988,45 @@ export async function _quickPrompt(label) {
|
|
|
914
988
|
rl.close();
|
|
915
989
|
return ans.trim();
|
|
916
990
|
}
|
|
991
|
+
|
|
992
|
+
// Masked raw-mode reader: echoes one bullet per typed character, handles
|
|
993
|
+
// Backspace, Enter/Ctrl-D (submit), and Ctrl-C (abort → empty string). The real
|
|
994
|
+
// characters accumulate off-screen and are returned trimmed.
|
|
995
|
+
export async function _quickPromptSecret(label) {
|
|
996
|
+
const stdin = process.stdin;
|
|
997
|
+
process.stdout.write('\n' + label);
|
|
998
|
+
const wasRaw = !!stdin.isRaw;
|
|
999
|
+
try { stdin.setRawMode(true); } catch (_) {}
|
|
1000
|
+
stdin.resume();
|
|
1001
|
+
if (stdin.ref) stdin.ref();
|
|
1002
|
+
const prevEnc = stdin.readableEncoding;
|
|
1003
|
+
stdin.setEncoding('utf8');
|
|
1004
|
+
let buf = '';
|
|
1005
|
+
const value = await new Promise((resolve) => {
|
|
1006
|
+
const onData = (chunk) => {
|
|
1007
|
+
for (const ch of String(chunk)) {
|
|
1008
|
+
if (ch === '\r' || ch === '\n' || ch === '\x04') { // Enter / Ctrl-D
|
|
1009
|
+
cleanup(); resolve(buf); return;
|
|
1010
|
+
}
|
|
1011
|
+
if (ch === '\x03') { // Ctrl-C abort
|
|
1012
|
+
cleanup(); resolve(''); return;
|
|
1013
|
+
}
|
|
1014
|
+
if (ch === '\x7f' || ch === '\x08') { // Backspace / Delete
|
|
1015
|
+
if (buf.length) { buf = buf.slice(0, -1); process.stdout.write('\b \b'); }
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
if (ch < ' ') continue; // ignore other control chars (arrows, etc.)
|
|
1019
|
+
buf += ch;
|
|
1020
|
+
process.stdout.write('•');
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
const cleanup = () => {
|
|
1024
|
+
stdin.removeListener('data', onData);
|
|
1025
|
+
try { stdin.setRawMode(wasRaw); } catch (_) {}
|
|
1026
|
+
if (prevEnc) { try { stdin.setEncoding(prevEnc); } catch (_) {} }
|
|
1027
|
+
process.stdout.write('\n');
|
|
1028
|
+
};
|
|
1029
|
+
stdin.on('data', onData);
|
|
1030
|
+
});
|
|
1031
|
+
return value.trim();
|
|
1032
|
+
}
|