lazyclaw 6.3.1 → 6.5.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 +7 -193
- package/commands/agents_registry.mjs +205 -0
- package/commands/auth_nodes.mjs +19 -1
- package/commands/automation.mjs +28 -94
- package/commands/automation_loops.mjs +94 -0
- package/commands/channels.mjs +46 -3
- package/commands/chat.mjs +127 -704
- package/commands/chat_hardening.mjs +23 -0
- package/commands/chat_legacy_slash.mjs +716 -0
- package/commands/config.mjs +36 -2
- package/commands/daemon.mjs +99 -1
- package/commands/gateway.mjs +123 -4
- package/commands/help_text.mjs +78 -0
- package/commands/mcp.mjs +150 -0
- package/commands/misc.mjs +20 -0
- package/commands/sessions.mjs +68 -8
- package/commands/setup.mjs +44 -80
- 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 +41 -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 +27 -3
- 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 +68 -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 +183 -0
- package/providers/cli_error.mjs +38 -0
- package/providers/cli_login.mjs +179 -0
- package/providers/codex_cli.mjs +70 -12
- package/providers/gemini.mjs +104 -3
- package/providers/gemini_cli.mjs +78 -17
- 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 +29 -2
- package/providers/tool_use/openai.mjs +35 -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/banner.mjs +72 -0
- package/tui/chat_mode_slash.mjs +59 -0
- package/tui/config_picker.mjs +1 -0
- package/tui/editor.mjs +0 -0
- package/tui/editor_anchor.mjs +46 -0
- 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/orchestrator_setup.mjs +135 -0
- package/tui/pickers.mjs +218 -248
- package/tui/repl.mjs +118 -209
- package/tui/repl_altbuffer.mjs +63 -0
- package/tui/repl_reducers.mjs +114 -0
- package/tui/repl_reset.mjs +37 -0
- package/tui/run_turn.mjs +228 -26
- package/tui/slash_args.mjs +159 -0
- package/tui/slash_channels.mjs +208 -0
- package/tui/slash_commands.mjs +7 -5
- package/tui/slash_dashboard.mjs +220 -0
- package/tui/slash_dispatcher.mjs +339 -774
- package/tui/slash_helpers.mjs +68 -0
- package/tui/slash_popup.mjs +5 -1
- package/tui/slash_trainer.mjs +173 -0
- 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,58 @@ 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
|
+
// Banner renderers live in a leaf sibling module (tui/banner.mjs). We import
|
|
16
|
+
// the ones pickers' own code still calls (_orange / _ORANGE_RGB in
|
|
17
|
+
// _printChatBanner, _loadBannerAssets / _renderBanner / _orange in
|
|
18
|
+
// _renderV5Banner) and re-export the public surface so existing importers
|
|
19
|
+
// (cli.mjs / commands/setup.mjs / commands/chat.mjs) keep resolving them here.
|
|
20
|
+
import {
|
|
21
|
+
_ORANGE_RGB,
|
|
22
|
+
_orange,
|
|
23
|
+
_renderBanner,
|
|
24
|
+
_loadBannerAssets,
|
|
25
|
+
} from './banner.mjs';
|
|
26
|
+
export {
|
|
27
|
+
_orange,
|
|
28
|
+
_renderMascot,
|
|
29
|
+
_renderMascotTiny,
|
|
30
|
+
_renderBanner,
|
|
31
|
+
_loadBannerAssets,
|
|
32
|
+
} from './banner.mjs';
|
|
33
|
+
|
|
34
|
+
// Read exactly ONE line from a non-TTY stream (piped stdin) and hand the rest
|
|
35
|
+
// back so the next reader (the next picker / a credential prompt) still gets
|
|
36
|
+
// its input. The old fallbacks resolved the whole buffered payload at the
|
|
37
|
+
// first newline — matching no id (skip) and swallowing every later line so the
|
|
38
|
+
// following prompt hung. Pause before unshift so the buffered remainder isn't
|
|
39
|
+
// dropped while no 'data' listener is attached.
|
|
40
|
+
export function _readOneLine(stream) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
let buf = '';
|
|
43
|
+
const done = (line) => {
|
|
44
|
+
stream.off('data', onData);
|
|
45
|
+
stream.off('end', onEnd);
|
|
46
|
+
resolve(line.replace(/\r$/, '').trim());
|
|
47
|
+
};
|
|
48
|
+
const onData = (chunk) => {
|
|
49
|
+
buf += chunk.toString();
|
|
50
|
+
const nl = buf.indexOf('\n');
|
|
51
|
+
if (nl === -1) return;
|
|
52
|
+
stream.pause();
|
|
53
|
+
const rest = buf.slice(nl + 1);
|
|
54
|
+
if (rest) stream.unshift(Buffer.from(rest, 'utf8'));
|
|
55
|
+
done(buf.slice(0, nl));
|
|
56
|
+
};
|
|
57
|
+
const onEnd = () => done(buf);
|
|
58
|
+
stream.on('data', onData);
|
|
59
|
+
stream.on('end', onEnd);
|
|
60
|
+
// A prior _readOneLine pauses the stream after unshifting its remainder.
|
|
61
|
+
// Adding a 'data' listener does NOT auto-resume an explicitly-paused
|
|
62
|
+
// stream, so resume() here or the next read would block on buffered input.
|
|
63
|
+
stream.resume();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
14
66
|
|
|
15
67
|
export function _attachGhostAutocomplete(rl) {
|
|
16
68
|
// Returns `{ dispose, suspend, resume }`. Dispose detaches the keypress +
|
|
@@ -107,94 +159,16 @@ export function _attachGhostAutocomplete(rl) {
|
|
|
107
159
|
};
|
|
108
160
|
}
|
|
109
161
|
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
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.
|
|
137
|
-
//
|
|
138
|
-
// _renderMascot / _renderMascotTiny are kept as stubs so any leftover
|
|
139
|
-
// caller doesn't crash; no state-coloured art is produced any more.
|
|
140
|
-
|
|
141
|
-
const _ORANGE_RGB = '241;130;70'; // #F18246
|
|
142
|
-
export function _orange(s) { return `\x1b[38;2;${_ORANGE_RGB}m${s}\x1b[0m`; }
|
|
143
|
-
|
|
144
|
-
export function _renderMascot() {
|
|
145
|
-
return ['lazyclaw'];
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export function _renderMascotTiny() {
|
|
149
|
-
return 'lazyclaw';
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// figlet "standard" "lazy", trimmed of leading blank line. Each row
|
|
153
|
-
// is left-padded by two spaces inside the box, and every row is then
|
|
154
|
-
// padded to INNER_W cells.
|
|
155
|
-
const _LAZY_STANDARD = [
|
|
156
|
-
' _ ',
|
|
157
|
-
'| | __ _ _____ _ ',
|
|
158
|
-
'| |/ _` |_ / | | | ',
|
|
159
|
-
'| | (_| |/ /| |_| | ',
|
|
160
|
-
'|_|\\__,_/___|\\__, | ',
|
|
161
|
-
' |___/ ',
|
|
162
|
-
];
|
|
163
|
-
|
|
164
|
-
const _INNER_W = 32; // 2 left pad + 20 letter art + caption headroom
|
|
165
|
-
|
|
166
|
-
export function _renderBanner(version) {
|
|
167
|
-
const v = String(version || '?.?.?');
|
|
168
|
-
const cap = ` LazyClaw v${v}`;
|
|
169
|
-
const padInner = (s) => ' ' + s.padEnd(_INNER_W - 2, ' ');
|
|
170
|
-
const wrap = (inner) => _orange('│') + _orange(inner) + _orange('│');
|
|
171
|
-
const top = _orange('╭' + '─'.repeat(_INNER_W) + '╮');
|
|
172
|
-
const bot = _orange('╰' + '─'.repeat(_INNER_W) + '╯');
|
|
173
|
-
return [
|
|
174
|
-
top,
|
|
175
|
-
..._LAZY_STANDARD.map((row) => wrap(padInner(row))),
|
|
176
|
-
wrap(padInner(cap)),
|
|
177
|
-
bot,
|
|
178
|
-
];
|
|
179
|
-
}
|
|
162
|
+
// Banner renderers (_ORANGE_RGB / _orange / _renderMascot / _renderMascotTiny
|
|
163
|
+
// / _renderBanner / _loadBannerAssets) moved to tui/banner.mjs — imported at
|
|
164
|
+
// the top of this file and re-exported there for existing callers. The v5
|
|
165
|
+
// splash composer and chat header below still live here because they read
|
|
166
|
+
// pickers-local state and stay close to the wizard that prints them.
|
|
180
167
|
|
|
181
168
|
// v5 hero banner — ANSI Shadow LAZYCLAW wordmark stacked on top of the
|
|
182
169
|
// braille sloth (tui/banner.generated.mjs + tui/wordmark.mjs). Left-aligned
|
|
183
170
|
// with a 2-cell margin so wide terminals don't push the art to the right.
|
|
184
171
|
// Opt out with LAZYCLAW_LEGACY_MENU=1 to fall back to the v4 figlet box.
|
|
185
|
-
let _bannerAssetsCache = null;
|
|
186
|
-
export async function _loadBannerAssets() {
|
|
187
|
-
if (_bannerAssetsCache !== null) return _bannerAssetsCache;
|
|
188
|
-
try {
|
|
189
|
-
const { banner } = await import('./banner.generated.mjs');
|
|
190
|
-
const { wordmark } = await import('./wordmark.mjs');
|
|
191
|
-
_bannerAssetsCache = { banner, wordmark };
|
|
192
|
-
} catch {
|
|
193
|
-
_bannerAssetsCache = null;
|
|
194
|
-
}
|
|
195
|
-
return _bannerAssetsCache;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
172
|
export async function _renderV5Banner(version) {
|
|
199
173
|
const a = await _loadBannerAssets();
|
|
200
174
|
if (!a) return _renderBanner(version); // missing tarball asset → v4 figlet
|
|
@@ -261,14 +235,7 @@ export async function _arrowMenu({ title, subtitle, footer, items, defaultIdx =
|
|
|
261
235
|
process.stderr.write(`${title}\n`);
|
|
262
236
|
items.forEach((it, i) => process.stderr.write(` ${i + 1}. ${it.label}${it.desc ? ' — ' + it.desc : ''}\n`));
|
|
263
237
|
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
|
-
});
|
|
238
|
+
const ans = await _readOneLine(process.stdin);
|
|
272
239
|
if (!ans) return items[0];
|
|
273
240
|
const byNum = parseInt(ans, 10);
|
|
274
241
|
if (Number.isFinite(byNum) && byNum >= 1 && byNum <= items.length) return items[byNum - 1];
|
|
@@ -286,9 +253,18 @@ export async function _arrowMenu({ title, subtitle, footer, items, defaultIdx =
|
|
|
286
253
|
// before drawing so the picker always receives the first keypress.
|
|
287
254
|
process.stdin.resume();
|
|
288
255
|
if (process.stdin.ref) process.stdin.ref();
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
256
|
+
// Render on the ALTERNATE screen buffer (the same trick vim / less / fzf
|
|
257
|
+
// use). Without it, the menu's full-screen \x1b[2J clears land on the main
|
|
258
|
+
// buffer, interleave with the readline output from prior wizard steps, and
|
|
259
|
+
// every step pushes a screenful into scrollback — the "화면이 밀린다" bug.
|
|
260
|
+
// On the alt buffer the menu draws in isolation; leaving it (cleanup) restores
|
|
261
|
+
// the main buffer (and the wizard text on it) verbatim, with nothing pushed.
|
|
262
|
+
const altScreen = !!(process.stdout.isTTY);
|
|
263
|
+
if (altScreen) process.stdout.write('\x1b[?1049h');
|
|
264
|
+
// Menu chrome via the theme gate: plain text under NO_COLOR / dumb / non-TTY.
|
|
265
|
+
const accent = (s) => paint('38;5;208', s);
|
|
266
|
+
const dim = (s) => paint('2', s);
|
|
267
|
+
const bold = (s) => paint('1', s);
|
|
292
268
|
|
|
293
269
|
// Typeahead state. `query` accumulates printable chars when searchable
|
|
294
270
|
// is on; the visible item slice is recomputed on every keystroke. We
|
|
@@ -381,7 +357,10 @@ export async function _arrowMenu({ title, subtitle, footer, items, defaultIdx =
|
|
|
381
357
|
cleanup();
|
|
382
358
|
resolve(view[idx]);
|
|
383
359
|
}
|
|
384
|
-
|
|
360
|
+
// Ctrl+C mid-wizard cancels this step (resolve the CANCEL sentinel the
|
|
361
|
+
// callers already handle) instead of hard-killing the whole process —
|
|
362
|
+
// a wizard step must be abortable without taking the app down.
|
|
363
|
+
else if (key.ctrl && key.name === 'c') { cleanup(); resolve('CANCEL'); }
|
|
385
364
|
else if (key.ctrl && key.name === 'u') { if (searchable) { query = ''; recompute(); } }
|
|
386
365
|
else if (key.name === 'escape') {
|
|
387
366
|
if (searchable && query) { query = ''; recompute(); return; }
|
|
@@ -403,12 +382,50 @@ export async function _arrowMenu({ title, subtitle, footer, items, defaultIdx =
|
|
|
403
382
|
const cleanup = () => {
|
|
404
383
|
process.stdin.off('keypress', onKey);
|
|
405
384
|
if (process.stdin.setRawMode) process.stdin.setRawMode(false);
|
|
406
|
-
|
|
385
|
+
// Show the cursor, then leave the alt screen → the main buffer (with the
|
|
386
|
+
// wizard's prior output) reappears exactly as it was, nothing pushed into
|
|
387
|
+
// scrollback. Fall back to a clear+home when the alt buffer wasn't used.
|
|
388
|
+
if (altScreen) process.stdout.write('\x1b[?25h\x1b[?1049l');
|
|
389
|
+
else process.stdout.write('\x1b[?25h\x1b[2J\x1b[H');
|
|
390
|
+
// Release stdin so a one-shot CLI caller (the setup / onboard wizard) can
|
|
391
|
+
// exit. We resume()+ref() stdin on entry to receive keypresses; if it's
|
|
392
|
+
// never unref'd, the event loop stays alive after the LAST picker and the
|
|
393
|
+
// process hangs at "Setup complete" instead of returning to the shell.
|
|
394
|
+
// The chat REPL re-refs stdin (via _pauseChatForSubMenu's finally) and
|
|
395
|
+
// its own readline keeps the loop alive, so this is safe there.
|
|
396
|
+
if (process.stdin.unref) process.stdin.unref();
|
|
407
397
|
};
|
|
408
398
|
process.stdin.on('keypress', onKey);
|
|
409
399
|
});
|
|
410
400
|
}
|
|
411
401
|
|
|
402
|
+
// Arrow-key yes/no — replaces the typed `[Y/n]` prompts in the wizard so the
|
|
403
|
+
// user never types a letter. Returns a boolean. Esc / q resolve to the default.
|
|
404
|
+
// Inherits _arrowMenu's non-TTY fallback (reads a line). `pick` is injectable
|
|
405
|
+
// for tests.
|
|
406
|
+
export async function _pickYesNo(title, { subtitle, yesLabel = 'Yes', noLabel = 'No', defaultYes = true, pick = _arrowMenu } = {}) {
|
|
407
|
+
const picked = await pick({
|
|
408
|
+
title,
|
|
409
|
+
subtitle,
|
|
410
|
+
items: [
|
|
411
|
+
{ id: 'yes', label: yesLabel },
|
|
412
|
+
{ id: 'no', label: noLabel },
|
|
413
|
+
],
|
|
414
|
+
defaultIdx: defaultYes ? 0 : 1,
|
|
415
|
+
});
|
|
416
|
+
if (picked === 'BACK' || picked === 'CANCEL' || picked == null) return defaultYes;
|
|
417
|
+
const id = typeof picked === 'object' ? picked.id : picked;
|
|
418
|
+
return id === 'yes';
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Arrow-key single choice — `options` is [{ id, label, desc }]. Returns the
|
|
422
|
+
// chosen id, or `fallback` on Esc/cancel. `pick` injectable for tests.
|
|
423
|
+
export async function _pickChoice(title, options, { subtitle, defaultIdx = 0, fallback = null, pick = _arrowMenu } = {}) {
|
|
424
|
+
const picked = await pick({ title, subtitle, items: options, defaultIdx, searchable: false });
|
|
425
|
+
if (picked === 'BACK' || picked === 'CANCEL' || picked == null) return fallback;
|
|
426
|
+
return typeof picked === 'object' ? picked.id : picked;
|
|
427
|
+
}
|
|
428
|
+
|
|
412
429
|
// Bucket every registered provider into one of three auth-method
|
|
413
430
|
// families. The picker's first step asks the user which family
|
|
414
431
|
// they want before drilling into specific providers — much less
|
|
@@ -420,10 +437,10 @@ export function _providerFamilies() {
|
|
|
420
437
|
// tui/provider_families.mjs; the ANSI tags below are readline-specific.
|
|
421
438
|
const b = _bucketProviders(getRegistry());
|
|
422
439
|
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: '
|
|
440
|
+
api: { label: 'API key', desc: 'paste an sk-... key during setup', tag: paint('38;5;245', '[needs key]'), members: b.api },
|
|
441
|
+
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 },
|
|
442
|
+
meta: { label: 'Multi-agent', desc: 'orchestrator — fan a task out to a planner + workers (advanced)', tag: paint('38;5;245', '[meta]'), members: b.meta },
|
|
443
|
+
mock: { label: 'Mock', desc: 'offline echo, only useful for testing', tag: paint('38;5;245', '[test]'), members: b.mock },
|
|
427
444
|
};
|
|
428
445
|
}
|
|
429
446
|
|
|
@@ -446,14 +463,7 @@ export async function _pickProviderInteractive() {
|
|
|
446
463
|
// Non-TTY fallback — single-prompt picker, identical to before.
|
|
447
464
|
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
448
465
|
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
|
-
});
|
|
466
|
+
const ans = await _readOneLine(process.stdin);
|
|
457
467
|
// v5.3.2 — non-TTY fallback used to be `providers[0]`, which was
|
|
458
468
|
// whatever happened to be first in the registry (currently
|
|
459
469
|
// anthropic). Pin to claude-cli to match the interactive onboard
|
|
@@ -510,8 +520,8 @@ export async function _pickProviderInteractive() {
|
|
|
510
520
|
label: name,
|
|
511
521
|
desc,
|
|
512
522
|
tag: isCustom
|
|
513
|
-
? '
|
|
514
|
-
: (meta.requiresApiKey ? '
|
|
523
|
+
? paint('38;5;213', '[custom]')
|
|
524
|
+
: (meta.requiresApiKey ? paint('38;5;245', '[api key]') : paint('38;5;208', '[no key]')),
|
|
515
525
|
};
|
|
516
526
|
});
|
|
517
527
|
// Surface a "+ Add a new custom endpoint…" entry inside the API-key
|
|
@@ -523,7 +533,7 @@ export async function _pickProviderInteractive() {
|
|
|
523
533
|
id: '__add_custom__',
|
|
524
534
|
label: '+ Add a custom OpenAI-compatible endpoint…',
|
|
525
535
|
desc: 'NVIDIA NIM · OpenRouter · Together · Groq · vLLM · LM Studio · …',
|
|
526
|
-
tag: '
|
|
536
|
+
tag: paint('38;5;213', '[new]'),
|
|
527
537
|
});
|
|
528
538
|
}
|
|
529
539
|
if (memberNames.length === 1 && family.id !== 'api') {
|
|
@@ -569,124 +579,10 @@ export async function _pickProviderInteractive() {
|
|
|
569
579
|
return { provider: provider.id, model: picked };
|
|
570
580
|
}
|
|
571
581
|
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
//
|
|
575
|
-
//
|
|
576
|
-
// planner: single picker over registered non-composite providers.
|
|
577
|
-
// workers: multi-select with a running list + add/remove/done loop.
|
|
578
|
-
// maxSubtasks: typed integer, default 5.
|
|
579
|
-
export async function _setupOrchestratorInteractive() {
|
|
580
|
-
const accent = (s) => `\x1b[38;5;208m${s}\x1b[0m`;
|
|
581
|
-
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
582
|
-
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
583
|
-
const ok = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
584
|
-
const info = getRegistry().PROVIDER_INFO || {};
|
|
585
|
-
const eligibleNames = Object.keys(getRegistry().PROVIDERS).filter((n) => n !== 'orchestrator' && n !== 'mock');
|
|
586
|
-
if (eligibleNames.length === 0) {
|
|
587
|
-
process.stdout.write('\n' + accent('orchestrator setup') + ': no eligible workers — register a real provider first.\n');
|
|
588
|
-
await _quickPrompt(' press Enter to continue ');
|
|
589
|
-
return 'CANCEL';
|
|
590
|
-
}
|
|
591
|
-
const cfg = readConfig();
|
|
592
|
-
const existing = cfg.orchestrator && typeof cfg.orchestrator === 'object' ? cfg.orchestrator : {};
|
|
593
|
-
|
|
594
|
-
// ── Pick planner ─────────────────────────────────────────────────
|
|
595
|
-
const plannerItems = eligibleNames.map((name) => {
|
|
596
|
-
const m = info[name] || {};
|
|
597
|
-
const defaultModel = m.defaultModel || '';
|
|
598
|
-
return {
|
|
599
|
-
id: `${name}${defaultModel ? ':' + defaultModel : ''}`,
|
|
600
|
-
label: m.label && m.label !== name ? `${name} — ${m.label}` : name,
|
|
601
|
-
desc: defaultModel ? `default model: ${defaultModel}` : '',
|
|
602
|
-
};
|
|
603
|
-
});
|
|
604
|
-
const plannerPick = await _arrowMenu({
|
|
605
|
-
title: 'LazyClaw setup — Step 3 of 3: orchestrator — pick the planner',
|
|
606
|
-
subtitle: 'The planner decomposes the user request into subtasks and writes the final synthesis. Strong reasoning models work best here.',
|
|
607
|
-
items: plannerItems,
|
|
608
|
-
searchable: true,
|
|
609
|
-
defaultIdx: Math.max(0, plannerItems.findIndex((p) => p.id === existing.planner)),
|
|
610
|
-
});
|
|
611
|
-
if (plannerPick === 'CANCEL') return 'CANCEL';
|
|
612
|
-
if (plannerPick === 'BACK') return 'BACK';
|
|
613
|
-
const planner = plannerPick.id;
|
|
614
|
-
|
|
615
|
-
// ── Pick workers (iterative add/remove) ──────────────────────────
|
|
616
|
-
const workers = Array.isArray(existing.workers) ? existing.workers.slice() : [];
|
|
617
|
-
while (true) {
|
|
618
|
-
process.stdout.write('\x1b[2J\x1b[H');
|
|
619
|
-
process.stdout.write(accent('Orchestrator workers') + '\n');
|
|
620
|
-
process.stdout.write(dim('Subtasks are dispatched round-robin across this list.') + '\n\n');
|
|
621
|
-
if (workers.length === 0) {
|
|
622
|
-
process.stdout.write(' ' + dim('(none yet — add at least one)') + '\n\n');
|
|
623
|
-
} else {
|
|
624
|
-
workers.forEach((w, i) => {
|
|
625
|
-
process.stdout.write(` ${i + 1}. ${ok(w)}\n`);
|
|
626
|
-
});
|
|
627
|
-
process.stdout.write('\n');
|
|
628
|
-
}
|
|
629
|
-
const items = [
|
|
630
|
-
{ id: '__add__', label: '+ Add a worker', desc: 'pick from registered providers' },
|
|
631
|
-
{ id: '__remove__', label: '- Remove a worker', desc: workers.length ? 'pick which entry to drop' : '(nothing to remove)' },
|
|
632
|
-
{ id: '__done__', label: `Done${workers.length ? ` (${workers.length} worker${workers.length === 1 ? '' : 's'})` : ' — at least one worker required'}`, desc: workers.length ? 'save cfg.orchestrator and finish' : 'add one worker first' },
|
|
633
|
-
];
|
|
634
|
-
const action = await _arrowMenu({
|
|
635
|
-
title: 'LazyClaw setup — orchestrator workers',
|
|
636
|
-
subtitle: `Planner: ${planner}`,
|
|
637
|
-
items,
|
|
638
|
-
});
|
|
639
|
-
if (action === 'CANCEL') return 'CANCEL';
|
|
640
|
-
if (action === 'BACK') return 'BACK';
|
|
641
|
-
if (action.id === '__add__') {
|
|
642
|
-
const wPick = await _arrowMenu({
|
|
643
|
-
title: 'Add worker',
|
|
644
|
-
subtitle: 'Picked entries are appended to the workers list.',
|
|
645
|
-
items: plannerItems.filter((p) => !workers.includes(p.id)),
|
|
646
|
-
searchable: true,
|
|
647
|
-
});
|
|
648
|
-
if (wPick === 'CANCEL' || wPick === 'BACK') continue;
|
|
649
|
-
workers.push(wPick.id);
|
|
650
|
-
continue;
|
|
651
|
-
}
|
|
652
|
-
if (action.id === '__remove__') {
|
|
653
|
-
if (!workers.length) continue;
|
|
654
|
-
const rPick = await _arrowMenu({
|
|
655
|
-
title: 'Remove worker',
|
|
656
|
-
subtitle: 'Highlighted entry is removed from the list.',
|
|
657
|
-
items: workers.map((w) => ({ id: w, label: w })),
|
|
658
|
-
});
|
|
659
|
-
if (rPick === 'CANCEL' || rPick === 'BACK') continue;
|
|
660
|
-
const idx = workers.indexOf(rPick.id);
|
|
661
|
-
if (idx >= 0) workers.splice(idx, 1);
|
|
662
|
-
continue;
|
|
663
|
-
}
|
|
664
|
-
if (action.id === '__done__') {
|
|
665
|
-
if (workers.length === 0) continue;
|
|
666
|
-
break;
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
// ── maxSubtasks ──────────────────────────────────────────────────
|
|
671
|
-
const defaultMax = Number.isFinite(existing.maxSubtasks) && existing.maxSubtasks > 0
|
|
672
|
-
? Math.min(10, existing.maxSubtasks)
|
|
673
|
-
: 5;
|
|
674
|
-
const rawMax = (await _quickPrompt(` ${bold('maxSubtasks')} ${dim(`(2..10, blank → ${defaultMax}):`)} `)).trim();
|
|
675
|
-
let maxSubtasks = defaultMax;
|
|
676
|
-
if (rawMax) {
|
|
677
|
-
const n = parseInt(rawMax, 10);
|
|
678
|
-
if (Number.isFinite(n) && n >= 1) maxSubtasks = Math.min(10, Math.max(1, n));
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// ── Persist ──────────────────────────────────────────────────────
|
|
682
|
-
cfg.orchestrator = { planner, workers, maxSubtasks };
|
|
683
|
-
writeConfig(cfg);
|
|
684
|
-
process.stdout.write('\n');
|
|
685
|
-
process.stdout.write(` ${ok('✓ orchestrator saved')} ${dim('→')} ` +
|
|
686
|
-
`planner ${ok(planner)} · ${workers.length} worker${workers.length === 1 ? '' : 's'} · maxSubtasks ${maxSubtasks}\n`);
|
|
687
|
-
await _quickPrompt(' press Enter to continue ');
|
|
688
|
-
return { ok: true };
|
|
689
|
-
}
|
|
582
|
+
// _setupOrchestratorInteractive moved to tui/orchestrator_setup.mjs (it imports
|
|
583
|
+
// _arrowMenu / _quickPrompt back from here at call time → a safe cycle). The
|
|
584
|
+
// re-export line sits at the BOTTOM of this file, after those helpers are
|
|
585
|
+
// declared, so the live bindings resolve.
|
|
690
586
|
|
|
691
587
|
// Pause the chat REPL's readline + ghost-autocomplete while a sub-picker
|
|
692
588
|
// (provider / model arrow menu) takes over the terminal. The sub-picker
|
|
@@ -735,25 +631,40 @@ export async function _pickModelInteractive(providerId, opts = {}) {
|
|
|
735
631
|
let dynamicModels = [];
|
|
736
632
|
while (true) {
|
|
737
633
|
const allModels = Array.from(new Set([...baseModels, ...dynamicModels]));
|
|
738
|
-
|
|
634
|
+
// "Use the provider's own default" — send no `-m`, the reliable path for
|
|
635
|
+
// keyless CLI providers (codex-cli / gemini-cli) whose allowed model set
|
|
636
|
+
// is fixed by the logged-in account: forcing a model the plan isn't
|
|
637
|
+
// entitled to makes the CLI reject the turn. Pre-selected when the
|
|
638
|
+
// provider declares no defaultModel.
|
|
639
|
+
const modelItems = [{
|
|
640
|
+
id: '__provider_default__',
|
|
641
|
+
label: "▷ Use the provider's own default model",
|
|
642
|
+
desc: 'no model override — the CLI/login picks (recommended for codex-cli / gemini-cli)',
|
|
643
|
+
tag: paint('38;5;208', '[default]'),
|
|
644
|
+
}];
|
|
739
645
|
if (supportsLiveFetch) {
|
|
740
|
-
modelItems.
|
|
646
|
+
modelItems.push({
|
|
741
647
|
id: '__fetch_models__',
|
|
742
648
|
label: '↻ Fetch live model list from /v1/models',
|
|
743
649
|
desc: isCustom || isBuiltinCompat ? `GET ${meta.baseUrl}/models` : 'pulls the up-to-date catalogue from the provider',
|
|
744
|
-
tag: '
|
|
650
|
+
tag: paint('38;5;245', '[live]'),
|
|
745
651
|
});
|
|
746
652
|
}
|
|
653
|
+
for (const m of allModels) modelItems.push({ id: m, label: m, desc: '' });
|
|
747
654
|
modelItems.push({
|
|
748
655
|
id: '__custom_model__',
|
|
749
656
|
label: '… type a custom model id',
|
|
750
657
|
desc: 'use any model id supported by this provider, even if not listed above',
|
|
751
|
-
tag: '
|
|
658
|
+
tag: paint('38;5;245', '[free]'),
|
|
752
659
|
});
|
|
753
660
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
661
|
+
// Land the cursor on the configured default model when there is one;
|
|
662
|
+
// otherwise pre-select "use the provider's own default" (index 0).
|
|
663
|
+
let defaultIdx = 0;
|
|
664
|
+
if (meta.defaultModel) {
|
|
665
|
+
const i = modelItems.findIndex((it) => it.id === meta.defaultModel);
|
|
666
|
+
if (i >= 0) defaultIdx = i;
|
|
667
|
+
}
|
|
757
668
|
const titlePrefix = opts.titlePrefix ? `${opts.titlePrefix} ` : '';
|
|
758
669
|
const picked = await _arrowMenu({
|
|
759
670
|
title: `${titlePrefix}pick a model for ${providerId}`,
|
|
@@ -764,6 +675,8 @@ export async function _pickModelInteractive(providerId, opts = {}) {
|
|
|
764
675
|
});
|
|
765
676
|
if (picked === 'CANCEL') return 'CANCEL';
|
|
766
677
|
if (picked === 'BACK') return 'BACK';
|
|
678
|
+
// Empty string = "no explicit model" (use the provider/CLI default).
|
|
679
|
+
if (picked.id === '__provider_default__') return '';
|
|
767
680
|
if (picked.id === '__custom_model__') {
|
|
768
681
|
const typed = (await _quickPrompt(` model id for ${providerId}: `)).trim();
|
|
769
682
|
if (!typed) continue;
|
|
@@ -774,7 +687,7 @@ export async function _pickModelInteractive(providerId, opts = {}) {
|
|
|
774
687
|
process.stdout.write(`\n fetching ${providerId} model list…\n`);
|
|
775
688
|
const fetched = await _fetchModelsForProvider(providerId);
|
|
776
689
|
if (!fetched.length) {
|
|
777
|
-
process.stdout.write(` ${'
|
|
690
|
+
process.stdout.write(` ${paint(33, 'no models returned')} — falling back to the suggested list.\n`);
|
|
778
691
|
await _quickPrompt(' press Enter to continue ');
|
|
779
692
|
} else {
|
|
780
693
|
dynamicModels = fetched;
|
|
@@ -782,7 +695,7 @@ export async function _pickModelInteractive(providerId, opts = {}) {
|
|
|
782
695
|
await _quickPrompt(' press Enter to pick one ');
|
|
783
696
|
}
|
|
784
697
|
} catch (e) {
|
|
785
|
-
process.stdout.write(`\n ${'
|
|
698
|
+
process.stdout.write(`\n ${paint(33, 'fetch failed:')} ${e?.message || e}\n`);
|
|
786
699
|
await _quickPrompt(' press Enter to continue ');
|
|
787
700
|
}
|
|
788
701
|
continue;
|
|
@@ -819,10 +732,10 @@ export async function _fetchModelsForProvider(providerId) {
|
|
|
819
732
|
// Persists into cfg.customProviders[] and returns { name } on success,
|
|
820
733
|
// or null when the user backs out.
|
|
821
734
|
export async function _addCustomProviderInteractive() {
|
|
822
|
-
const accent = (s) =>
|
|
823
|
-
const dim = (s) =>
|
|
824
|
-
const bold = (s) =>
|
|
825
|
-
const ok = (s) =>
|
|
735
|
+
const accent = (s) => paint('38;5;208', s);
|
|
736
|
+
const dim = (s) => paint('2', s);
|
|
737
|
+
const bold = (s) => paint('1', s);
|
|
738
|
+
const ok = (s) => paint('32', s);
|
|
826
739
|
|
|
827
740
|
process.stdout.write('\x1b[2J\x1b[H');
|
|
828
741
|
process.stdout.write(accent('Add a custom OpenAI-compatible endpoint') + '\n');
|
|
@@ -844,7 +757,7 @@ export async function _addCustomProviderInteractive() {
|
|
|
844
757
|
}
|
|
845
758
|
try { name = validateCustomProviderName(raw); }
|
|
846
759
|
catch (e) {
|
|
847
|
-
process.stdout.write(`
|
|
760
|
+
process.stdout.write(` ${paint(33, e.message)} — try again.\n`);
|
|
848
761
|
continue;
|
|
849
762
|
}
|
|
850
763
|
// OpenAI-compat builtins (nim / openrouter / groq / …) can be overridden
|
|
@@ -854,9 +767,9 @@ export async function _addCustomProviderInteractive() {
|
|
|
854
767
|
// the override so it isn't a silent surprise.
|
|
855
768
|
if (typeof isBuiltinOpenAICompatName === 'function' && isBuiltinOpenAICompatName(name)) {
|
|
856
769
|
process.stdout.write(
|
|
857
|
-
`
|
|
770
|
+
paint(2, `Note: "${name}" is a built-in OpenAI-compatible provider; ` +
|
|
858
771
|
`your custom entry will override the built-in baseUrl/api-key for this install. ` +
|
|
859
|
-
`Remove with: lazyclaw providers remove ${name}\
|
|
772
|
+
`Remove with: lazyclaw providers remove ${name}`) + '\n'
|
|
860
773
|
);
|
|
861
774
|
}
|
|
862
775
|
break;
|
|
@@ -864,7 +777,7 @@ export async function _addCustomProviderInteractive() {
|
|
|
864
777
|
const baseUrlRaw = (await _quickPrompt(` ${bold('baseUrl')} ${dim('(must end in /v1, no trailing slash needed):')} `)).trim();
|
|
865
778
|
if (!baseUrlRaw) { process.stdout.write(dim(' cancelled — baseUrl is required.\n')); return null; }
|
|
866
779
|
if (!/^https?:\/\//i.test(baseUrlRaw)) {
|
|
867
|
-
process.stdout.write(
|
|
780
|
+
process.stdout.write(` ${paint(33, 'baseUrl must start with http:// or https://')} — cancelled.\n`);
|
|
868
781
|
return null;
|
|
869
782
|
}
|
|
870
783
|
const apiKey = (await _quickPrompt(` ${bold('api-key')} ${dim('(blank if the endpoint is auth-less, e.g. local vLLM):')} `)).trim();
|
|
@@ -888,7 +801,7 @@ export async function _addCustomProviderInteractive() {
|
|
|
888
801
|
} else if (result.probe.ok) {
|
|
889
802
|
probeMsg = ` ${ok('✓')} registered — /v1/models returned no entries (will rely on free-text model id).\n`;
|
|
890
803
|
} else {
|
|
891
|
-
probeMsg = `
|
|
804
|
+
probeMsg = ` ${paint(33, '!')} registered, but /v1/models probe failed: ${result.probe.error}\n`;
|
|
892
805
|
}
|
|
893
806
|
process.stdout.write('\n');
|
|
894
807
|
process.stdout.write(` ${ok(bold('✓ custom provider saved:'))} ${entry.name} ${dim('→')} ${entry.baseUrl}\n`);
|
|
@@ -897,7 +810,15 @@ export async function _addCustomProviderInteractive() {
|
|
|
897
810
|
await _quickPrompt(' press Enter to continue ');
|
|
898
811
|
return { name };
|
|
899
812
|
}
|
|
900
|
-
|
|
813
|
+
// Single-line readline prompt. Pass { secret: true } to mask the typed value
|
|
814
|
+
// (api keys, channel tokens) — the bytes are read in raw mode and echoed as
|
|
815
|
+
// bullets so the secret never appears on screen / scrollback / a screen-share.
|
|
816
|
+
// Non-TTY input can't be masked (no raw mode); it falls back to the plain read,
|
|
817
|
+
// which is fine for piped automation where there is no screen to leak to.
|
|
818
|
+
export async function _quickPrompt(label, opts = {}) {
|
|
819
|
+
if (opts.secret && process.stdin.isTTY && process.stdin.setRawMode) {
|
|
820
|
+
return _quickPromptSecret(label);
|
|
821
|
+
}
|
|
901
822
|
const readline = await import('node:readline');
|
|
902
823
|
process.stdout.write('\n');
|
|
903
824
|
// Make sure stdin is in cooked / line-buffered mode for the
|
|
@@ -914,3 +835,52 @@ export async function _quickPrompt(label) {
|
|
|
914
835
|
rl.close();
|
|
915
836
|
return ans.trim();
|
|
916
837
|
}
|
|
838
|
+
|
|
839
|
+
// Masked raw-mode reader: echoes one bullet per typed character, handles
|
|
840
|
+
// Backspace, Enter/Ctrl-D (submit), and Ctrl-C (abort → empty string). The real
|
|
841
|
+
// characters accumulate off-screen and are returned trimmed.
|
|
842
|
+
export async function _quickPromptSecret(label) {
|
|
843
|
+
const stdin = process.stdin;
|
|
844
|
+
process.stdout.write('\n' + label);
|
|
845
|
+
const wasRaw = !!stdin.isRaw;
|
|
846
|
+
try { stdin.setRawMode(true); } catch (_) {}
|
|
847
|
+
stdin.resume();
|
|
848
|
+
if (stdin.ref) stdin.ref();
|
|
849
|
+
const prevEnc = stdin.readableEncoding;
|
|
850
|
+
stdin.setEncoding('utf8');
|
|
851
|
+
let buf = '';
|
|
852
|
+
const value = await new Promise((resolve) => {
|
|
853
|
+
const onData = (chunk) => {
|
|
854
|
+
for (const ch of String(chunk)) {
|
|
855
|
+
if (ch === '\r' || ch === '\n' || ch === '\x04') { // Enter / Ctrl-D
|
|
856
|
+
cleanup(); resolve(buf); return;
|
|
857
|
+
}
|
|
858
|
+
if (ch === '\x03') { // Ctrl-C abort
|
|
859
|
+
cleanup(); resolve(''); return;
|
|
860
|
+
}
|
|
861
|
+
if (ch === '\x7f' || ch === '\x08') { // Backspace / Delete
|
|
862
|
+
if (buf.length) { buf = buf.slice(0, -1); process.stdout.write('\b \b'); }
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
if (ch < ' ') continue; // ignore other control chars (arrows, etc.)
|
|
866
|
+
buf += ch;
|
|
867
|
+
process.stdout.write('•');
|
|
868
|
+
}
|
|
869
|
+
};
|
|
870
|
+
const cleanup = () => {
|
|
871
|
+
stdin.removeListener('data', onData);
|
|
872
|
+
try { stdin.setRawMode(wasRaw); } catch (_) {}
|
|
873
|
+
if (prevEnc) { try { stdin.setEncoding(prevEnc); } catch (_) {} }
|
|
874
|
+
process.stdout.write('\n');
|
|
875
|
+
};
|
|
876
|
+
stdin.on('data', onData);
|
|
877
|
+
});
|
|
878
|
+
return value.trim();
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Re-export the orchestrator setup wizard (moved to tui/orchestrator_setup.mjs).
|
|
882
|
+
// Kept at the BOTTOM so _arrowMenu / _quickPrompt — which that module imports
|
|
883
|
+
// back from here — are already declared when the cycle resolves. The helpers
|
|
884
|
+
// are hoisted top-level function declarations and used only at call time, so
|
|
885
|
+
// the pickers ↔ orchestrator_setup cycle never reads an uninitialised binding.
|
|
886
|
+
export { _setupOrchestratorInteractive } from './orchestrator_setup.mjs';
|