lazyclaw 3.99.6 → 3.99.9
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 +431 -158
- package/package.json +1 -1
package/README.md
CHANGED
package/cli.mjs
CHANGED
|
@@ -1318,14 +1318,18 @@ async function cmdAgent(prompt, flags) {
|
|
|
1318
1318
|
// (replaces rl.line with the full command). Tab still goes through
|
|
1319
1319
|
// readline's tab-completer for cycling.
|
|
1320
1320
|
function _attachGhostAutocomplete(rl) {
|
|
1321
|
-
// Returns
|
|
1322
|
-
//
|
|
1323
|
-
//
|
|
1324
|
-
//
|
|
1325
|
-
//
|
|
1326
|
-
|
|
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 };
|
|
1327
1330
|
const cmds = SLASH_COMMANDS.map((c) => c.cmd);
|
|
1328
1331
|
let lastGhost = '';
|
|
1332
|
+
let suspended = false;
|
|
1329
1333
|
// Find the longest match for the current input. Returns '' when
|
|
1330
1334
|
// nothing matches or when the input already equals a command.
|
|
1331
1335
|
const findMatch = () => {
|
|
@@ -1362,6 +1366,10 @@ function _attachGhostAutocomplete(rl) {
|
|
|
1362
1366
|
// _refreshLine, then return without forwarding the keypress.
|
|
1363
1367
|
const onKeypress = (_str, key) => {
|
|
1364
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;
|
|
1365
1373
|
if (key.name === 'right' && lastGhost && rl.line === rl.line.trim() &&
|
|
1366
1374
|
rl.cursor === (rl.line || '').length && (rl.line || '').length < lastGhost.length) {
|
|
1367
1375
|
const accepted = lastGhost;
|
|
@@ -1387,13 +1395,23 @@ function _attachGhostAutocomplete(rl) {
|
|
|
1387
1395
|
// over between turns.
|
|
1388
1396
|
const onLine = () => { lastGhost = ''; };
|
|
1389
1397
|
rl.on('line', onLine);
|
|
1390
|
-
|
|
1398
|
+
const dispose = () => {
|
|
1391
1399
|
try { process.stdin.removeListener('keypress', onKeypress); } catch (_) {}
|
|
1392
1400
|
try { rl.removeListener('line', onLine); } catch (_) {}
|
|
1393
1401
|
// Wipe any leftover ghost on screen so the user's terminal doesn't
|
|
1394
1402
|
// keep a dim suffix after we exit.
|
|
1395
1403
|
try { process.stdout.write('\x1b[s\x1b[K\x1b[u'); } catch (_) {}
|
|
1396
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
|
+
};
|
|
1397
1415
|
}
|
|
1398
1416
|
|
|
1399
1417
|
// LazyClaw banner — printed once at the top of every interactive chat
|
|
@@ -1466,31 +1484,25 @@ function _printChatBanner(activeProvName, activeModel, version) {
|
|
|
1466
1484
|
// when the user passes --pick. Falls back to plain stdin reads when
|
|
1467
1485
|
// stdout isn't a TTY (CI/script callers should pass --non-interactive
|
|
1468
1486
|
// equivalents instead).
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
? meta.suggestedModels
|
|
1482
|
-
: [null];
|
|
1483
|
-
const keyTag = meta.requiresApiKey
|
|
1484
|
-
? '\x1b[38;5;245m[api key]\x1b[0m'
|
|
1485
|
-
: (name === 'claude-cli' ? '\x1b[38;5;208m[subscription]\x1b[0m' : '\x1b[38;5;245m[no key]\x1b[0m');
|
|
1486
|
-
for (const m of models) {
|
|
1487
|
-
const label = m ? `${name.padEnd(11)} ${m}` : `${name.padEnd(11)} (no model)`;
|
|
1488
|
-
items.push({ provider: name, model: m, label, keyTag });
|
|
1489
|
-
}
|
|
1490
|
-
}
|
|
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 }) {
|
|
1491
1499
|
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
1492
|
-
//
|
|
1493
|
-
|
|
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): ');
|
|
1494
1506
|
const ans = await new Promise((resolve) => {
|
|
1495
1507
|
let buf = '';
|
|
1496
1508
|
const onData = (chunk) => {
|
|
@@ -1499,38 +1511,47 @@ async function _pickProviderInteractive() {
|
|
|
1499
1511
|
};
|
|
1500
1512
|
process.stdin.on('data', onData);
|
|
1501
1513
|
});
|
|
1502
|
-
|
|
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];
|
|
1503
1519
|
}
|
|
1504
|
-
// Default cursor: lands on item 0 (= the first row from PROVIDERS
|
|
1505
|
-
// insertion order, which registry.mjs deliberately curates as the
|
|
1506
|
-
// most user-familiar vendor — gemini at the time of writing).
|
|
1507
|
-
let idx = 0;
|
|
1508
1520
|
|
|
1509
1521
|
const readline = await import('node:readline');
|
|
1510
1522
|
readline.emitKeypressEvents(process.stdin);
|
|
1511
1523
|
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
1512
|
-
|
|
1513
|
-
|
|
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
|
+
|
|
1514
1529
|
const draw = () => {
|
|
1515
|
-
process.stdout.write('\x1b[?25l');
|
|
1516
|
-
process.stdout.write('\
|
|
1517
|
-
process.stdout.write(
|
|
1518
|
-
process.stdout.write('
|
|
1519
|
-
process.stdout.
|
|
1520
|
-
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);
|
|
1521
1535
|
let from = Math.max(0, idx - Math.floor(rows / 2));
|
|
1522
1536
|
if (from + rows > items.length) from = Math.max(0, items.length - rows);
|
|
1523
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);
|
|
1524
1540
|
for (let i = from; i < to; i++) {
|
|
1525
1541
|
const it = items[i];
|
|
1526
|
-
const marker = i === idx ? '
|
|
1527
|
-
const
|
|
1528
|
-
|
|
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`);
|
|
1529
1548
|
}
|
|
1530
1549
|
if (to < items.length) {
|
|
1531
|
-
process.stdout.write(
|
|
1550
|
+
process.stdout.write(`${dim(` …(${items.length - to} more)`)}\n`);
|
|
1532
1551
|
}
|
|
1552
|
+
if (footer) process.stdout.write('\n' + dim(footer) + '\n');
|
|
1533
1553
|
};
|
|
1554
|
+
|
|
1534
1555
|
draw();
|
|
1535
1556
|
return await new Promise((resolve) => {
|
|
1536
1557
|
const onKey = (_str, key) => {
|
|
@@ -1543,18 +1564,145 @@ async function _pickProviderInteractive() {
|
|
|
1543
1564
|
else if (key.name === 'end') { idx = items.length - 1; draw(); }
|
|
1544
1565
|
else if (key.name === 'return') { cleanup(); resolve(items[idx]); }
|
|
1545
1566
|
else if (key.ctrl && key.name === 'c') { cleanup(); process.exit(130); }
|
|
1546
|
-
else if (key.name === '
|
|
1567
|
+
else if (key.name === 'escape') { cleanup(); resolve('BACK'); }
|
|
1568
|
+
else if (key.name === 'q') { cleanup(); resolve('CANCEL'); }
|
|
1547
1569
|
};
|
|
1548
1570
|
const cleanup = () => {
|
|
1549
1571
|
process.stdin.off('keypress', onKey);
|
|
1550
1572
|
if (process.stdin.setRawMode) process.stdin.setRawMode(false);
|
|
1551
|
-
process.stdout.write('\x1b[?25h');
|
|
1552
|
-
process.stdout.write('\x1b[2J\x1b[H'); // clear screen
|
|
1573
|
+
process.stdout.write('\x1b[?25h\x1b[2J\x1b[H');
|
|
1553
1574
|
};
|
|
1554
1575
|
process.stdin.on('keypress', onKey);
|
|
1555
1576
|
});
|
|
1556
1577
|
}
|
|
1557
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
|
+
|
|
1558
1706
|
async function cmdChat(flags = {}) {
|
|
1559
1707
|
await ensureRegistry();
|
|
1560
1708
|
const sessionsMod = await import('./sessions.mjs');
|
|
@@ -1597,13 +1745,13 @@ async function cmdChat(flags = {}) {
|
|
|
1597
1745
|
terminal: useTerminal,
|
|
1598
1746
|
prompt: useTerminal ? '\x1b[38;5;208m›\x1b[0m ' : '',
|
|
1599
1747
|
});
|
|
1600
|
-
let
|
|
1748
|
+
let _ghost = { dispose: () => {}, suspend: () => {}, resume: () => {} };
|
|
1601
1749
|
if (useTerminal) {
|
|
1602
1750
|
// Cursor-style ghost autocomplete: when the buffer starts with `/`,
|
|
1603
1751
|
// render the longest matching command after the cursor in dim grey.
|
|
1604
1752
|
// Right-arrow at end-of-line accepts. Tab still cycles via the
|
|
1605
1753
|
// existing handleSlash branch; this only adds the inline preview.
|
|
1606
|
-
|
|
1754
|
+
_ghost = _attachGhostAutocomplete(rl) || _ghost;
|
|
1607
1755
|
rl.prompt();
|
|
1608
1756
|
}
|
|
1609
1757
|
|
|
@@ -1855,6 +2003,32 @@ async function cmdChat(flags = {}) {
|
|
|
1855
2003
|
process.stdout.write('\n^C interrupted — prompt is back\n');
|
|
1856
2004
|
};
|
|
1857
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
|
+
};
|
|
1858
2032
|
try {
|
|
1859
2033
|
for await (const chunk of prov.sendMessage(messages, {
|
|
1860
2034
|
apiKey: _resolveAuthKey(cfg, activeProvName),
|
|
@@ -1863,13 +2037,21 @@ async function cmdChat(flags = {}) {
|
|
|
1863
2037
|
signal: turnAc.signal,
|
|
1864
2038
|
onUsage: accumulateUsage,
|
|
1865
2039
|
})) {
|
|
1866
|
-
|
|
2040
|
+
_writeChunk(chunk);
|
|
1867
2041
|
acc += chunk;
|
|
1868
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();
|
|
1869
2047
|
process.stdout.write('\n');
|
|
1870
2048
|
messages.push({ role: 'assistant', content: acc });
|
|
1871
2049
|
persistTurn('assistant', acc);
|
|
1872
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();
|
|
1873
2055
|
// ABORT errors are user-initiated; partial assistant output is
|
|
1874
2056
|
// discarded (we don't append a half-reply to the message history
|
|
1875
2057
|
// because the next turn would treat it as a complete reply and
|
|
@@ -1879,6 +2061,7 @@ async function cmdChat(flags = {}) {
|
|
|
1879
2061
|
}
|
|
1880
2062
|
} finally {
|
|
1881
2063
|
process.off('SIGINT', onSigint);
|
|
2064
|
+
if (useTerminal) _ghost.resume();
|
|
1882
2065
|
}
|
|
1883
2066
|
if (useTerminal) rl.prompt();
|
|
1884
2067
|
} } finally {
|
|
@@ -1886,7 +2069,7 @@ async function cmdChat(flags = {}) {
|
|
|
1886
2069
|
// hung for ~3-5 s while Node waited for stdin's keypress listener
|
|
1887
2070
|
// and raw mode to release. Tearing them down explicitly drops the
|
|
1888
2071
|
// exit time to <100 ms.
|
|
1889
|
-
try {
|
|
2072
|
+
try { _ghost.dispose(); } catch (_) {}
|
|
1890
2073
|
try { rl.close(); } catch (_) {}
|
|
1891
2074
|
if (useTerminal && process.stdin.isTTY && process.stdin.setRawMode) {
|
|
1892
2075
|
try { process.stdin.setRawMode(false); } catch (_) {}
|
|
@@ -3224,16 +3407,23 @@ async function cmdSetup(_sub, _positional, flags = {}) {
|
|
|
3224
3407
|
try {
|
|
3225
3408
|
await cmdOnboard({ pick: true });
|
|
3226
3409
|
} catch (e) {
|
|
3410
|
+
// Don't kill the process — the setup wizard is often called
|
|
3411
|
+
// from inside cmdLauncher's loop, and a process.exit there
|
|
3412
|
+
// would close the launcher entirely (the surface bug the
|
|
3413
|
+
// user reported as "Setup 누르고 엔터 누르니까 바로 꺼져").
|
|
3414
|
+
// Surface the error and let the caller decide.
|
|
3227
3415
|
process.stderr.write(`onboard error: ${e?.message || e}\n`);
|
|
3228
|
-
|
|
3416
|
+
return;
|
|
3229
3417
|
}
|
|
3230
3418
|
// Re-read config after onboard wrote it. If the user aborted with
|
|
3231
3419
|
// no provider set, bail out early — the rest of the wizard depends
|
|
3232
|
-
// on a provider being configured.
|
|
3420
|
+
// on a provider being configured. `return` (not process.exit) so a
|
|
3421
|
+
// launcher caller can re-prompt or fall back gracefully.
|
|
3233
3422
|
const cfgAfterOnboard = readConfig();
|
|
3234
3423
|
if (!cfgAfterOnboard.provider) {
|
|
3235
|
-
process.stdout.write(`\n ${warn('Setup
|
|
3236
|
-
process.
|
|
3424
|
+
process.stdout.write(`\n ${warn('Setup not completed — provider was not configured.')}\n`);
|
|
3425
|
+
process.stdout.write(` ${dim('Run `lazyclaw setup` again when ready, or pick "Onboard" from the menu for a single-step picker.')}\n\n`);
|
|
3426
|
+
return;
|
|
3237
3427
|
}
|
|
3238
3428
|
process.stdout.write(`\n ${ok('✓ provider:')} ${cfgAfterOnboard.provider} ${dim('model:')} ${cfgAfterOnboard.model || '(default)'}\n\n`);
|
|
3239
3429
|
|
|
@@ -3360,32 +3550,81 @@ async function _runFirstTimeOnboard() {
|
|
|
3360
3550
|
process.stdout.write('\n');
|
|
3361
3551
|
}
|
|
3362
3552
|
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3553
|
+
// Marker exception used by the launcher's process.exit guard. See
|
|
3554
|
+
// _dispatchMenuChoice below for why intercepting process.exit is
|
|
3555
|
+
// the cleanest way to keep the menu loop alive.
|
|
3556
|
+
class _DispatchExit extends Error {
|
|
3557
|
+
constructor(code) {
|
|
3558
|
+
super(`subcommand requested exit ${code}`);
|
|
3559
|
+
this.name = 'DispatchExit';
|
|
3560
|
+
this.exitCode = Number.isFinite(code) ? code : 0;
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
// Direct dispatch from a launcher pick. Replaces the previous
|
|
3565
|
+
// `process.argv = [...]; await main()` round-trip so we can reuse
|
|
3566
|
+
// the launcher across multiple iterations without compounding
|
|
3567
|
+
// state.
|
|
3568
|
+
//
|
|
3569
|
+
// Subcommand functions across this CLI freely call `process.exit()`
|
|
3570
|
+
// to signal their result — perfectly fine for one-shot CLI use,
|
|
3571
|
+
// fatal to a launcher loop because the first exit kills the whole
|
|
3572
|
+
// process before we can redraw the menu. Intercept process.exit for
|
|
3573
|
+
// the duration of the dispatch and turn it into a thrown exception
|
|
3574
|
+
// the loop can catch + log + continue from. This mirrors how Python
|
|
3575
|
+
// CLI frameworks handle SystemExit when running inside a REPL.
|
|
3576
|
+
async function _dispatchMenuChoice(argv) {
|
|
3577
|
+
const sub = argv[0];
|
|
3578
|
+
const rest = argv.slice(1);
|
|
3579
|
+
const realExit = process.exit.bind(process);
|
|
3580
|
+
process.exit = (code) => { throw new _DispatchExit(code); };
|
|
3581
|
+
try {
|
|
3582
|
+
switch (sub) {
|
|
3583
|
+
case 'chat': return await cmdChat({});
|
|
3584
|
+
case 'agent': return await cmdAgent(rest[0] || '-', {});
|
|
3585
|
+
case 'onboard': return await cmdOnboard({});
|
|
3586
|
+
case 'setup': return await cmdSetup(undefined, rest, {});
|
|
3587
|
+
case 'workspace': return await cmdWorkspace(rest[0], rest.slice(1), {});
|
|
3588
|
+
case 'browse': return await cmdBrowse(rest[0], {});
|
|
3589
|
+
case 'skills': return await cmdSkills(rest[0], rest.slice(1), {});
|
|
3590
|
+
case 'sessions': return await cmdSessions(rest[0], rest.slice(1), {});
|
|
3591
|
+
case 'providers': return await cmdProviders(rest[0], rest.slice(1), {});
|
|
3592
|
+
case 'cron': return await cmdCron(rest[0], rest.slice(1), {});
|
|
3593
|
+
case 'auth': return await cmdAuth(rest[0], rest.slice(1), {});
|
|
3594
|
+
case 'pairing': return await cmdPairing(rest[0], rest.slice(1), {});
|
|
3595
|
+
case 'nodes': return await cmdNodes(rest[0], rest.slice(1), {});
|
|
3596
|
+
case 'message': return await cmdMessage(rest[0], rest.slice(1), {});
|
|
3597
|
+
case 'doctor': return await cmdDoctor();
|
|
3598
|
+
case 'status': return await cmdStatus();
|
|
3599
|
+
case 'help': return cmdHelp();
|
|
3600
|
+
case 'dashboard': return await cmdDashboard({});
|
|
3601
|
+
default: throw new Error(`unknown menu choice: ${sub}`);
|
|
3602
|
+
}
|
|
3603
|
+
} catch (e) {
|
|
3604
|
+
if (e instanceof _DispatchExit) {
|
|
3605
|
+
// Subcommand wanted to exit. Surface a non-zero code so the
|
|
3606
|
+
// user knows something flagged, but DON'T propagate — we want
|
|
3607
|
+
// the launcher loop to continue.
|
|
3608
|
+
if (e.exitCode !== 0) {
|
|
3609
|
+
process.stderr.write(` \x1b[2m(subcommand returned exit code ${e.exitCode})\x1b[0m\n`);
|
|
3610
|
+
}
|
|
3611
|
+
return;
|
|
3382
3612
|
}
|
|
3613
|
+
throw e;
|
|
3614
|
+
} finally {
|
|
3615
|
+
process.exit = realExit;
|
|
3383
3616
|
}
|
|
3384
|
-
|
|
3385
|
-
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
async function cmdLauncher() {
|
|
3620
|
+
await ensureRegistry();
|
|
3621
|
+
// Item table is fixed across iterations — only the dispatcher and
|
|
3622
|
+
// the per-iteration draw redraw on each loop tick.
|
|
3386
3623
|
const items = [
|
|
3387
3624
|
{ id: 'chat', label: 'Chat', desc: 'interactive REPL with the configured provider', argv: ['chat'] },
|
|
3388
3625
|
{ id: 'agent', label: 'Agent', desc: 'one-shot prompt — read text and exit', argv: ['agent'], promptForBody: true },
|
|
3626
|
+
{ id: 'dashboard', label: 'Dashboard', desc: 'open the lazyclaw web UI in your browser', argv: ['dashboard'] },
|
|
3627
|
+
{ id: 'setup', label: 'Setup', desc: 'multi-step provider / workspace / skill wizard', argv: ['setup'] },
|
|
3389
3628
|
{ id: 'onboard', label: 'Onboard', desc: 'pick provider / model / api-key', argv: ['onboard'] },
|
|
3390
3629
|
{ id: 'workspace', label: 'Workspace', desc: 'AGENTS.md / SOUL.md / TOOLS.md prompt bundles', argv: ['workspace', 'list'] },
|
|
3391
3630
|
{ id: 'browse', label: 'Browse', desc: 'fetch a URL → markdown', argv: ['browse'], promptForUrl: true },
|
|
@@ -3396,97 +3635,131 @@ async function cmdLauncher() {
|
|
|
3396
3635
|
{ id: 'doctor', label: 'Doctor', desc: 'diagnostic — config, providers, workflows', argv: ['doctor'] },
|
|
3397
3636
|
{ id: 'status', label: 'Status', desc: 'current provider / model / masked key', argv: ['status'] },
|
|
3398
3637
|
{ id: 'help', label: 'Help', desc: 'one-line summary of every subcommand', argv: ['help'] },
|
|
3399
|
-
{ id: 'quit', label: 'Quit', desc: 'exit
|
|
3638
|
+
{ id: 'quit', label: 'Quit', desc: 'exit lazyclaw', argv: null },
|
|
3400
3639
|
];
|
|
3401
3640
|
|
|
3402
|
-
const readline = await import('node:readline');
|
|
3403
|
-
readline.emitKeypressEvents(process.stdin);
|
|
3404
|
-
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
3405
|
-
let idx = 0;
|
|
3406
|
-
|
|
3407
|
-
// Pretty header — same accent palette as _printChatBanner so
|
|
3408
|
-
// returning users recognise it.
|
|
3409
3641
|
const accent = (s) => `\x1b[38;5;208m${s}\x1b[0m`;
|
|
3410
3642
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
3411
3643
|
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
3412
3644
|
const ok = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
3413
3645
|
const warn = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
3414
3646
|
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
process.stdout.write(`${marker}${lbl} ${dim(it.desc)}\n`);
|
|
3647
|
+
let idx = 0;
|
|
3648
|
+
// Outer loop — each iteration is one menu render → pick →
|
|
3649
|
+
// dispatch round. Subcommand return drops back here and the menu
|
|
3650
|
+
// is redrawn. Quit / Esc / Ctrl-C breaks the loop and returns,
|
|
3651
|
+
// which lets the calling main() exit naturally (no process.exit
|
|
3652
|
+
// so the buffered writer / open file descriptors close cleanly).
|
|
3653
|
+
while (true) {
|
|
3654
|
+
// First-run / config-missing guard: a fresh install has no
|
|
3655
|
+
// `provider` set, so any menu pick that calls a provider would
|
|
3656
|
+
// error halfway through. Funnel through cmdSetup before
|
|
3657
|
+
// rendering the menu the first time around.
|
|
3658
|
+
let cfg = readConfig();
|
|
3659
|
+
if (!cfg.provider) {
|
|
3660
|
+
try { await cmdSetup(undefined, [], {}); }
|
|
3661
|
+
catch (e) {
|
|
3662
|
+
process.stderr.write(`setup error: ${e?.message || e}\n`);
|
|
3663
|
+
}
|
|
3664
|
+
cfg = readConfig();
|
|
3665
|
+
if (!cfg.provider) {
|
|
3666
|
+
process.stdout.write('\n Setup not completed — exiting.\n Run `lazyclaw setup` when ready, then try `lazyclaw` again.\n\n');
|
|
3667
|
+
return;
|
|
3668
|
+
}
|
|
3438
3669
|
}
|
|
3439
|
-
|
|
3440
|
-
|
|
3670
|
+
const provider = cfg.provider;
|
|
3671
|
+
const model = cfg.model || '(default)';
|
|
3441
3672
|
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
process.
|
|
3449
|
-
process.
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3673
|
+
// Re-establish stdin in raw / ref'd mode. A previous iteration
|
|
3674
|
+
// (e.g. `chat`) deliberately paused + unref'd stdin in its
|
|
3675
|
+
// exit-cleanup path so the process could end on /exit; now that
|
|
3676
|
+
// we want to keep going, re-attach.
|
|
3677
|
+
const readline = await import('node:readline');
|
|
3678
|
+
readline.emitKeypressEvents(process.stdin);
|
|
3679
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
3680
|
+
process.stdin.resume();
|
|
3681
|
+
process.stdin.ref();
|
|
3682
|
+
|
|
3683
|
+
const draw = () => {
|
|
3684
|
+
process.stdout.write('\x1b[?25l\x1b[2J\x1b[H'); // hide cursor + clear
|
|
3685
|
+
_renderBanner(readVersionFromRepo()).forEach((l) => process.stdout.write(l + '\n'));
|
|
3686
|
+
process.stdout.write('\n');
|
|
3687
|
+
process.stdout.write(` ${dim('provider ·')} ${ok(provider)}\n`);
|
|
3688
|
+
process.stdout.write(` ${dim('model ·')} ${ok(model)}\n`);
|
|
3689
|
+
process.stdout.write(` ${dim('config ·')} ${dim(configPath())}\n`);
|
|
3690
|
+
process.stdout.write('\n');
|
|
3691
|
+
process.stdout.write(` ${dim('↑/↓ to move · Enter to select · q or Esc to quit')}\n\n`);
|
|
3692
|
+
const rowsAvail = Math.max(items.length, (process.stdout.rows || 30) - 14);
|
|
3693
|
+
const fromIdx = Math.max(0, Math.min(items.length - rowsAvail, idx - Math.floor(rowsAvail / 2)));
|
|
3694
|
+
const toIdx = Math.min(items.length, fromIdx + rowsAvail);
|
|
3695
|
+
for (let i = fromIdx; i < toIdx; i++) {
|
|
3696
|
+
const it = items[i];
|
|
3697
|
+
const marker = i === idx ? accent('❯ ') : ' ';
|
|
3698
|
+
const lbl = i === idx ? bold(it.label.padEnd(11)) : it.label.padEnd(11);
|
|
3699
|
+
process.stdout.write(`${marker}${lbl} ${dim(it.desc)}\n`);
|
|
3700
|
+
}
|
|
3701
|
+
process.stdout.write('\n');
|
|
3465
3702
|
};
|
|
3466
|
-
process.stdin.on('keypress', onKey);
|
|
3467
|
-
});
|
|
3468
3703
|
|
|
3469
|
-
|
|
3470
|
-
|
|
3704
|
+
draw();
|
|
3705
|
+
const picked = await new Promise((resolve) => {
|
|
3706
|
+
const onKey = (_str, key) => {
|
|
3707
|
+
if (!key) return;
|
|
3708
|
+
if (key.name === 'up') { idx = (idx - 1 + items.length) % items.length; draw(); }
|
|
3709
|
+
else if (key.name === 'down') { idx = (idx + 1) % items.length; draw(); }
|
|
3710
|
+
else if (key.name === 'home') { idx = 0; draw(); }
|
|
3711
|
+
else if (key.name === 'end') { idx = items.length - 1; draw(); }
|
|
3712
|
+
else if (key.name === 'pageup') { idx = Math.max(0, idx - 5); draw(); }
|
|
3713
|
+
else if (key.name === 'pagedown') { idx = Math.min(items.length - 1, idx + 5); draw(); }
|
|
3714
|
+
else if (key.name === 'return') { cleanup(); resolve(items[idx]); }
|
|
3715
|
+
else if (key.ctrl && key.name === 'c') { cleanup(); resolve({ id: 'quit', argv: null }); }
|
|
3716
|
+
else if (key.name === 'escape' || key.name === 'q') { cleanup(); resolve({ id: 'quit', argv: null }); }
|
|
3717
|
+
function cleanup() {
|
|
3718
|
+
process.stdin.off('keypress', onKey);
|
|
3719
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(false);
|
|
3720
|
+
process.stdout.write('\x1b[?25h\x1b[2J\x1b[H');
|
|
3721
|
+
}
|
|
3722
|
+
};
|
|
3723
|
+
process.stdin.on('keypress', onKey);
|
|
3724
|
+
});
|
|
3725
|
+
|
|
3726
|
+
if (!picked || picked.id === 'quit' || !picked.argv) {
|
|
3727
|
+
// Plain return so main() can exit naturally.
|
|
3728
|
+
return;
|
|
3729
|
+
}
|
|
3730
|
+
|
|
3731
|
+
// Two menu items need a follow-up question before they can run:
|
|
3732
|
+
// agent (prompt body), browse (URL). Ask once, then dispatch.
|
|
3733
|
+
let argv = picked.argv;
|
|
3734
|
+
if (picked.promptForBody) {
|
|
3735
|
+
const body = await _quickPrompt('prompt: ');
|
|
3736
|
+
if (!body) continue; // back to menu
|
|
3737
|
+
argv = ['agent', body];
|
|
3738
|
+
} else if (picked.promptForUrl) {
|
|
3739
|
+
const url = await _quickPrompt('url: ');
|
|
3740
|
+
if (!url) continue; // back to menu
|
|
3741
|
+
argv = ['browse', url];
|
|
3742
|
+
}
|
|
3743
|
+
|
|
3744
|
+
// Dispatch. Errors don't terminate the launcher — they're
|
|
3745
|
+
// surfaced as a stderr line and the menu redraws. Lets the
|
|
3746
|
+
// user recover from a transient API hiccup without a relaunch.
|
|
3747
|
+
try {
|
|
3748
|
+
await _dispatchMenuChoice(argv);
|
|
3749
|
+
} catch (e) {
|
|
3750
|
+
process.stderr.write(`\n ${accent('✗')} ${e?.message || String(e)}\n`);
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3753
|
+
// Pause before re-drawing so the user can read the subcommand's
|
|
3754
|
+
// output. `chat` is the special case: its REPL has already kept
|
|
3755
|
+
// the user oriented for a long session, and they typed /exit
|
|
3756
|
+
// explicitly, so jumping straight back to the menu reads as
|
|
3757
|
+
// "ok, done with that conversation, back to the dashboard."
|
|
3758
|
+
if (picked.id !== 'chat') {
|
|
3759
|
+
process.stdout.write('\n');
|
|
3760
|
+
await _quickPrompt(` ${dim('Press Enter to return to the menu… ')}`);
|
|
3761
|
+
}
|
|
3471
3762
|
}
|
|
3472
|
-
// Two surfaces need a follow-up question before they can run:
|
|
3473
|
-
// - `agent`: needs a prompt body
|
|
3474
|
-
// - `browse`: needs a URL
|
|
3475
|
-
// Ask via a simple readline prompt so the launcher stays
|
|
3476
|
-
// self-contained instead of forwarding into a half-typed argv.
|
|
3477
|
-
if (picked.promptForBody) {
|
|
3478
|
-
const body = await _quickPrompt('prompt: ');
|
|
3479
|
-
if (!body) process.exit(0);
|
|
3480
|
-
picked.argv = ['agent', body];
|
|
3481
|
-
} else if (picked.promptForUrl) {
|
|
3482
|
-
const url = await _quickPrompt('url: ');
|
|
3483
|
-
if (!url) process.exit(0);
|
|
3484
|
-
picked.argv = ['browse', url];
|
|
3485
|
-
}
|
|
3486
|
-
// Replace argv and re-enter main(). The chosen subcommand sees
|
|
3487
|
-
// the same parser surface as if the user had typed it directly.
|
|
3488
|
-
process.argv = [process.argv[0], process.argv[1], ...picked.argv];
|
|
3489
|
-
await main();
|
|
3490
3763
|
}
|
|
3491
3764
|
|
|
3492
3765
|
async function _quickPrompt(label) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lazyclaw",
|
|
3
|
-
"version": "3.99.
|
|
3
|
+
"version": "3.99.9",
|
|
4
4
|
"description": "Lazy, elegant terminal CLI for chatting with Claude / OpenAI / Gemini / Ollama and orchestrating multi-step LLM workflows. Banner-on-launch, slash-command ghost autocomplete, persistent sessions, local HTTP gateway.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|