lazyclaw 3.99.5 → 3.99.8
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.md +1 -1
- package/cli.mjs +647 -180
- package/daemon.mjs +28 -0
- package/package.json +1 -1
- package/web/dashboard.html +459 -0
package/cli.mjs
CHANGED
|
@@ -877,6 +877,8 @@ const SUBCOMMANDS = [
|
|
|
877
877
|
'rates',
|
|
878
878
|
// OpenClaw-parity subsurfaces (v3.93–v3.98)
|
|
879
879
|
'auth', 'pairing', 'nodes', 'message', 'workspace', 'browse', 'cron',
|
|
880
|
+
// v3.99.6 — multi-step setup wizard + lazyclaw-only dashboard
|
|
881
|
+
'setup', 'dashboard',
|
|
880
882
|
];
|
|
881
883
|
|
|
882
884
|
const SUBCOMMAND_SUBS = {
|
|
@@ -1107,6 +1109,8 @@ const HELP_SUMMARIES = {
|
|
|
1107
1109
|
workspace: 'AGENTS.md / SOUL.md / TOOLS.md system-prompt convention (workspace list|init|show|remove|path)',
|
|
1108
1110
|
browse: 'Fetch a URL and emit Markdown on stdout (browse <url> [--max-bytes <N>])',
|
|
1109
1111
|
cron: 'Schedule recurring agent runs via launchd / crontab (cron list|add|remove|show|sync|run)',
|
|
1112
|
+
setup: 'OpenClaw-style multi-step first-run wizard (provider + workspace + skill + webhook + ping)',
|
|
1113
|
+
dashboard: 'Launch the lazyclaw-only web UI (lighter than the full lazyclaude dashboard)',
|
|
1110
1114
|
inspect: 'Print persisted workflow state without executing',
|
|
1111
1115
|
clear: 'Delete a persisted workflow state file (idempotent)',
|
|
1112
1116
|
validate: 'Static-check a workflow file: shape, deps, cycles, parallelism',
|
|
@@ -1144,6 +1148,8 @@ const HELP_DETAILS = {
|
|
|
1144
1148
|
workspace: 'Usage: lazyclaw workspace <list | init <name> | show <name> [<file>] | remove <name> | path <name>>\n Workspace = a directory under <configDir>/workspaces/<name>/ containing AGENTS.md, SOUL.md, TOOLS.md.\n When `chat` or `agent` is invoked with --workspace <name>, the three files are stitched into a single system prompt at the head of the conversation. Missing files are skipped silently.\n init scaffolds the three files with short stubs you replace.\n show prints the composed prompt; show <name> AGENTS.md (etc) prints just one file.',
|
|
1145
1149
|
browse: 'Usage: lazyclaw browse <url> [--max-bytes <N>] [--timeout-ms <N>] [--user-agent <ua>] [--meta]\n Fetches the URL and emits Markdown on stdout. Pipes cleanly into `agent`:\n lazyclaw browse https://example.com/docs | lazyclaw agent -\n Strips <script>/<style>/<svg>/comments, prefers <main>/<article>, falls back to <body>.\n --max-bytes caps the body read (default 2 MB) so a misconfigured upstream can\'t OOM the process.\n --meta prints { url, title, bytes, truncated } as JSON to stderr alongside the markdown on stdout.',
|
|
1146
1150
|
cron: 'Usage: lazyclaw cron <list | add <name> "<cron-spec>" -- <cmd> ... | remove <name> | show <name> | sync | run <name>>\n Schedule recurring agent runs. macOS uses launchd (~/Library/LaunchAgents/com.lazyclaw.<name>.plist); Linux / WSL uses the user crontab.\n Cron spec is the standard 5-field form (minute hour dom month dow). Supports *, range a-b, list a,b,c, step */N.\n add: pass the command after `--`. Typical use:\n lazyclaw cron add daily-summary "0 9 * * 1-5" -- lazyclaw agent "Summarise today\'s TODOs"\n list / show: read from cfg.cron[name] (config is the source of truth).\n sync: re-installs every job in cfg.cron into the system scheduler — handy after a reinstall.\n run: one-shot in-process execution of the named job; the OS scheduler does the same thing on its trigger.\n Logs: ~/.lazyclaw/logs/cron-<name>.{out,err}.log (macOS launchd path).',
|
|
1151
|
+
setup: 'Usage: lazyclaw setup [--skip-test]\n OpenClaw-style multi-step first-run wizard. Walks through:\n 1. Provider + model + api-key (delegates to onboard --pick)\n 2. Optional workspace init (AGENTS.md / SOUL.md / TOOLS.md)\n 3. Optional skill bundle install from GitHub\n 4. Optional outbound webhook (Slack / Discord)\n 5. Reachability test against the picked provider\n Each optional step takes Enter or "skip" to bypass. Re-runnable safely.\n Also fires automatically on first run when `lazyclaw` is invoked with no config.',
|
|
1152
|
+
dashboard: 'Usage: lazyclaw dashboard [--port <N>] [--no-open]\n Launches the lazyclaw-only web UI on http://127.0.0.1:<port> (default 19600) and opens it in the default browser.\n Wraps `lazyclaw daemon` + a static HTML; no Python / lazyclaude dashboard required.\n Tabs: Chat · Sessions · Skills · Workspace · Providers · Status. Each tab calls existing daemon endpoints.\n --no-open keeps the browser closed (handy for SSH / headless / dev). The bound URL is always printed to stdout.',
|
|
1147
1153
|
};
|
|
1148
1154
|
|
|
1149
1155
|
function cmdHelp(name) {
|
|
@@ -1312,14 +1318,18 @@ async function cmdAgent(prompt, flags) {
|
|
|
1312
1318
|
// (replaces rl.line with the full command). Tab still goes through
|
|
1313
1319
|
// readline's tab-completer for cycling.
|
|
1314
1320
|
function _attachGhostAutocomplete(rl) {
|
|
1315
|
-
// Returns
|
|
1316
|
-
//
|
|
1317
|
-
//
|
|
1318
|
-
//
|
|
1319
|
-
//
|
|
1320
|
-
|
|
1321
|
+
// Returns `{ dispose, suspend, resume }`. Dispose detaches the
|
|
1322
|
+
// keypress + rl 'line' listeners (failure to do so leaks the
|
|
1323
|
+
// event-loop ref, which is exactly the slow-exit bug v3.92
|
|
1324
|
+
// fixed). Suspend / resume gate the keypress handler so the
|
|
1325
|
+
// streaming chat output isn't interleaved with `\x1b[s\x1b[K\x1b[u`
|
|
1326
|
+
// ghost-render escapes — that interleaving is what surfaces as
|
|
1327
|
+
// visible gaps between Korean characters in long replies.
|
|
1328
|
+
const noop = () => {};
|
|
1329
|
+
if (!process.stdout.isTTY) return { dispose: noop, suspend: noop, resume: noop };
|
|
1321
1330
|
const cmds = SLASH_COMMANDS.map((c) => c.cmd);
|
|
1322
1331
|
let lastGhost = '';
|
|
1332
|
+
let suspended = false;
|
|
1323
1333
|
// Find the longest match for the current input. Returns '' when
|
|
1324
1334
|
// nothing matches or when the input already equals a command.
|
|
1325
1335
|
const findMatch = () => {
|
|
@@ -1356,6 +1366,10 @@ function _attachGhostAutocomplete(rl) {
|
|
|
1356
1366
|
// _refreshLine, then return without forwarding the keypress.
|
|
1357
1367
|
const onKeypress = (_str, key) => {
|
|
1358
1368
|
if (!key) return;
|
|
1369
|
+
// While a streaming response is being printed, do nothing —
|
|
1370
|
+
// any ANSI cursor save / restore we emit would tear the wide-
|
|
1371
|
+
// character (CJK) output apart on the visible terminal.
|
|
1372
|
+
if (suspended) return;
|
|
1359
1373
|
if (key.name === 'right' && lastGhost && rl.line === rl.line.trim() &&
|
|
1360
1374
|
rl.cursor === (rl.line || '').length && (rl.line || '').length < lastGhost.length) {
|
|
1361
1375
|
const accepted = lastGhost;
|
|
@@ -1381,33 +1395,81 @@ function _attachGhostAutocomplete(rl) {
|
|
|
1381
1395
|
// over between turns.
|
|
1382
1396
|
const onLine = () => { lastGhost = ''; };
|
|
1383
1397
|
rl.on('line', onLine);
|
|
1384
|
-
|
|
1398
|
+
const dispose = () => {
|
|
1385
1399
|
try { process.stdin.removeListener('keypress', onKeypress); } catch (_) {}
|
|
1386
1400
|
try { rl.removeListener('line', onLine); } catch (_) {}
|
|
1387
1401
|
// Wipe any leftover ghost on screen so the user's terminal doesn't
|
|
1388
1402
|
// keep a dim suffix after we exit.
|
|
1389
1403
|
try { process.stdout.write('\x1b[s\x1b[K\x1b[u'); } catch (_) {}
|
|
1390
1404
|
};
|
|
1405
|
+
return {
|
|
1406
|
+
dispose,
|
|
1407
|
+
suspend: () => {
|
|
1408
|
+
suspended = true;
|
|
1409
|
+
// Wipe any half-rendered ghost before streaming starts so the
|
|
1410
|
+
// first chunk lands at the same column as the prompt.
|
|
1411
|
+
try { process.stdout.write('\x1b[s\x1b[K\x1b[u'); } catch (_) {}
|
|
1412
|
+
},
|
|
1413
|
+
resume: () => { suspended = false; },
|
|
1414
|
+
};
|
|
1391
1415
|
}
|
|
1392
1416
|
|
|
1393
1417
|
// LazyClaw banner — printed once at the top of every interactive chat
|
|
1394
1418
|
// session so users see the active provider/model before they start
|
|
1395
1419
|
// typing. Plain ANSI; auto-skipped when stdout isn't a TTY (so piped
|
|
1396
1420
|
// invocations stay clean for tests/scripts).
|
|
1421
|
+
// Single source of truth for the LazyClaw banner — used by the chat
|
|
1422
|
+
// REPL header, the no-arg launcher, and the first-run welcome panel.
|
|
1423
|
+
// Returns an array of pre-formatted lines (with ANSI colour) so the
|
|
1424
|
+
// caller can splice in additional rows without re-implementing the
|
|
1425
|
+
// alignment.
|
|
1426
|
+
//
|
|
1427
|
+
// Width-management rule: every inner line is forced through
|
|
1428
|
+
// `.padEnd(W)` so a stray width miscount can't punch the right
|
|
1429
|
+
// border off the box (which is exactly the bug v3.99.5 shipped:
|
|
1430
|
+
// two of the inner lines were 33 cols vs the others' 32, so the
|
|
1431
|
+
// ╮ rendered into the next line).
|
|
1432
|
+
function _renderBanner(version) {
|
|
1433
|
+
const W = 30;
|
|
1434
|
+
const accent = (s) => `\x1b[38;5;208m${s}\x1b[0m`;
|
|
1435
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
1436
|
+
// Inner content of each banner row — DO NOT pad here, the wrapper
|
|
1437
|
+
// does it. Backslashes are JS-escaped so each `\\` renders as one
|
|
1438
|
+
// literal `\` in the output.
|
|
1439
|
+
const inner = [
|
|
1440
|
+
' _',
|
|
1441
|
+
' | |__ _ _____ _ _',
|
|
1442
|
+
" | / _` |_ / || | '_|",
|
|
1443
|
+
' |_\\__,_/__\\_, |_|',
|
|
1444
|
+
' LazyClaw |__/ ' + String(version || '?.?.?').padEnd(10).slice(0, 10),
|
|
1445
|
+
];
|
|
1446
|
+
// Sleepy-cat mascot on the right, lined up with the busiest part
|
|
1447
|
+
// of the wordmark. Three rows of ASCII art + "zz" trail. Plain
|
|
1448
|
+
// ASCII (no box-drawing on the cat) so it lands well in any font.
|
|
1449
|
+
const mascot = [
|
|
1450
|
+
'',
|
|
1451
|
+
'',
|
|
1452
|
+
' /\\_/\\',
|
|
1453
|
+
' ( -.- ) ' + dim('z z'),
|
|
1454
|
+
' > ^ < ' + dim('z'),
|
|
1455
|
+
'',
|
|
1456
|
+
'',
|
|
1457
|
+
];
|
|
1458
|
+
const banner = [
|
|
1459
|
+
'╭' + '─'.repeat(W) + '╮',
|
|
1460
|
+
...inner.map((s) => '│' + s.padEnd(W).slice(0, W) + '│'),
|
|
1461
|
+
'╰' + '─'.repeat(W) + '╯',
|
|
1462
|
+
];
|
|
1463
|
+
return banner.map((l, i) => ' ' + accent(l) + (mascot[i] ? ' ' + mascot[i] : ''));
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1397
1466
|
function _printChatBanner(activeProvName, activeModel, version) {
|
|
1398
1467
|
if (!process.stdout.isTTY) return;
|
|
1399
1468
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
1400
|
-
const accent = (s) => `\x1b[38;5;208m${s}\x1b[0m`;
|
|
1401
1469
|
const ok = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
1402
1470
|
const lines = [
|
|
1403
1471
|
'',
|
|
1404
|
-
|
|
1405
|
-
accent(' │ _ │'),
|
|
1406
|
-
accent(' │ | |__ _ _____ _ _ │'),
|
|
1407
|
-
accent(' │ | / _` |_ / || | \'_| │'),
|
|
1408
|
-
accent(' │ |_\\__,_/__\\_, |_| │'),
|
|
1409
|
-
accent(' │ LazyClaw |__/ ' + (version || '').padEnd(10) + ' │'),
|
|
1410
|
-
accent(' ╰──────────────────────────────╯'),
|
|
1472
|
+
..._renderBanner(version),
|
|
1411
1473
|
'',
|
|
1412
1474
|
` ${dim('provider ·')} ${ok(activeProvName)}`,
|
|
1413
1475
|
` ${dim('model ·')} ${ok(activeModel || '(default)')}`,
|
|
@@ -1422,31 +1484,25 @@ function _printChatBanner(activeProvName, activeModel, version) {
|
|
|
1422
1484
|
// when the user passes --pick. Falls back to plain stdin reads when
|
|
1423
1485
|
// stdout isn't a TTY (CI/script callers should pass --non-interactive
|
|
1424
1486
|
// equivalents instead).
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
? meta.suggestedModels
|
|
1438
|
-
: [null];
|
|
1439
|
-
const keyTag = meta.requiresApiKey
|
|
1440
|
-
? '\x1b[38;5;245m[api key]\x1b[0m'
|
|
1441
|
-
: (name === 'claude-cli' ? '\x1b[38;5;208m[subscription]\x1b[0m' : '\x1b[38;5;245m[no key]\x1b[0m');
|
|
1442
|
-
for (const m of models) {
|
|
1443
|
-
const label = m ? `${name.padEnd(11)} ${m}` : `${name.padEnd(11)} (no model)`;
|
|
1444
|
-
items.push({ provider: name, model: m, label, keyTag });
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1487
|
+
// Generic arrow-key menu used by the multi-step provider/model
|
|
1488
|
+
// picker below. Returns the picked item, or one of the sentinel
|
|
1489
|
+
// strings 'BACK' (Esc — caller should retry the previous step) or
|
|
1490
|
+
// 'CANCEL' (q — caller should bail entirely). Ctrl-C exits the
|
|
1491
|
+
// process directly, matching every other interactive prompt in the
|
|
1492
|
+
// CLI.
|
|
1493
|
+
//
|
|
1494
|
+
// `items` is an array of { id, label, desc, tag }. `tag` is an
|
|
1495
|
+
// optional pre-coloured pill (e.g. "[api key]") that lands on the
|
|
1496
|
+
// right side of the row. `defaultIdx` lets the caller pin where the
|
|
1497
|
+
// cursor lands; default 0.
|
|
1498
|
+
async function _arrowMenu({ title, subtitle, footer, items, defaultIdx = 0 }) {
|
|
1447
1499
|
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
1448
|
-
//
|
|
1449
|
-
|
|
1500
|
+
// Non-TTY fallback: print the labels on stderr and read a single
|
|
1501
|
+
// line of stdin. Used when somebody pipes input to `lazyclaw
|
|
1502
|
+
// setup` — the wizard still works, just without arrows.
|
|
1503
|
+
process.stderr.write(`${title}\n`);
|
|
1504
|
+
items.forEach((it, i) => process.stderr.write(` ${i + 1}. ${it.label}${it.desc ? ' — ' + it.desc : ''}\n`));
|
|
1505
|
+
process.stderr.write('pick (number / id, blank for first): ');
|
|
1450
1506
|
const ans = await new Promise((resolve) => {
|
|
1451
1507
|
let buf = '';
|
|
1452
1508
|
const onData = (chunk) => {
|
|
@@ -1455,38 +1511,47 @@ async function _pickProviderInteractive() {
|
|
|
1455
1511
|
};
|
|
1456
1512
|
process.stdin.on('data', onData);
|
|
1457
1513
|
});
|
|
1458
|
-
|
|
1514
|
+
if (!ans) return items[0];
|
|
1515
|
+
const byNum = parseInt(ans, 10);
|
|
1516
|
+
if (Number.isFinite(byNum) && byNum >= 1 && byNum <= items.length) return items[byNum - 1];
|
|
1517
|
+
const byId = items.find((it) => it.id === ans || it.label === ans);
|
|
1518
|
+
return byId || items[0];
|
|
1459
1519
|
}
|
|
1460
|
-
// Default cursor: lands on item 0 (= the first row from PROVIDERS
|
|
1461
|
-
// insertion order, which registry.mjs deliberately curates as the
|
|
1462
|
-
// most user-familiar vendor — gemini at the time of writing).
|
|
1463
|
-
let idx = 0;
|
|
1464
1520
|
|
|
1465
1521
|
const readline = await import('node:readline');
|
|
1466
1522
|
readline.emitKeypressEvents(process.stdin);
|
|
1467
1523
|
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
1468
|
-
|
|
1469
|
-
|
|
1524
|
+
let idx = Math.max(0, Math.min(items.length - 1, defaultIdx));
|
|
1525
|
+
const accent = (s) => `\x1b[38;5;208m${s}\x1b[0m`;
|
|
1526
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
1527
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
1528
|
+
|
|
1470
1529
|
const draw = () => {
|
|
1471
|
-
process.stdout.write('\x1b[?25l');
|
|
1472
|
-
process.stdout.write('\
|
|
1473
|
-
process.stdout.write(
|
|
1474
|
-
process.stdout.write('
|
|
1475
|
-
process.stdout.
|
|
1476
|
-
const rows = Math.max(6, (process.stdout.rows || 24) - 6);
|
|
1530
|
+
process.stdout.write('\x1b[?25l\x1b[2J\x1b[H');
|
|
1531
|
+
process.stdout.write(accent(title) + '\n');
|
|
1532
|
+
if (subtitle) process.stdout.write(dim(subtitle) + '\n');
|
|
1533
|
+
process.stdout.write(dim('↑/↓ to move · Enter to confirm · Esc to back · q to quit') + '\n\n');
|
|
1534
|
+
const rows = Math.max(6, (process.stdout.rows || 24) - 8);
|
|
1477
1535
|
let from = Math.max(0, idx - Math.floor(rows / 2));
|
|
1478
1536
|
if (from + rows > items.length) from = Math.max(0, items.length - rows);
|
|
1479
1537
|
const to = Math.min(items.length, from + rows);
|
|
1538
|
+
// Pre-compute label width so descriptions line up across rows.
|
|
1539
|
+
const labelW = items.reduce((w, it) => Math.max(w, (it.label || '').length), 12);
|
|
1480
1540
|
for (let i = from; i < to; i++) {
|
|
1481
1541
|
const it = items[i];
|
|
1482
|
-
const marker = i === idx ? '
|
|
1483
|
-
const
|
|
1484
|
-
|
|
1542
|
+
const marker = i === idx ? accent('❯ ') : ' ';
|
|
1543
|
+
const lbl = (it.label || '').padEnd(labelW);
|
|
1544
|
+
const lblOut = i === idx ? bold(lbl) : lbl;
|
|
1545
|
+
const desc = it.desc ? ' ' + dim(it.desc) : '';
|
|
1546
|
+
const tag = it.tag ? ' ' + it.tag : '';
|
|
1547
|
+
process.stdout.write(`${marker}${lblOut}${desc}${tag}\n`);
|
|
1485
1548
|
}
|
|
1486
1549
|
if (to < items.length) {
|
|
1487
|
-
process.stdout.write(
|
|
1550
|
+
process.stdout.write(`${dim(` …(${items.length - to} more)`)}\n`);
|
|
1488
1551
|
}
|
|
1552
|
+
if (footer) process.stdout.write('\n' + dim(footer) + '\n');
|
|
1489
1553
|
};
|
|
1554
|
+
|
|
1490
1555
|
draw();
|
|
1491
1556
|
return await new Promise((resolve) => {
|
|
1492
1557
|
const onKey = (_str, key) => {
|
|
@@ -1499,18 +1564,145 @@ async function _pickProviderInteractive() {
|
|
|
1499
1564
|
else if (key.name === 'end') { idx = items.length - 1; draw(); }
|
|
1500
1565
|
else if (key.name === 'return') { cleanup(); resolve(items[idx]); }
|
|
1501
1566
|
else if (key.ctrl && key.name === 'c') { cleanup(); process.exit(130); }
|
|
1502
|
-
else if (key.name === '
|
|
1567
|
+
else if (key.name === 'escape') { cleanup(); resolve('BACK'); }
|
|
1568
|
+
else if (key.name === 'q') { cleanup(); resolve('CANCEL'); }
|
|
1503
1569
|
};
|
|
1504
1570
|
const cleanup = () => {
|
|
1505
1571
|
process.stdin.off('keypress', onKey);
|
|
1506
1572
|
if (process.stdin.setRawMode) process.stdin.setRawMode(false);
|
|
1507
|
-
process.stdout.write('\x1b[?25h');
|
|
1508
|
-
process.stdout.write('\x1b[2J\x1b[H'); // clear screen
|
|
1573
|
+
process.stdout.write('\x1b[?25h\x1b[2J\x1b[H');
|
|
1509
1574
|
};
|
|
1510
1575
|
process.stdin.on('keypress', onKey);
|
|
1511
1576
|
});
|
|
1512
1577
|
}
|
|
1513
1578
|
|
|
1579
|
+
// Bucket every registered provider into one of three auth-method
|
|
1580
|
+
// families. The picker's first step asks the user which family
|
|
1581
|
+
// they want before drilling into specific providers — much less
|
|
1582
|
+
// overwhelming than a flat 40-row list. Bucket assignment lives
|
|
1583
|
+
// here (rather than registry.mjs) because it's a UX concept, not
|
|
1584
|
+
// an intrinsic provider attribute.
|
|
1585
|
+
function _providerFamilies() {
|
|
1586
|
+
const info = _registryMod.PROVIDER_INFO || {};
|
|
1587
|
+
const all = Object.keys(_registryMod.PROVIDERS);
|
|
1588
|
+
const buckets = {
|
|
1589
|
+
api: { label: 'API key', desc: 'paste an sk-... key during setup', tag: '\x1b[38;5;245m[needs key]\x1b[0m', members: [] },
|
|
1590
|
+
cli: { label: 'CLI / Local', desc: 'keyless — uses an existing CLI login or a local daemon', tag: '\x1b[38;5;208m[no key]\x1b[0m', members: [] },
|
|
1591
|
+
mock: { label: 'Mock', desc: 'offline echo, only useful for testing', tag: '\x1b[38;5;245m[test]\x1b[0m', members: [] },
|
|
1592
|
+
};
|
|
1593
|
+
for (const name of all) {
|
|
1594
|
+
if (name === 'mock') buckets.mock.members.push(name);
|
|
1595
|
+
else if ((info[name] || {}).requiresApiKey) buckets.api.members.push(name);
|
|
1596
|
+
else buckets.cli.members.push(name);
|
|
1597
|
+
}
|
|
1598
|
+
return buckets;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// Multi-step provider / model picker — replaces the flat 40-row
|
|
1602
|
+
// list of v3.99.5 with a drill-in:
|
|
1603
|
+
//
|
|
1604
|
+
// Step 1 — auth family (API key / CLI-Local / Mock)
|
|
1605
|
+
// Step 2 — provider in that family (gemini / openai / claude-cli / …)
|
|
1606
|
+
// Step 3 — model in that provider's suggestedModels
|
|
1607
|
+
//
|
|
1608
|
+
// Esc at any step goes back one. q or Ctrl-C cancels entirely.
|
|
1609
|
+
// Steps that have only one option auto-advance so the user doesn't
|
|
1610
|
+
// stare at a single-row menu (e.g. the Mock family has just `mock`).
|
|
1611
|
+
async function _pickProviderInteractive() {
|
|
1612
|
+
const providers = Object.keys(_registryMod.PROVIDERS);
|
|
1613
|
+
if (!providers.length) return { provider: 'mock', model: null };
|
|
1614
|
+
const info = _registryMod.PROVIDER_INFO || {};
|
|
1615
|
+
const families = _providerFamilies();
|
|
1616
|
+
|
|
1617
|
+
// Non-TTY fallback — single-prompt picker, identical to before.
|
|
1618
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
1619
|
+
process.stdout.write(`provider [${providers.join('|')}]: `);
|
|
1620
|
+
const ans = await new Promise((resolve) => {
|
|
1621
|
+
let buf = '';
|
|
1622
|
+
const onData = (chunk) => {
|
|
1623
|
+
buf += chunk.toString();
|
|
1624
|
+
if (buf.includes('\n')) { process.stdin.off('data', onData); resolve(buf.trim()); }
|
|
1625
|
+
};
|
|
1626
|
+
process.stdin.on('data', onData);
|
|
1627
|
+
});
|
|
1628
|
+
return { provider: ans || providers[0], model: null };
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// ── Step 1 — auth family ──────────────────────────────────────
|
|
1632
|
+
let family = null;
|
|
1633
|
+
while (!family) {
|
|
1634
|
+
const familyItems = Object.entries(families)
|
|
1635
|
+
.filter(([, b]) => b.members.length > 0)
|
|
1636
|
+
.map(([id, b]) => ({
|
|
1637
|
+
id,
|
|
1638
|
+
label: b.label,
|
|
1639
|
+
desc: `${b.desc} · ${b.members.join(' / ')}`,
|
|
1640
|
+
tag: b.tag,
|
|
1641
|
+
}));
|
|
1642
|
+
const picked = await _arrowMenu({
|
|
1643
|
+
title: 'LazyClaw setup — Step 1 of 3: pick how you want to auth',
|
|
1644
|
+
subtitle: 'API: bring your own key · CLI/Local: use what\'s already on this machine · Mock: offline test',
|
|
1645
|
+
items: familyItems,
|
|
1646
|
+
});
|
|
1647
|
+
if (picked === 'CANCEL' || picked === 'BACK') return null;
|
|
1648
|
+
family = picked;
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
// ── Step 2 — provider in that family ──────────────────────────
|
|
1652
|
+
let provider = null;
|
|
1653
|
+
while (!provider) {
|
|
1654
|
+
const memberNames = families[family.id].members;
|
|
1655
|
+
if (memberNames.length === 1) {
|
|
1656
|
+
// Auto-advance — no point making the user pick from a single
|
|
1657
|
+
// row.
|
|
1658
|
+
provider = { id: memberNames[0] };
|
|
1659
|
+
break;
|
|
1660
|
+
}
|
|
1661
|
+
const provItems = memberNames.map((name) => {
|
|
1662
|
+
const meta = info[name] || {};
|
|
1663
|
+
const models = (meta.suggestedModels || []).slice(0, 4).join(' · ') || '(default)';
|
|
1664
|
+
return {
|
|
1665
|
+
id: name,
|
|
1666
|
+
label: name,
|
|
1667
|
+
desc: `models: ${models}`,
|
|
1668
|
+
tag: meta.requiresApiKey ? '\x1b[38;5;245m[api key]\x1b[0m' : '\x1b[38;5;208m[no key]\x1b[0m',
|
|
1669
|
+
};
|
|
1670
|
+
});
|
|
1671
|
+
const picked = await _arrowMenu({
|
|
1672
|
+
title: `LazyClaw setup — Step 2 of 3: pick a ${family.label} provider`,
|
|
1673
|
+
subtitle: `Showing ${memberNames.length} ${family.label.toLowerCase()} provider(s).`,
|
|
1674
|
+
items: provItems,
|
|
1675
|
+
});
|
|
1676
|
+
if (picked === 'CANCEL') return null;
|
|
1677
|
+
if (picked === 'BACK') { family = null; return _pickProviderInteractive(); }
|
|
1678
|
+
provider = picked;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// ── Step 3 — model ────────────────────────────────────────────
|
|
1682
|
+
const meta = info[provider.id] || {};
|
|
1683
|
+
const models = Array.isArray(meta.suggestedModels) ? meta.suggestedModels : [];
|
|
1684
|
+
if (!models.length) {
|
|
1685
|
+
// Provider has no curated models (mock) — return without a
|
|
1686
|
+
// model so the underlying call uses the provider default.
|
|
1687
|
+
return { provider: provider.id, model: null };
|
|
1688
|
+
}
|
|
1689
|
+
while (true) {
|
|
1690
|
+
const modelItems = models.map((m) => ({ id: m, label: m, desc: '' }));
|
|
1691
|
+
// Pin the cursor to the provider's defaultModel so Enter without
|
|
1692
|
+
// navigation picks the most-recommended one.
|
|
1693
|
+
const defaultIdx = Math.max(0, models.indexOf(meta.defaultModel || models[0]));
|
|
1694
|
+
const picked = await _arrowMenu({
|
|
1695
|
+
title: `LazyClaw setup — Step 3 of 3: pick a model for ${provider.id}`,
|
|
1696
|
+
subtitle: `Showing ${models.length} suggested model(s). Type the model id directly later via /model in chat to use anything not listed here.`,
|
|
1697
|
+
items: modelItems,
|
|
1698
|
+
defaultIdx,
|
|
1699
|
+
});
|
|
1700
|
+
if (picked === 'CANCEL') return null;
|
|
1701
|
+
if (picked === 'BACK') return _pickProviderInteractive(); // back to step 1
|
|
1702
|
+
return { provider: provider.id, model: picked.id };
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1514
1706
|
async function cmdChat(flags = {}) {
|
|
1515
1707
|
await ensureRegistry();
|
|
1516
1708
|
const sessionsMod = await import('./sessions.mjs');
|
|
@@ -1553,13 +1745,13 @@ async function cmdChat(flags = {}) {
|
|
|
1553
1745
|
terminal: useTerminal,
|
|
1554
1746
|
prompt: useTerminal ? '\x1b[38;5;208m›\x1b[0m ' : '',
|
|
1555
1747
|
});
|
|
1556
|
-
let
|
|
1748
|
+
let _ghost = { dispose: () => {}, suspend: () => {}, resume: () => {} };
|
|
1557
1749
|
if (useTerminal) {
|
|
1558
1750
|
// Cursor-style ghost autocomplete: when the buffer starts with `/`,
|
|
1559
1751
|
// render the longest matching command after the cursor in dim grey.
|
|
1560
1752
|
// Right-arrow at end-of-line accepts. Tab still cycles via the
|
|
1561
1753
|
// existing handleSlash branch; this only adds the inline preview.
|
|
1562
|
-
|
|
1754
|
+
_ghost = _attachGhostAutocomplete(rl) || _ghost;
|
|
1563
1755
|
rl.prompt();
|
|
1564
1756
|
}
|
|
1565
1757
|
|
|
@@ -1811,6 +2003,32 @@ async function cmdChat(flags = {}) {
|
|
|
1811
2003
|
process.stdout.write('\n^C interrupted — prompt is back\n');
|
|
1812
2004
|
};
|
|
1813
2005
|
process.on('SIGINT', onSigint);
|
|
2006
|
+
// Pause the ghost-autocomplete keypress handler while the
|
|
2007
|
+
// provider is streaming. Without this, every stale stdin event
|
|
2008
|
+
// would trigger `\x1b[s\x1b[K\x1b[u` cursor save/restore writes
|
|
2009
|
+
// that interleave with the streamed text and surface as visible
|
|
2010
|
+
// gaps between CJK characters (visible in user-reported screen
|
|
2011
|
+
// captures of Korean replies).
|
|
2012
|
+
if (useTerminal) _ghost.suspend();
|
|
2013
|
+
// Buffered writer — coalesce single-character streaming chunks
|
|
2014
|
+
// into ~30 ms windows. Two reasons:
|
|
2015
|
+
// 1. Korean / Japanese / Chinese tokens often arrive as one
|
|
2016
|
+
// character per chunk. Each individual `process.stdout.write`
|
|
2017
|
+
// can race against terminal redraw on a wide-cell character,
|
|
2018
|
+
// producing the same "visible space between every character"
|
|
2019
|
+
// symptom the suspend above also addresses.
|
|
2020
|
+
// 2. Far fewer syscalls. A 200-char Korean reply was ~200
|
|
2021
|
+
// separate writes; this collapses to ~7-10.
|
|
2022
|
+
let _writeBuf = '';
|
|
2023
|
+
let _writeTimer = null;
|
|
2024
|
+
const _flush = () => {
|
|
2025
|
+
if (_writeBuf) { process.stdout.write(_writeBuf); _writeBuf = ''; }
|
|
2026
|
+
_writeTimer = null;
|
|
2027
|
+
};
|
|
2028
|
+
const _writeChunk = (s) => {
|
|
2029
|
+
_writeBuf += s;
|
|
2030
|
+
if (!_writeTimer) _writeTimer = setTimeout(_flush, 30);
|
|
2031
|
+
};
|
|
1814
2032
|
try {
|
|
1815
2033
|
for await (const chunk of prov.sendMessage(messages, {
|
|
1816
2034
|
apiKey: _resolveAuthKey(cfg, activeProvName),
|
|
@@ -1819,13 +2037,21 @@ async function cmdChat(flags = {}) {
|
|
|
1819
2037
|
signal: turnAc.signal,
|
|
1820
2038
|
onUsage: accumulateUsage,
|
|
1821
2039
|
})) {
|
|
1822
|
-
|
|
2040
|
+
_writeChunk(chunk);
|
|
1823
2041
|
acc += chunk;
|
|
1824
2042
|
}
|
|
2043
|
+
// Drain anything still buffered before the trailing newline so
|
|
2044
|
+
// the prompt lands on its own line cleanly.
|
|
2045
|
+
if (_writeTimer) clearTimeout(_writeTimer);
|
|
2046
|
+
_flush();
|
|
1825
2047
|
process.stdout.write('\n');
|
|
1826
2048
|
messages.push({ role: 'assistant', content: acc });
|
|
1827
2049
|
persistTurn('assistant', acc);
|
|
1828
2050
|
} catch (err) {
|
|
2051
|
+
// Drain pending buffer so partial reply stays on screen even
|
|
2052
|
+
// when the stream errors mid-flight.
|
|
2053
|
+
if (_writeTimer) clearTimeout(_writeTimer);
|
|
2054
|
+
_flush();
|
|
1829
2055
|
// ABORT errors are user-initiated; partial assistant output is
|
|
1830
2056
|
// discarded (we don't append a half-reply to the message history
|
|
1831
2057
|
// because the next turn would treat it as a complete reply and
|
|
@@ -1835,6 +2061,7 @@ async function cmdChat(flags = {}) {
|
|
|
1835
2061
|
}
|
|
1836
2062
|
} finally {
|
|
1837
2063
|
process.off('SIGINT', onSigint);
|
|
2064
|
+
if (useTerminal) _ghost.resume();
|
|
1838
2065
|
}
|
|
1839
2066
|
if (useTerminal) rl.prompt();
|
|
1840
2067
|
} } finally {
|
|
@@ -1842,7 +2069,7 @@ async function cmdChat(flags = {}) {
|
|
|
1842
2069
|
// hung for ~3-5 s while Node waited for stdin's keypress listener
|
|
1843
2070
|
// and raw mode to release. Tearing them down explicitly drops the
|
|
1844
2071
|
// exit time to <100 ms.
|
|
1845
|
-
try {
|
|
2072
|
+
try { _ghost.dispose(); } catch (_) {}
|
|
1846
2073
|
try { rl.close(); } catch (_) {}
|
|
1847
2074
|
if (useTerminal && process.stdin.isTTY && process.stdin.setRawMode) {
|
|
1848
2075
|
try { process.stdin.setRawMode(false); } catch (_) {}
|
|
@@ -1855,6 +2082,69 @@ async function cmdChat(flags = {}) {
|
|
|
1855
2082
|
}
|
|
1856
2083
|
}
|
|
1857
2084
|
|
|
2085
|
+
// Light wrapper around the daemon — meant for users who installed
|
|
2086
|
+
// via npm and don't want to remember `daemon` flags. Boots the
|
|
2087
|
+
// daemon on a fixed default port (override with --port), then opens
|
|
2088
|
+
// the dashboard URL in the user's default browser.
|
|
2089
|
+
//
|
|
2090
|
+
// Why a separate command: typing `lazyclaw daemon` works too, but
|
|
2091
|
+
// `dashboard` is the discoverable name and it auto-opens the browser
|
|
2092
|
+
// (which the bare daemon doesn't, since most daemon callers are
|
|
2093
|
+
// scripts).
|
|
2094
|
+
async function cmdDashboard(flags = {}) {
|
|
2095
|
+
await ensureRegistry();
|
|
2096
|
+
const sessionsMod = await import('./sessions.mjs');
|
|
2097
|
+
const { startDaemon } = await import('./daemon.mjs');
|
|
2098
|
+
const port = flags.port !== undefined ? parseInt(flags.port, 10) : 19600;
|
|
2099
|
+
const cfgDir = path.dirname(configPath());
|
|
2100
|
+
const d = await startDaemon({
|
|
2101
|
+
port,
|
|
2102
|
+
once: false,
|
|
2103
|
+
readConfig,
|
|
2104
|
+
sessionsDirGetter: () => cfgDir,
|
|
2105
|
+
sessionsMod,
|
|
2106
|
+
version: () => readVersionFromRepo(),
|
|
2107
|
+
workflowStateDir: () => process.env.LAZYCLAW_WORKFLOW_STATE_DIR || '.workflow-state',
|
|
2108
|
+
// No auth token by default — same loopback-only assumption the
|
|
2109
|
+
// bare daemon uses. Users who want to expose the dashboard set
|
|
2110
|
+
// LAZYCLAW_AUTH_TOKEN + --allow-origin via the daemon command.
|
|
2111
|
+
authToken: undefined,
|
|
2112
|
+
allowedOrigins: [],
|
|
2113
|
+
rateLimit: null,
|
|
2114
|
+
responseCache: null,
|
|
2115
|
+
logger: null,
|
|
2116
|
+
costCap: null,
|
|
2117
|
+
});
|
|
2118
|
+
const url = `http://127.0.0.1:${d.port}/dashboard`;
|
|
2119
|
+
process.stdout.write(`🦞 LazyClaw dashboard listening at ${url}\n`);
|
|
2120
|
+
if (!flags['no-open']) {
|
|
2121
|
+
// macOS uses `open`; Linux generally `xdg-open`; Windows
|
|
2122
|
+
// `cmd /c start`. Detect by platform; bail silently if the
|
|
2123
|
+
// helper fails — the URL is already on stdout for fallback.
|
|
2124
|
+
const { spawn } = await import('node:child_process');
|
|
2125
|
+
let cmd, args;
|
|
2126
|
+
if (process.platform === 'darwin') { cmd = 'open'; args = [url]; }
|
|
2127
|
+
else if (process.platform === 'win32') { cmd = 'cmd'; args = ['/c', 'start', '""', url]; }
|
|
2128
|
+
else { cmd = 'xdg-open'; args = [url]; }
|
|
2129
|
+
try {
|
|
2130
|
+
spawn(cmd, args, { stdio: 'ignore', detached: true }).unref();
|
|
2131
|
+
} catch (_) { /* user can click the URL above */ }
|
|
2132
|
+
}
|
|
2133
|
+
// Forward SIGINT/SIGTERM to a graceful shutdown so Ctrl-C doesn't
|
|
2134
|
+
// strand a port-bound server. Same shape cmdDaemon uses.
|
|
2135
|
+
const { gracefulShutdown } = await import('./daemon.mjs');
|
|
2136
|
+
let shuttingDown = false;
|
|
2137
|
+
const shutdown = async () => {
|
|
2138
|
+
if (shuttingDown) return process.exit(1);
|
|
2139
|
+
shuttingDown = true;
|
|
2140
|
+
process.stdout.write('\n shutting down…\n');
|
|
2141
|
+
const result = await gracefulShutdown(d.server, 5_000);
|
|
2142
|
+
process.exit(result.forced ? 1 : 0);
|
|
2143
|
+
};
|
|
2144
|
+
process.on('SIGINT', shutdown);
|
|
2145
|
+
process.on('SIGTERM', shutdown);
|
|
2146
|
+
}
|
|
2147
|
+
|
|
1858
2148
|
async function cmdDaemon(flags) {
|
|
1859
2149
|
await ensureRegistry();
|
|
1860
2150
|
const sessionsMod = await import('./sessions.mjs');
|
|
@@ -3082,6 +3372,144 @@ function parseArgs(argv) {
|
|
|
3082
3372
|
// so chat / agent / etc. behave bit-identically to typing them
|
|
3083
3373
|
// directly. Non-TTY (piped, scripted) callers still see the
|
|
3084
3374
|
// classic "Usage: …" line so automation isn't surprised.
|
|
3375
|
+
// Multi-step setup wizard — OpenClaw-style first-run experience.
|
|
3376
|
+
// Provider/model/key + optional workspace + optional sample skill
|
|
3377
|
+
// + reachability ping. Each step can be skipped (Enter on prompt /
|
|
3378
|
+
// "n" on yes-no). Re-runnable safely: existing state is reused, not
|
|
3379
|
+
// clobbered, except when the user explicitly opts in.
|
|
3380
|
+
//
|
|
3381
|
+
// `lazyclaw setup` exposes this directly so users can re-run the
|
|
3382
|
+
// wizard any time. The first-run code path also funnels through it
|
|
3383
|
+
// so a fresh install sees the same flow whether they typed
|
|
3384
|
+
// `lazyclaw` or `lazyclaw setup`.
|
|
3385
|
+
async function cmdSetup(_sub, _positional, flags = {}) {
|
|
3386
|
+
await ensureRegistry();
|
|
3387
|
+
const accent = (s) => `\x1b[38;5;208m${s}\x1b[0m`;
|
|
3388
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
3389
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
3390
|
+
const ok = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
3391
|
+
const warn = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
3392
|
+
|
|
3393
|
+
// Header.
|
|
3394
|
+
if (process.stdout.isTTY) process.stdout.write('\x1b[2J\x1b[H');
|
|
3395
|
+
_renderBanner(readVersionFromRepo()).forEach((l) => process.stdout.write(l + '\n'));
|
|
3396
|
+
process.stdout.write('\n');
|
|
3397
|
+
process.stdout.write(` ${bold('🔧 Setup wizard')}\n`);
|
|
3398
|
+
process.stdout.write(` ${dim('Five short steps. Press Enter to accept the default; type "skip" or "n" to bypass an optional step.')}\n\n`);
|
|
3399
|
+
|
|
3400
|
+
const cfg = readConfig();
|
|
3401
|
+
const cfgDir = path.dirname(configPath());
|
|
3402
|
+
|
|
3403
|
+
// ── Step 1: Provider + model (mandatory) ────────────────────
|
|
3404
|
+
process.stdout.write(` ${accent('Step 1/5 ·')} ${bold('Pick a provider + model')}\n`);
|
|
3405
|
+
process.stdout.write(` ${dim('Opens the arrow-key picker. The list leads with gemini / openai / claude-cli — pick the one you have an account or login for.')}\n\n`);
|
|
3406
|
+
await _quickPrompt(' ▶ press Enter to open the picker ');
|
|
3407
|
+
try {
|
|
3408
|
+
await cmdOnboard({ pick: true });
|
|
3409
|
+
} catch (e) {
|
|
3410
|
+
process.stderr.write(`onboard error: ${e?.message || e}\n`);
|
|
3411
|
+
process.exit(1);
|
|
3412
|
+
}
|
|
3413
|
+
// Re-read config after onboard wrote it. If the user aborted with
|
|
3414
|
+
// no provider set, bail out early — the rest of the wizard depends
|
|
3415
|
+
// on a provider being configured.
|
|
3416
|
+
const cfgAfterOnboard = readConfig();
|
|
3417
|
+
if (!cfgAfterOnboard.provider) {
|
|
3418
|
+
process.stdout.write(`\n ${warn('Setup aborted — no provider configured. Run `lazyclaw setup` again when ready.')}\n\n`);
|
|
3419
|
+
process.exit(0);
|
|
3420
|
+
}
|
|
3421
|
+
process.stdout.write(`\n ${ok('✓ provider:')} ${cfgAfterOnboard.provider} ${dim('model:')} ${cfgAfterOnboard.model || '(default)'}\n\n`);
|
|
3422
|
+
|
|
3423
|
+
// ── Step 2: Optional workspace ──────────────────────────────
|
|
3424
|
+
process.stdout.write(` ${accent('Step 2/5 ·')} ${bold('Initialise a workspace?')} ${dim('(optional)')}\n`);
|
|
3425
|
+
process.stdout.write(` ${dim('A workspace is a folder of AGENTS.md / SOUL.md / TOOLS.md prompt files that auto-inject into chat / agent. Skip if you don\'t need project-specific personas yet.')}\n\n`);
|
|
3426
|
+
const wsName = (await _quickPrompt(' workspace name (Enter to skip): ')).trim();
|
|
3427
|
+
if (wsName && /^[A-Za-z0-9_.-]+$/.test(wsName)) {
|
|
3428
|
+
try {
|
|
3429
|
+
const ws = await import('./workspace.mjs');
|
|
3430
|
+
const dir = ws.initWorkspace(cfgDir, wsName);
|
|
3431
|
+
process.stdout.write(` ${ok('✓ workspace created:')} ${dir}\n`);
|
|
3432
|
+
process.stdout.write(` ${dim('Edit AGENTS.md / SOUL.md / TOOLS.md any time. Use with: lazyclaw chat --workspace ' + wsName)}\n\n`);
|
|
3433
|
+
} catch (e) {
|
|
3434
|
+
process.stdout.write(` ${warn('skipped:')} ${e?.message || e}\n\n`);
|
|
3435
|
+
}
|
|
3436
|
+
} else if (wsName) {
|
|
3437
|
+
process.stdout.write(` ${warn('skipped:')} workspace name must match [A-Za-z0-9_.-]+\n\n`);
|
|
3438
|
+
} else {
|
|
3439
|
+
process.stdout.write(` ${dim('— skipped —')}\n\n`);
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
// ── Step 3: Optional skill bundle install ───────────────────
|
|
3443
|
+
process.stdout.write(` ${accent('Step 3/5 ·')} ${bold('Install a skill bundle from GitHub?')} ${dim('(optional)')}\n`);
|
|
3444
|
+
process.stdout.write(` ${dim('Format: <user>/<repo>[@<ref>]. Skills are .md prompt fragments that compose into the system prompt via --skill.')}\n\n`);
|
|
3445
|
+
const skillSpec = (await _quickPrompt(' github spec (Enter to skip): ')).trim();
|
|
3446
|
+
if (skillSpec) {
|
|
3447
|
+
try {
|
|
3448
|
+
const inst = await import('./skills_install.mjs');
|
|
3449
|
+
const r = await inst.installFromGithub(skillSpec, cfgDir, { force: false });
|
|
3450
|
+
process.stdout.write(` ${ok('✓ installed')} ${r.installed.length} ${dim('skill(s) from')} ${skillSpec}\n`);
|
|
3451
|
+
r.installed.forEach((s) => process.stdout.write(` · ${s.name} ${dim(`(${s.bytes} bytes)`)}\n`));
|
|
3452
|
+
if (r.skipped.length) {
|
|
3453
|
+
process.stdout.write(` ${dim('skipped (already installed):')} ${r.skipped.map((s) => s.name).join(', ')}\n`);
|
|
3454
|
+
}
|
|
3455
|
+
process.stdout.write('\n');
|
|
3456
|
+
} catch (e) {
|
|
3457
|
+
process.stdout.write(` ${warn('skipped:')} ${e?.message || e}\n\n`);
|
|
3458
|
+
}
|
|
3459
|
+
} else {
|
|
3460
|
+
process.stdout.write(` ${dim('— skipped —')}\n\n`);
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
// ── Step 4: Optional outbound webhook ───────────────────────
|
|
3464
|
+
process.stdout.write(` ${accent('Step 4/5 ·')} ${bold('Add an outbound webhook?')} ${dim('(optional)')}\n`);
|
|
3465
|
+
process.stdout.write(` ${dim('Use with: lazyclaw message send <name> <text>. Slack / Discord Incoming Webhook URLs work as-is.')}\n\n`);
|
|
3466
|
+
const hookName = (await _quickPrompt(' webhook name (Enter to skip): ')).trim();
|
|
3467
|
+
if (hookName) {
|
|
3468
|
+
const hookUrl = (await _quickPrompt(' webhook URL: ')).trim();
|
|
3469
|
+
if (!hookUrl) {
|
|
3470
|
+
process.stdout.write(` ${warn('skipped:')} URL required\n\n`);
|
|
3471
|
+
} else {
|
|
3472
|
+
try {
|
|
3473
|
+
const cf = await import('./config_features.mjs');
|
|
3474
|
+
const fresh = readConfig();
|
|
3475
|
+
cf.messageAdd(fresh, hookName, hookUrl);
|
|
3476
|
+
writeConfig(fresh);
|
|
3477
|
+
process.stdout.write(` ${ok('✓ webhook saved:')} ${hookName}\n\n`);
|
|
3478
|
+
} catch (e) {
|
|
3479
|
+
process.stdout.write(` ${warn('skipped:')} ${e?.message || e}\n\n`);
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
} else {
|
|
3483
|
+
process.stdout.write(` ${dim('— skipped —')}\n\n`);
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
// ── Step 5: Reachability check ──────────────────────────────
|
|
3487
|
+
process.stdout.write(` ${accent('Step 5/5 ·')} ${bold('Verify the picked provider responds')}\n`);
|
|
3488
|
+
process.stdout.write(` ${dim('Sends a 1-token "ping" via `lazyclaw providers test`. Confirms your key / subscription / local daemon is wired up.')}\n\n`);
|
|
3489
|
+
const wantPing = !flags['skip-test'] && (await _quickPrompt(' test now? [Y/n] ')).trim().toLowerCase() !== 'n';
|
|
3490
|
+
if (wantPing) {
|
|
3491
|
+
try {
|
|
3492
|
+
// Reuse the existing providers-test path so behaviour matches
|
|
3493
|
+
// a manual `lazyclaw providers test`.
|
|
3494
|
+
await cmdProviders('test', [cfgAfterOnboard.provider], {});
|
|
3495
|
+
} catch (e) {
|
|
3496
|
+
process.stdout.write(` ${warn('test errored:')} ${e?.message || e}\n`);
|
|
3497
|
+
process.stdout.write(` ${dim('Setup still completed; you can retry with:')} lazyclaw providers test ${cfgAfterOnboard.provider}\n`);
|
|
3498
|
+
}
|
|
3499
|
+
} else {
|
|
3500
|
+
process.stdout.write(` ${dim('— skipped —')}\n`);
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
// ── Wrap up ─────────────────────────────────────────────────
|
|
3504
|
+
process.stdout.write('\n');
|
|
3505
|
+
process.stdout.write(` ${ok(bold('🎉 Setup complete.'))}\n`);
|
|
3506
|
+
process.stdout.write(` ${dim('Run')} ${bold('lazyclaw')} ${dim('any time to open the menu, or jump in directly:')}\n`);
|
|
3507
|
+
process.stdout.write(` ${dim('•')} lazyclaw chat ${dim('— REPL with the configured provider')}\n`);
|
|
3508
|
+
process.stdout.write(` ${dim('•')} lazyclaw agent "..." ${dim('— one-shot prompt')}\n`);
|
|
3509
|
+
process.stdout.write(` ${dim('•')} lazyclaw doctor ${dim('— diagnostic JSON')}\n`);
|
|
3510
|
+
process.stdout.write(` ${dim('•')} lazyclaw setup ${dim('— re-run this wizard any time')}\n\n`);
|
|
3511
|
+
}
|
|
3512
|
+
|
|
3085
3513
|
// First-run welcome panel + delegated onboard. Drawn once before the
|
|
3086
3514
|
// main launcher menu when the config has no provider yet. Walks the
|
|
3087
3515
|
// user through the same arrow-key picker that `lazyclaw onboard`
|
|
@@ -3093,16 +3521,7 @@ async function _runFirstTimeOnboard() {
|
|
|
3093
3521
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
3094
3522
|
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
3095
3523
|
process.stdout.write('\x1b[2J\x1b[H');
|
|
3096
|
-
|
|
3097
|
-
accent(' ╭──────────────────────────────╮'),
|
|
3098
|
-
accent(' │ _ │'),
|
|
3099
|
-
accent(' │ | |__ _ _____ _ _ │'),
|
|
3100
|
-
accent(' │ | / _` |_ / || | \'_| │'),
|
|
3101
|
-
accent(' │ |_\\__,_/__\\_, |_| │'),
|
|
3102
|
-
accent(' │ LazyClaw |__/ ' + (readVersionFromRepo() || '').padEnd(10) + ' │'),
|
|
3103
|
-
accent(' ╰──────────────────────────────╯'),
|
|
3104
|
-
];
|
|
3105
|
-
banner.forEach((l) => process.stdout.write(l + '\n'));
|
|
3524
|
+
_renderBanner(readVersionFromRepo()).forEach((l) => process.stdout.write(l + '\n'));
|
|
3106
3525
|
process.stdout.write('\n');
|
|
3107
3526
|
process.stdout.write(` ${bold('👋 Welcome — first-time setup')}\n\n`);
|
|
3108
3527
|
process.stdout.write(` ${dim('No provider configured yet at')} ${configPath()}\n`);
|
|
@@ -3124,31 +3543,46 @@ async function _runFirstTimeOnboard() {
|
|
|
3124
3543
|
process.stdout.write('\n');
|
|
3125
3544
|
}
|
|
3126
3545
|
|
|
3546
|
+
// Direct dispatch from a launcher pick. Replaces the previous
|
|
3547
|
+
// `process.argv = [...]; await main()` round-trip so we can reuse
|
|
3548
|
+
// the launcher across multiple iterations without compounding
|
|
3549
|
+
// state. Each menu choice maps to its native cmd handler with the
|
|
3550
|
+
// same flag defaults the bare CLI would parse.
|
|
3551
|
+
async function _dispatchMenuChoice(argv) {
|
|
3552
|
+
const sub = argv[0];
|
|
3553
|
+
const rest = argv.slice(1);
|
|
3554
|
+
switch (sub) {
|
|
3555
|
+
case 'chat': return cmdChat({});
|
|
3556
|
+
case 'agent': return cmdAgent(rest[0] || '-', {});
|
|
3557
|
+
case 'onboard': return cmdOnboard({});
|
|
3558
|
+
case 'setup': return cmdSetup(undefined, rest, {});
|
|
3559
|
+
case 'workspace': return cmdWorkspace(rest[0], rest.slice(1), {});
|
|
3560
|
+
case 'browse': return cmdBrowse(rest[0], {});
|
|
3561
|
+
case 'skills': return cmdSkills(rest[0], rest.slice(1), {});
|
|
3562
|
+
case 'sessions': return cmdSessions(rest[0], rest.slice(1), {});
|
|
3563
|
+
case 'providers': return cmdProviders(rest[0], rest.slice(1), {});
|
|
3564
|
+
case 'cron': return cmdCron(rest[0], rest.slice(1), {});
|
|
3565
|
+
case 'auth': return cmdAuth(rest[0], rest.slice(1), {});
|
|
3566
|
+
case 'pairing': return cmdPairing(rest[0], rest.slice(1), {});
|
|
3567
|
+
case 'nodes': return cmdNodes(rest[0], rest.slice(1), {});
|
|
3568
|
+
case 'message': return cmdMessage(rest[0], rest.slice(1), {});
|
|
3569
|
+
case 'doctor': return cmdDoctor();
|
|
3570
|
+
case 'status': return cmdStatus();
|
|
3571
|
+
case 'help': return cmdHelp();
|
|
3572
|
+
case 'dashboard': return cmdDashboard({});
|
|
3573
|
+
default: throw new Error(`unknown menu choice: ${sub}`);
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
|
|
3127
3577
|
async function cmdLauncher() {
|
|
3128
3578
|
await ensureRegistry();
|
|
3129
|
-
|
|
3130
|
-
//
|
|
3131
|
-
// menu pick that calls a provider (Chat / Agent / Doctor / etc.)
|
|
3132
|
-
// would error halfway through with a confusing "missing api key"
|
|
3133
|
-
// or "unknown provider". Detect that state up front and walk the
|
|
3134
|
-
// user through onboard before showing the menu — once they've
|
|
3135
|
-
// picked, re-read the config and continue normally.
|
|
3136
|
-
if (!cfg.provider) {
|
|
3137
|
-
await _runFirstTimeOnboard();
|
|
3138
|
-
cfg = readConfig();
|
|
3139
|
-
// If they cancelled / aborted onboard we still don't have a
|
|
3140
|
-
// provider — drop straight out instead of showing a menu where
|
|
3141
|
-
// every item leads to the same error.
|
|
3142
|
-
if (!cfg.provider) {
|
|
3143
|
-
process.stdout.write('\n Setup not completed — exiting.\n Run `lazyclaw onboard` when ready, then try `lazyclaw` again.\n\n');
|
|
3144
|
-
process.exit(0);
|
|
3145
|
-
}
|
|
3146
|
-
}
|
|
3147
|
-
const provider = cfg.provider;
|
|
3148
|
-
const model = cfg.model || '(default)';
|
|
3579
|
+
// Item table is fixed across iterations — only the dispatcher and
|
|
3580
|
+
// the per-iteration draw redraw on each loop tick.
|
|
3149
3581
|
const items = [
|
|
3150
3582
|
{ id: 'chat', label: 'Chat', desc: 'interactive REPL with the configured provider', argv: ['chat'] },
|
|
3151
3583
|
{ id: 'agent', label: 'Agent', desc: 'one-shot prompt — read text and exit', argv: ['agent'], promptForBody: true },
|
|
3584
|
+
{ id: 'dashboard', label: 'Dashboard', desc: 'open the lazyclaw web UI in your browser', argv: ['dashboard'] },
|
|
3585
|
+
{ id: 'setup', label: 'Setup', desc: 'multi-step provider / workspace / skill wizard', argv: ['setup'] },
|
|
3152
3586
|
{ id: 'onboard', label: 'Onboard', desc: 'pick provider / model / api-key', argv: ['onboard'] },
|
|
3153
3587
|
{ id: 'workspace', label: 'Workspace', desc: 'AGENTS.md / SOUL.md / TOOLS.md prompt bundles', argv: ['workspace', 'list'] },
|
|
3154
3588
|
{ id: 'browse', label: 'Browse', desc: 'fetch a URL → markdown', argv: ['browse'], promptForUrl: true },
|
|
@@ -3159,106 +3593,131 @@ async function cmdLauncher() {
|
|
|
3159
3593
|
{ id: 'doctor', label: 'Doctor', desc: 'diagnostic — config, providers, workflows', argv: ['doctor'] },
|
|
3160
3594
|
{ id: 'status', label: 'Status', desc: 'current provider / model / masked key', argv: ['status'] },
|
|
3161
3595
|
{ id: 'help', label: 'Help', desc: 'one-line summary of every subcommand', argv: ['help'] },
|
|
3162
|
-
{ id: 'quit', label: 'Quit', desc: 'exit
|
|
3596
|
+
{ id: 'quit', label: 'Quit', desc: 'exit lazyclaw', argv: null },
|
|
3163
3597
|
];
|
|
3164
3598
|
|
|
3165
|
-
const readline = await import('node:readline');
|
|
3166
|
-
readline.emitKeypressEvents(process.stdin);
|
|
3167
|
-
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
3168
|
-
let idx = 0;
|
|
3169
|
-
|
|
3170
|
-
// Pretty header — same accent palette as _printChatBanner so
|
|
3171
|
-
// returning users recognise it.
|
|
3172
3599
|
const accent = (s) => `\x1b[38;5;208m${s}\x1b[0m`;
|
|
3173
3600
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
3174
3601
|
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
3175
3602
|
const ok = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
3176
3603
|
const warn = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
3177
3604
|
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
// Trim list to terminal height so the menu still fits when
|
|
3201
|
-
// someone shrinks the window or runs in a small split pane.
|
|
3202
|
-
const rowsAvail = Math.max(items.length, (process.stdout.rows || 30) - 14);
|
|
3203
|
-
const fromIdx = Math.max(0, Math.min(items.length - rowsAvail, idx - Math.floor(rowsAvail / 2)));
|
|
3204
|
-
const toIdx = Math.min(items.length, fromIdx + rowsAvail);
|
|
3205
|
-
for (let i = fromIdx; i < toIdx; i++) {
|
|
3206
|
-
const it = items[i];
|
|
3207
|
-
const marker = i === idx ? accent('❯ ') : ' ';
|
|
3208
|
-
const lbl = i === idx ? bold(it.label.padEnd(11)) : it.label.padEnd(11);
|
|
3209
|
-
process.stdout.write(`${marker}${lbl} ${dim(it.desc)}\n`);
|
|
3605
|
+
let idx = 0;
|
|
3606
|
+
// Outer loop — each iteration is one menu render → pick →
|
|
3607
|
+
// dispatch round. Subcommand return drops back here and the menu
|
|
3608
|
+
// is redrawn. Quit / Esc / Ctrl-C breaks the loop and returns,
|
|
3609
|
+
// which lets the calling main() exit naturally (no process.exit
|
|
3610
|
+
// so the buffered writer / open file descriptors close cleanly).
|
|
3611
|
+
while (true) {
|
|
3612
|
+
// First-run / config-missing guard: a fresh install has no
|
|
3613
|
+
// `provider` set, so any menu pick that calls a provider would
|
|
3614
|
+
// error halfway through. Funnel through cmdSetup before
|
|
3615
|
+
// rendering the menu the first time around.
|
|
3616
|
+
let cfg = readConfig();
|
|
3617
|
+
if (!cfg.provider) {
|
|
3618
|
+
try { await cmdSetup(undefined, [], {}); }
|
|
3619
|
+
catch (e) {
|
|
3620
|
+
process.stderr.write(`setup error: ${e?.message || e}\n`);
|
|
3621
|
+
}
|
|
3622
|
+
cfg = readConfig();
|
|
3623
|
+
if (!cfg.provider) {
|
|
3624
|
+
process.stdout.write('\n Setup not completed — exiting.\n Run `lazyclaw setup` when ready, then try `lazyclaw` again.\n\n');
|
|
3625
|
+
return;
|
|
3626
|
+
}
|
|
3210
3627
|
}
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
// Tear down raw mode + listeners cleanly so the next subcommand
|
|
3215
|
-
// starts with a sane stdin (otherwise `chat` after launcher inherits
|
|
3216
|
-
// the launcher's raw mode and behaves weirdly).
|
|
3217
|
-
const teardown = (onKey) => {
|
|
3218
|
-
if (onKey) process.stdin.off('keypress', onKey);
|
|
3219
|
-
if (process.stdin.setRawMode) process.stdin.setRawMode(false);
|
|
3220
|
-
process.stdout.write('\x1b[?25h'); // show cursor
|
|
3221
|
-
process.stdout.write('\x1b[2J\x1b[H'); // clear screen
|
|
3222
|
-
};
|
|
3628
|
+
const provider = cfg.provider;
|
|
3629
|
+
const model = cfg.model || '(default)';
|
|
3223
3630
|
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3631
|
+
// Re-establish stdin in raw / ref'd mode. A previous iteration
|
|
3632
|
+
// (e.g. `chat`) deliberately paused + unref'd stdin in its
|
|
3633
|
+
// exit-cleanup path so the process could end on /exit; now that
|
|
3634
|
+
// we want to keep going, re-attach.
|
|
3635
|
+
const readline = await import('node:readline');
|
|
3636
|
+
readline.emitKeypressEvents(process.stdin);
|
|
3637
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
3638
|
+
process.stdin.resume();
|
|
3639
|
+
process.stdin.ref();
|
|
3640
|
+
|
|
3641
|
+
const draw = () => {
|
|
3642
|
+
process.stdout.write('\x1b[?25l\x1b[2J\x1b[H'); // hide cursor + clear
|
|
3643
|
+
_renderBanner(readVersionFromRepo()).forEach((l) => process.stdout.write(l + '\n'));
|
|
3644
|
+
process.stdout.write('\n');
|
|
3645
|
+
process.stdout.write(` ${dim('provider ·')} ${ok(provider)}\n`);
|
|
3646
|
+
process.stdout.write(` ${dim('model ·')} ${ok(model)}\n`);
|
|
3647
|
+
process.stdout.write(` ${dim('config ·')} ${dim(configPath())}\n`);
|
|
3648
|
+
process.stdout.write('\n');
|
|
3649
|
+
process.stdout.write(` ${dim('↑/↓ to move · Enter to select · q or Esc to quit')}\n\n`);
|
|
3650
|
+
const rowsAvail = Math.max(items.length, (process.stdout.rows || 30) - 14);
|
|
3651
|
+
const fromIdx = Math.max(0, Math.min(items.length - rowsAvail, idx - Math.floor(rowsAvail / 2)));
|
|
3652
|
+
const toIdx = Math.min(items.length, fromIdx + rowsAvail);
|
|
3653
|
+
for (let i = fromIdx; i < toIdx; i++) {
|
|
3654
|
+
const it = items[i];
|
|
3655
|
+
const marker = i === idx ? accent('❯ ') : ' ';
|
|
3656
|
+
const lbl = i === idx ? bold(it.label.padEnd(11)) : it.label.padEnd(11);
|
|
3657
|
+
process.stdout.write(`${marker}${lbl} ${dim(it.desc)}\n`);
|
|
3658
|
+
}
|
|
3659
|
+
process.stdout.write('\n');
|
|
3237
3660
|
};
|
|
3238
|
-
process.stdin.on('keypress', onKey);
|
|
3239
|
-
});
|
|
3240
3661
|
|
|
3241
|
-
|
|
3242
|
-
|
|
3662
|
+
draw();
|
|
3663
|
+
const picked = await new Promise((resolve) => {
|
|
3664
|
+
const onKey = (_str, key) => {
|
|
3665
|
+
if (!key) return;
|
|
3666
|
+
if (key.name === 'up') { idx = (idx - 1 + items.length) % items.length; draw(); }
|
|
3667
|
+
else if (key.name === 'down') { idx = (idx + 1) % items.length; draw(); }
|
|
3668
|
+
else if (key.name === 'home') { idx = 0; draw(); }
|
|
3669
|
+
else if (key.name === 'end') { idx = items.length - 1; draw(); }
|
|
3670
|
+
else if (key.name === 'pageup') { idx = Math.max(0, idx - 5); draw(); }
|
|
3671
|
+
else if (key.name === 'pagedown') { idx = Math.min(items.length - 1, idx + 5); draw(); }
|
|
3672
|
+
else if (key.name === 'return') { cleanup(); resolve(items[idx]); }
|
|
3673
|
+
else if (key.ctrl && key.name === 'c') { cleanup(); resolve({ id: 'quit', argv: null }); }
|
|
3674
|
+
else if (key.name === 'escape' || key.name === 'q') { cleanup(); resolve({ id: 'quit', argv: null }); }
|
|
3675
|
+
function cleanup() {
|
|
3676
|
+
process.stdin.off('keypress', onKey);
|
|
3677
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(false);
|
|
3678
|
+
process.stdout.write('\x1b[?25h\x1b[2J\x1b[H');
|
|
3679
|
+
}
|
|
3680
|
+
};
|
|
3681
|
+
process.stdin.on('keypress', onKey);
|
|
3682
|
+
});
|
|
3683
|
+
|
|
3684
|
+
if (!picked || picked.id === 'quit' || !picked.argv) {
|
|
3685
|
+
// Plain return so main() can exit naturally.
|
|
3686
|
+
return;
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
// Two menu items need a follow-up question before they can run:
|
|
3690
|
+
// agent (prompt body), browse (URL). Ask once, then dispatch.
|
|
3691
|
+
let argv = picked.argv;
|
|
3692
|
+
if (picked.promptForBody) {
|
|
3693
|
+
const body = await _quickPrompt('prompt: ');
|
|
3694
|
+
if (!body) continue; // back to menu
|
|
3695
|
+
argv = ['agent', body];
|
|
3696
|
+
} else if (picked.promptForUrl) {
|
|
3697
|
+
const url = await _quickPrompt('url: ');
|
|
3698
|
+
if (!url) continue; // back to menu
|
|
3699
|
+
argv = ['browse', url];
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
// Dispatch. Errors don't terminate the launcher — they're
|
|
3703
|
+
// surfaced as a stderr line and the menu redraws. Lets the
|
|
3704
|
+
// user recover from a transient API hiccup without a relaunch.
|
|
3705
|
+
try {
|
|
3706
|
+
await _dispatchMenuChoice(argv);
|
|
3707
|
+
} catch (e) {
|
|
3708
|
+
process.stderr.write(`\n ${accent('✗')} ${e?.message || String(e)}\n`);
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3711
|
+
// Pause before re-drawing so the user can read the subcommand's
|
|
3712
|
+
// output. `chat` is the special case: its REPL has already kept
|
|
3713
|
+
// the user oriented for a long session, and they typed /exit
|
|
3714
|
+
// explicitly, so jumping straight back to the menu reads as
|
|
3715
|
+
// "ok, done with that conversation, back to the dashboard."
|
|
3716
|
+
if (picked.id !== 'chat') {
|
|
3717
|
+
process.stdout.write('\n');
|
|
3718
|
+
await _quickPrompt(` ${dim('Press Enter to return to the menu… ')}`);
|
|
3719
|
+
}
|
|
3243
3720
|
}
|
|
3244
|
-
// Two surfaces need a follow-up question before they can run:
|
|
3245
|
-
// - `agent`: needs a prompt body
|
|
3246
|
-
// - `browse`: needs a URL
|
|
3247
|
-
// Ask via a simple readline prompt so the launcher stays
|
|
3248
|
-
// self-contained instead of forwarding into a half-typed argv.
|
|
3249
|
-
if (picked.promptForBody) {
|
|
3250
|
-
const body = await _quickPrompt('prompt: ');
|
|
3251
|
-
if (!body) process.exit(0);
|
|
3252
|
-
picked.argv = ['agent', body];
|
|
3253
|
-
} else if (picked.promptForUrl) {
|
|
3254
|
-
const url = await _quickPrompt('url: ');
|
|
3255
|
-
if (!url) process.exit(0);
|
|
3256
|
-
picked.argv = ['browse', url];
|
|
3257
|
-
}
|
|
3258
|
-
// Replace argv and re-enter main(). The chosen subcommand sees
|
|
3259
|
-
// the same parser surface as if the user had typed it directly.
|
|
3260
|
-
process.argv = [process.argv[0], process.argv[1], ...picked.argv];
|
|
3261
|
-
await main();
|
|
3262
3721
|
}
|
|
3263
3722
|
|
|
3264
3723
|
async function _quickPrompt(label) {
|
|
@@ -3444,6 +3903,14 @@ async function main() {
|
|
|
3444
3903
|
await cmdCron(sub, rest.positional.slice(1), rest.flags);
|
|
3445
3904
|
break;
|
|
3446
3905
|
}
|
|
3906
|
+
case 'setup': {
|
|
3907
|
+
await cmdSetup(undefined, rest.positional, rest.flags);
|
|
3908
|
+
break;
|
|
3909
|
+
}
|
|
3910
|
+
case 'dashboard': {
|
|
3911
|
+
await cmdDashboard(rest.flags);
|
|
3912
|
+
break;
|
|
3913
|
+
}
|
|
3447
3914
|
case 'daemon': {
|
|
3448
3915
|
await cmdDaemon(rest.flags);
|
|
3449
3916
|
break;
|