lazyclaw 6.3.0 → 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 +95 -37
- 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 +210 -95
- 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
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// Interactive TUI helpers — readline pickers, banner
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// are siblings (./banner.generated.mjs, ./wordmark.mjs).
|
|
1
|
+
// Interactive TUI helpers — readline pickers, banner renderers, arrow-key
|
|
2
|
+
// menu, provider/model selection, _quickPrompt. Extracted from cli.mjs (D4);
|
|
3
|
+
// lives in tui/ so banner asset imports are siblings.
|
|
5
4
|
import { readConfig, writeConfig, _resolveAuthKey } from '../lib/config.mjs';
|
|
5
|
+
import { SLASH_COMMANDS } from './slash_commands.mjs';
|
|
6
6
|
import { ensureRegistry, getRegistry } from '../lib/registry_boot.mjs';
|
|
7
7
|
import { bucketProviders as _bucketProviders } from './provider_families.mjs';
|
|
8
8
|
import { addCustomProvider } from '../providers/custom_provider.mjs';
|
|
@@ -11,15 +11,47 @@ 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
|
-
// Returns `{ dispose, suspend, resume }`. Dispose detaches the
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// ghost-render escapes — that interleaving is what surfaces as
|
|
22
|
-
// visible gaps between Korean characters in long replies.
|
|
50
|
+
// Returns `{ dispose, suspend, resume }`. Dispose detaches the keypress +
|
|
51
|
+
// rl 'line' listeners (leaking them is the v3.92 slow-exit bug). Suspend /
|
|
52
|
+
// resume gate the keypress handler so streaming chat output isn't
|
|
53
|
+
// interleaved with `\x1b[s\x1b[K\x1b[u` ghost-render escapes — that
|
|
54
|
+
// interleaving surfaced as visible gaps between Korean characters.
|
|
23
55
|
const noop = () => {};
|
|
24
56
|
if (!process.stdout.isTTY) return { dispose: noop, suspend: noop, resume: noop };
|
|
25
57
|
const cmds = SLASH_COMMANDS.map((c) => c.cmd);
|
|
@@ -109,36 +141,15 @@ export function _attachGhostAutocomplete(rl) {
|
|
|
109
141
|
};
|
|
110
142
|
}
|
|
111
143
|
|
|
112
|
-
// LazyClaw banner —
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
// Single source of truth for the LazyClaw banner — used by the chat
|
|
117
|
-
// REPL header, the no-arg launcher, and the first-run welcome panel.
|
|
118
|
-
// Returns an array of pre-formatted lines (with ANSI colour) so the
|
|
119
|
-
// caller can splice in additional rows without re-implementing the
|
|
120
|
-
// alignment.
|
|
121
|
-
//
|
|
122
|
-
// Width-management rule: every inner line is forced through
|
|
123
|
-
// `.padEnd(W)` so a stray width miscount can't punch the right
|
|
124
|
-
// border off the box (which is exactly the bug v3.99.5 shipped:
|
|
125
|
-
// v4.2.2 — boxed figlet "lazy" wordmark, single-colour orange. The
|
|
126
|
-
// previous mixed-colour banner (helmet-red letter art + ink-beige
|
|
127
|
-
// caption) read as "two banners glued together" because the colour
|
|
128
|
-
// changed mid-box. We use one warm orange (#F08246) for everything —
|
|
129
|
-
// border, letter art, caption — so the eye reads it as one badge.
|
|
130
|
-
//
|
|
131
|
-
// Letter art is figlet "standard" (6 rows) rather than the v3.99.11
|
|
132
|
-
// "small" (4 rows), because small renders as a pixel mush in most
|
|
133
|
-
// terminal fonts. Standard's strokes are wide enough that the
|
|
134
|
-
// letters read as `l a z y` even at small terminal sizes.
|
|
135
|
-
//
|
|
136
|
-
// Layout invariant: every inner row is exactly INNER_W visible cells
|
|
137
|
-
// (no double-width glyphs, all chars are 1 cell in any monospace
|
|
138
|
-
// 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.
|
|
139
148
|
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
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.
|
|
142
153
|
|
|
143
154
|
const _ORANGE_RGB = '241;130;70'; // #F18246
|
|
144
155
|
export function _orange(s) { return `\x1b[38;2;${_ORANGE_RGB}m${s}\x1b[0m`; }
|
|
@@ -219,15 +230,16 @@ export async function _renderV5Banner(version) {
|
|
|
219
230
|
return rows;
|
|
220
231
|
}
|
|
221
232
|
|
|
222
|
-
export function _printChatBanner(activeProvName, activeModel, version) {
|
|
233
|
+
export async function _printChatBanner(activeProvName, activeModel, version) {
|
|
223
234
|
if (!process.stdout.isTTY) return;
|
|
224
|
-
// Single-hue header: labels dim-orange, values/emphasis full-orange
|
|
225
|
-
//
|
|
235
|
+
// Single-hue header: labels dim-orange, values/emphasis full-orange. Uses
|
|
236
|
+
// the v5 sloth splash (NOT the retired v4 figlet box — see _renderV5Banner;
|
|
237
|
+
// figlet remains only as the missing-asset last resort).
|
|
226
238
|
const dimOrange = (s) => `\x1b[2m\x1b[38;2;${_ORANGE_RGB}m${s}\x1b[0m`;
|
|
227
239
|
const orange = _orange;
|
|
228
240
|
const lines = [
|
|
229
241
|
'',
|
|
230
|
-
...
|
|
242
|
+
...(await _renderV5Banner(version)),
|
|
231
243
|
'',
|
|
232
244
|
` ${dimOrange('provider ·')} ${orange(activeProvName)}`,
|
|
233
245
|
` ${dimOrange('model ·')} ${orange(activeModel || '(default)')}`,
|
|
@@ -262,14 +274,7 @@ export async function _arrowMenu({ title, subtitle, footer, items, defaultIdx =
|
|
|
262
274
|
process.stderr.write(`${title}\n`);
|
|
263
275
|
items.forEach((it, i) => process.stderr.write(` ${i + 1}. ${it.label}${it.desc ? ' — ' + it.desc : ''}\n`));
|
|
264
276
|
process.stderr.write('pick (number / id, blank for first): ');
|
|
265
|
-
const ans = await
|
|
266
|
-
let buf = '';
|
|
267
|
-
const onData = (chunk) => {
|
|
268
|
-
buf += chunk.toString();
|
|
269
|
-
if (buf.includes('\n')) { process.stdin.off('data', onData); resolve(buf.trim()); }
|
|
270
|
-
};
|
|
271
|
-
process.stdin.on('data', onData);
|
|
272
|
-
});
|
|
277
|
+
const ans = await _readOneLine(process.stdin);
|
|
273
278
|
if (!ans) return items[0];
|
|
274
279
|
const byNum = parseInt(ans, 10);
|
|
275
280
|
if (Number.isFinite(byNum) && byNum >= 1 && byNum <= items.length) return items[byNum - 1];
|
|
@@ -287,9 +292,18 @@ export async function _arrowMenu({ title, subtitle, footer, items, defaultIdx =
|
|
|
287
292
|
// before drawing so the picker always receives the first keypress.
|
|
288
293
|
process.stdin.resume();
|
|
289
294
|
if (process.stdin.ref) process.stdin.ref();
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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);
|
|
293
307
|
|
|
294
308
|
// Typeahead state. `query` accumulates printable chars when searchable
|
|
295
309
|
// is on; the visible item slice is recomputed on every keystroke. We
|
|
@@ -382,7 +396,10 @@ export async function _arrowMenu({ title, subtitle, footer, items, defaultIdx =
|
|
|
382
396
|
cleanup();
|
|
383
397
|
resolve(view[idx]);
|
|
384
398
|
}
|
|
385
|
-
|
|
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'); }
|
|
386
403
|
else if (key.ctrl && key.name === 'u') { if (searchable) { query = ''; recompute(); } }
|
|
387
404
|
else if (key.name === 'escape') {
|
|
388
405
|
if (searchable && query) { query = ''; recompute(); return; }
|
|
@@ -404,12 +421,50 @@ export async function _arrowMenu({ title, subtitle, footer, items, defaultIdx =
|
|
|
404
421
|
const cleanup = () => {
|
|
405
422
|
process.stdin.off('keypress', onKey);
|
|
406
423
|
if (process.stdin.setRawMode) process.stdin.setRawMode(false);
|
|
407
|
-
|
|
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();
|
|
408
436
|
};
|
|
409
437
|
process.stdin.on('keypress', onKey);
|
|
410
438
|
});
|
|
411
439
|
}
|
|
412
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
|
+
|
|
413
468
|
// Bucket every registered provider into one of three auth-method
|
|
414
469
|
// families. The picker's first step asks the user which family
|
|
415
470
|
// they want before drilling into specific providers — much less
|
|
@@ -421,10 +476,10 @@ export function _providerFamilies() {
|
|
|
421
476
|
// tui/provider_families.mjs; the ANSI tags below are readline-specific.
|
|
422
477
|
const b = _bucketProviders(getRegistry());
|
|
423
478
|
return {
|
|
424
|
-
api: { label: 'API key', desc: 'paste an sk-... key during setup', tag: '
|
|
425
|
-
cli: { label: 'CLI / Local', desc: 'keyless — uses an existing CLI login or a local daemon', tag: '
|
|
426
|
-
meta: { label: 'Multi-agent', desc: 'orchestrator — fan a task out to a planner + workers (advanced)', tag: '
|
|
427
|
-
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 },
|
|
428
483
|
};
|
|
429
484
|
}
|
|
430
485
|
|
|
@@ -447,14 +502,7 @@ export async function _pickProviderInteractive() {
|
|
|
447
502
|
// Non-TTY fallback — single-prompt picker, identical to before.
|
|
448
503
|
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
449
504
|
process.stdout.write(`provider [${providers.join('|')}]: `);
|
|
450
|
-
const ans = await
|
|
451
|
-
let buf = '';
|
|
452
|
-
const onData = (chunk) => {
|
|
453
|
-
buf += chunk.toString();
|
|
454
|
-
if (buf.includes('\n')) { process.stdin.off('data', onData); resolve(buf.trim()); }
|
|
455
|
-
};
|
|
456
|
-
process.stdin.on('data', onData);
|
|
457
|
-
});
|
|
505
|
+
const ans = await _readOneLine(process.stdin);
|
|
458
506
|
// v5.3.2 — non-TTY fallback used to be `providers[0]`, which was
|
|
459
507
|
// whatever happened to be first in the registry (currently
|
|
460
508
|
// anthropic). Pin to claude-cli to match the interactive onboard
|
|
@@ -511,8 +559,8 @@ export async function _pickProviderInteractive() {
|
|
|
511
559
|
label: name,
|
|
512
560
|
desc,
|
|
513
561
|
tag: isCustom
|
|
514
|
-
? '
|
|
515
|
-
: (meta.requiresApiKey ? '
|
|
562
|
+
? paint('38;5;213', '[custom]')
|
|
563
|
+
: (meta.requiresApiKey ? paint('38;5;245', '[api key]') : paint('38;5;208', '[no key]')),
|
|
516
564
|
};
|
|
517
565
|
});
|
|
518
566
|
// Surface a "+ Add a new custom endpoint…" entry inside the API-key
|
|
@@ -524,7 +572,7 @@ export async function _pickProviderInteractive() {
|
|
|
524
572
|
id: '__add_custom__',
|
|
525
573
|
label: '+ Add a custom OpenAI-compatible endpoint…',
|
|
526
574
|
desc: 'NVIDIA NIM · OpenRouter · Together · Groq · vLLM · LM Studio · …',
|
|
527
|
-
tag: '
|
|
575
|
+
tag: paint('38;5;213', '[new]'),
|
|
528
576
|
});
|
|
529
577
|
}
|
|
530
578
|
if (memberNames.length === 1 && family.id !== 'api') {
|
|
@@ -578,10 +626,10 @@ export async function _pickProviderInteractive() {
|
|
|
578
626
|
// workers: multi-select with a running list + add/remove/done loop.
|
|
579
627
|
// maxSubtasks: typed integer, default 5.
|
|
580
628
|
export async function _setupOrchestratorInteractive() {
|
|
581
|
-
const accent = (s) =>
|
|
582
|
-
const dim = (s) =>
|
|
583
|
-
const bold = (s) =>
|
|
584
|
-
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);
|
|
585
633
|
const info = getRegistry().PROVIDER_INFO || {};
|
|
586
634
|
const eligibleNames = Object.keys(getRegistry().PROVIDERS).filter((n) => n !== 'orchestrator' && n !== 'mock');
|
|
587
635
|
if (eligibleNames.length === 0) {
|
|
@@ -736,25 +784,40 @@ export async function _pickModelInteractive(providerId, opts = {}) {
|
|
|
736
784
|
let dynamicModels = [];
|
|
737
785
|
while (true) {
|
|
738
786
|
const allModels = Array.from(new Set([...baseModels, ...dynamicModels]));
|
|
739
|
-
|
|
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
|
+
}];
|
|
740
798
|
if (supportsLiveFetch) {
|
|
741
|
-
modelItems.
|
|
799
|
+
modelItems.push({
|
|
742
800
|
id: '__fetch_models__',
|
|
743
801
|
label: '↻ Fetch live model list from /v1/models',
|
|
744
802
|
desc: isCustom || isBuiltinCompat ? `GET ${meta.baseUrl}/models` : 'pulls the up-to-date catalogue from the provider',
|
|
745
|
-
tag: '
|
|
803
|
+
tag: paint('38;5;245', '[live]'),
|
|
746
804
|
});
|
|
747
805
|
}
|
|
806
|
+
for (const m of allModels) modelItems.push({ id: m, label: m, desc: '' });
|
|
748
807
|
modelItems.push({
|
|
749
808
|
id: '__custom_model__',
|
|
750
809
|
label: '… type a custom model id',
|
|
751
810
|
desc: 'use any model id supported by this provider, even if not listed above',
|
|
752
|
-
tag: '
|
|
811
|
+
tag: paint('38;5;245', '[free]'),
|
|
753
812
|
});
|
|
754
813
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
+
}
|
|
758
821
|
const titlePrefix = opts.titlePrefix ? `${opts.titlePrefix} ` : '';
|
|
759
822
|
const picked = await _arrowMenu({
|
|
760
823
|
title: `${titlePrefix}pick a model for ${providerId}`,
|
|
@@ -765,6 +828,8 @@ export async function _pickModelInteractive(providerId, opts = {}) {
|
|
|
765
828
|
});
|
|
766
829
|
if (picked === 'CANCEL') return 'CANCEL';
|
|
767
830
|
if (picked === 'BACK') return 'BACK';
|
|
831
|
+
// Empty string = "no explicit model" (use the provider/CLI default).
|
|
832
|
+
if (picked.id === '__provider_default__') return '';
|
|
768
833
|
if (picked.id === '__custom_model__') {
|
|
769
834
|
const typed = (await _quickPrompt(` model id for ${providerId}: `)).trim();
|
|
770
835
|
if (!typed) continue;
|
|
@@ -775,7 +840,7 @@ export async function _pickModelInteractive(providerId, opts = {}) {
|
|
|
775
840
|
process.stdout.write(`\n fetching ${providerId} model list…\n`);
|
|
776
841
|
const fetched = await _fetchModelsForProvider(providerId);
|
|
777
842
|
if (!fetched.length) {
|
|
778
|
-
process.stdout.write(` ${'
|
|
843
|
+
process.stdout.write(` ${paint(33, 'no models returned')} — falling back to the suggested list.\n`);
|
|
779
844
|
await _quickPrompt(' press Enter to continue ');
|
|
780
845
|
} else {
|
|
781
846
|
dynamicModels = fetched;
|
|
@@ -783,7 +848,7 @@ export async function _pickModelInteractive(providerId, opts = {}) {
|
|
|
783
848
|
await _quickPrompt(' press Enter to pick one ');
|
|
784
849
|
}
|
|
785
850
|
} catch (e) {
|
|
786
|
-
process.stdout.write(`\n ${'
|
|
851
|
+
process.stdout.write(`\n ${paint(33, 'fetch failed:')} ${e?.message || e}\n`);
|
|
787
852
|
await _quickPrompt(' press Enter to continue ');
|
|
788
853
|
}
|
|
789
854
|
continue;
|
|
@@ -820,10 +885,10 @@ export async function _fetchModelsForProvider(providerId) {
|
|
|
820
885
|
// Persists into cfg.customProviders[] and returns { name } on success,
|
|
821
886
|
// or null when the user backs out.
|
|
822
887
|
export async function _addCustomProviderInteractive() {
|
|
823
|
-
const accent = (s) =>
|
|
824
|
-
const dim = (s) =>
|
|
825
|
-
const bold = (s) =>
|
|
826
|
-
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);
|
|
827
892
|
|
|
828
893
|
process.stdout.write('\x1b[2J\x1b[H');
|
|
829
894
|
process.stdout.write(accent('Add a custom OpenAI-compatible endpoint') + '\n');
|
|
@@ -845,7 +910,7 @@ export async function _addCustomProviderInteractive() {
|
|
|
845
910
|
}
|
|
846
911
|
try { name = validateCustomProviderName(raw); }
|
|
847
912
|
catch (e) {
|
|
848
|
-
process.stdout.write(`
|
|
913
|
+
process.stdout.write(` ${paint(33, e.message)} — try again.\n`);
|
|
849
914
|
continue;
|
|
850
915
|
}
|
|
851
916
|
// OpenAI-compat builtins (nim / openrouter / groq / …) can be overridden
|
|
@@ -855,9 +920,9 @@ export async function _addCustomProviderInteractive() {
|
|
|
855
920
|
// the override so it isn't a silent surprise.
|
|
856
921
|
if (typeof isBuiltinOpenAICompatName === 'function' && isBuiltinOpenAICompatName(name)) {
|
|
857
922
|
process.stdout.write(
|
|
858
|
-
`
|
|
923
|
+
paint(2, `Note: "${name}" is a built-in OpenAI-compatible provider; ` +
|
|
859
924
|
`your custom entry will override the built-in baseUrl/api-key for this install. ` +
|
|
860
|
-
`Remove with: lazyclaw providers remove ${name}\
|
|
925
|
+
`Remove with: lazyclaw providers remove ${name}`) + '\n'
|
|
861
926
|
);
|
|
862
927
|
}
|
|
863
928
|
break;
|
|
@@ -865,7 +930,7 @@ export async function _addCustomProviderInteractive() {
|
|
|
865
930
|
const baseUrlRaw = (await _quickPrompt(` ${bold('baseUrl')} ${dim('(must end in /v1, no trailing slash needed):')} `)).trim();
|
|
866
931
|
if (!baseUrlRaw) { process.stdout.write(dim(' cancelled — baseUrl is required.\n')); return null; }
|
|
867
932
|
if (!/^https?:\/\//i.test(baseUrlRaw)) {
|
|
868
|
-
process.stdout.write(
|
|
933
|
+
process.stdout.write(` ${paint(33, 'baseUrl must start with http:// or https://')} — cancelled.\n`);
|
|
869
934
|
return null;
|
|
870
935
|
}
|
|
871
936
|
const apiKey = (await _quickPrompt(` ${bold('api-key')} ${dim('(blank if the endpoint is auth-less, e.g. local vLLM):')} `)).trim();
|
|
@@ -889,7 +954,7 @@ export async function _addCustomProviderInteractive() {
|
|
|
889
954
|
} else if (result.probe.ok) {
|
|
890
955
|
probeMsg = ` ${ok('✓')} registered — /v1/models returned no entries (will rely on free-text model id).\n`;
|
|
891
956
|
} else {
|
|
892
|
-
probeMsg = `
|
|
957
|
+
probeMsg = ` ${paint(33, '!')} registered, but /v1/models probe failed: ${result.probe.error}\n`;
|
|
893
958
|
}
|
|
894
959
|
process.stdout.write('\n');
|
|
895
960
|
process.stdout.write(` ${ok(bold('✓ custom provider saved:'))} ${entry.name} ${dim('→')} ${entry.baseUrl}\n`);
|
|
@@ -898,7 +963,15 @@ export async function _addCustomProviderInteractive() {
|
|
|
898
963
|
await _quickPrompt(' press Enter to continue ');
|
|
899
964
|
return { name };
|
|
900
965
|
}
|
|
901
|
-
|
|
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
|
+
}
|
|
902
975
|
const readline = await import('node:readline');
|
|
903
976
|
process.stdout.write('\n');
|
|
904
977
|
// Make sure stdin is in cooked / line-buffered mode for the
|
|
@@ -915,3 +988,45 @@ export async function _quickPrompt(label) {
|
|
|
915
988
|
rl.close();
|
|
916
989
|
return ans.trim();
|
|
917
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
|
+
}
|