lazyclaw 3.99.8 → 3.99.10

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/cli.mjs +84 -26
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -71,7 +71,7 @@ What you see on launch (TTY only):
71
71
  │ | |__ _ _____ _ _ │
72
72
  │ | / _` |_ / || | '_| │
73
73
  │ |_\__,_/__\_, |_| │
74
- │ LazyClaw |__/ 3.99.8
74
+ │ LazyClaw |__/ 3.99.10
75
75
  ╰──────────────────────────────╯
76
76
 
77
77
  provider · anthropic
package/cli.mjs CHANGED
@@ -1521,6 +1521,13 @@ async function _arrowMenu({ title, subtitle, footer, items, defaultIdx = 0 }) {
1521
1521
  const readline = await import('node:readline');
1522
1522
  readline.emitKeypressEvents(process.stdin);
1523
1523
  if (process.stdin.setRawMode) process.stdin.setRawMode(true);
1524
+ // A previous `readline.createInterface(...).close()` (e.g. from
1525
+ // `_quickPrompt`) leaves stdin paused — the keypress listener we
1526
+ // attach below would never fire and the menu would appear frozen
1527
+ // instead of responding to arrow keys. Resume + ref defensively
1528
+ // before drawing so the picker always receives the first keypress.
1529
+ process.stdin.resume();
1530
+ if (process.stdin.ref) process.stdin.ref();
1524
1531
  let idx = Math.max(0, Math.min(items.length - 1, defaultIdx));
1525
1532
  const accent = (s) => `\x1b[38;5;208m${s}\x1b[0m`;
1526
1533
  const dim = (s) => `\x1b[2m${s}\x1b[0m`;
@@ -3407,16 +3414,23 @@ async function cmdSetup(_sub, _positional, flags = {}) {
3407
3414
  try {
3408
3415
  await cmdOnboard({ pick: true });
3409
3416
  } catch (e) {
3417
+ // Don't kill the process — the setup wizard is often called
3418
+ // from inside cmdLauncher's loop, and a process.exit there
3419
+ // would close the launcher entirely (the surface bug the
3420
+ // user reported as "Setup 누르고 엔터 누르니까 바로 꺼져").
3421
+ // Surface the error and let the caller decide.
3410
3422
  process.stderr.write(`onboard error: ${e?.message || e}\n`);
3411
- process.exit(1);
3423
+ return;
3412
3424
  }
3413
3425
  // Re-read config after onboard wrote it. If the user aborted with
3414
3426
  // no provider set, bail out early — the rest of the wizard depends
3415
- // on a provider being configured.
3427
+ // on a provider being configured. `return` (not process.exit) so a
3428
+ // launcher caller can re-prompt or fall back gracefully.
3416
3429
  const cfgAfterOnboard = readConfig();
3417
3430
  if (!cfgAfterOnboard.provider) {
3418
- process.stdout.write(`\n ${warn('Setup abortedno provider configured. Run `lazyclaw setup` again when ready.')}\n\n`);
3419
- process.exit(0);
3431
+ process.stdout.write(`\n ${warn('Setup not completed — provider was not configured.')}\n`);
3432
+ process.stdout.write(` ${dim('Run `lazyclaw setup` again when ready, or pick "Onboard" from the menu for a single-step picker.')}\n\n`);
3433
+ return;
3420
3434
  }
3421
3435
  process.stdout.write(`\n ${ok('✓ provider:')} ${cfgAfterOnboard.provider} ${dim('model:')} ${cfgAfterOnboard.model || '(default)'}\n\n`);
3422
3436
 
@@ -3543,34 +3557,69 @@ async function _runFirstTimeOnboard() {
3543
3557
  process.stdout.write('\n');
3544
3558
  }
3545
3559
 
3560
+ // Marker exception used by the launcher's process.exit guard. See
3561
+ // _dispatchMenuChoice below for why intercepting process.exit is
3562
+ // the cleanest way to keep the menu loop alive.
3563
+ class _DispatchExit extends Error {
3564
+ constructor(code) {
3565
+ super(`subcommand requested exit ${code}`);
3566
+ this.name = 'DispatchExit';
3567
+ this.exitCode = Number.isFinite(code) ? code : 0;
3568
+ }
3569
+ }
3570
+
3546
3571
  // Direct dispatch from a launcher pick. Replaces the previous
3547
3572
  // `process.argv = [...]; await main()` round-trip so we can reuse
3548
3573
  // 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.
3574
+ // state.
3575
+ //
3576
+ // Subcommand functions across this CLI freely call `process.exit()`
3577
+ // to signal their result — perfectly fine for one-shot CLI use,
3578
+ // fatal to a launcher loop because the first exit kills the whole
3579
+ // process before we can redraw the menu. Intercept process.exit for
3580
+ // the duration of the dispatch and turn it into a thrown exception
3581
+ // the loop can catch + log + continue from. This mirrors how Python
3582
+ // CLI frameworks handle SystemExit when running inside a REPL.
3551
3583
  async function _dispatchMenuChoice(argv) {
3552
3584
  const sub = argv[0];
3553
3585
  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}`);
3586
+ const realExit = process.exit.bind(process);
3587
+ process.exit = (code) => { throw new _DispatchExit(code); };
3588
+ try {
3589
+ switch (sub) {
3590
+ case 'chat': return await cmdChat({});
3591
+ case 'agent': return await cmdAgent(rest[0] || '-', {});
3592
+ case 'onboard': return await cmdOnboard({});
3593
+ case 'setup': return await cmdSetup(undefined, rest, {});
3594
+ case 'workspace': return await cmdWorkspace(rest[0], rest.slice(1), {});
3595
+ case 'browse': return await cmdBrowse(rest[0], {});
3596
+ case 'skills': return await cmdSkills(rest[0], rest.slice(1), {});
3597
+ case 'sessions': return await cmdSessions(rest[0], rest.slice(1), {});
3598
+ case 'providers': return await cmdProviders(rest[0], rest.slice(1), {});
3599
+ case 'cron': return await cmdCron(rest[0], rest.slice(1), {});
3600
+ case 'auth': return await cmdAuth(rest[0], rest.slice(1), {});
3601
+ case 'pairing': return await cmdPairing(rest[0], rest.slice(1), {});
3602
+ case 'nodes': return await cmdNodes(rest[0], rest.slice(1), {});
3603
+ case 'message': return await cmdMessage(rest[0], rest.slice(1), {});
3604
+ case 'doctor': return await cmdDoctor();
3605
+ case 'status': return await cmdStatus();
3606
+ case 'help': return cmdHelp();
3607
+ case 'dashboard': return await cmdDashboard({});
3608
+ default: throw new Error(`unknown menu choice: ${sub}`);
3609
+ }
3610
+ } catch (e) {
3611
+ if (e instanceof _DispatchExit) {
3612
+ // Subcommand wanted to exit. Surface a non-zero code so the
3613
+ // user knows something flagged, but DON'T propagate — we want
3614
+ // the launcher loop to continue.
3615
+ if (e.exitCode !== 0) {
3616
+ process.stderr.write(` \x1b[2m(subcommand returned exit code ${e.exitCode})\x1b[0m\n`);
3617
+ }
3618
+ return;
3619
+ }
3620
+ throw e;
3621
+ } finally {
3622
+ process.exit = realExit;
3574
3623
  }
3575
3624
  }
3576
3625
 
@@ -3723,6 +3772,15 @@ async function cmdLauncher() {
3723
3772
  async function _quickPrompt(label) {
3724
3773
  const readline = await import('node:readline');
3725
3774
  process.stdout.write('\n');
3775
+ // Make sure stdin is in cooked / line-buffered mode for the
3776
+ // duration of the prompt — a prior `_arrowMenu` may have left raw
3777
+ // mode on, in which case readline.question() never fires its
3778
+ // line-event because each byte is delivered as a keypress instead.
3779
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
3780
+ try { process.stdin.setRawMode(false); } catch (_) {}
3781
+ }
3782
+ process.stdin.resume();
3783
+ if (process.stdin.ref) process.stdin.ref();
3726
3784
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3727
3785
  const ans = await new Promise((resolve) => rl.question(label, resolve));
3728
3786
  rl.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lazyclaw",
3
- "version": "3.99.8",
3
+ "version": "3.99.10",
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",