lazyclaw 3.99.8 → 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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/cli.mjs +68 -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.9
75
75
  ╰──────────────────────────────╯
76
76
 
77
77
  provider · anthropic
package/cli.mjs CHANGED
@@ -3407,16 +3407,23 @@ async function cmdSetup(_sub, _positional, flags = {}) {
3407
3407
  try {
3408
3408
  await cmdOnboard({ pick: true });
3409
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.
3410
3415
  process.stderr.write(`onboard error: ${e?.message || e}\n`);
3411
- process.exit(1);
3416
+ return;
3412
3417
  }
3413
3418
  // Re-read config after onboard wrote it. If the user aborted with
3414
3419
  // no provider set, bail out early — the rest of the wizard depends
3415
- // 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.
3416
3422
  const cfgAfterOnboard = readConfig();
3417
3423
  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);
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;
3420
3427
  }
3421
3428
  process.stdout.write(`\n ${ok('✓ provider:')} ${cfgAfterOnboard.provider} ${dim('model:')} ${cfgAfterOnboard.model || '(default)'}\n\n`);
3422
3429
 
@@ -3543,34 +3550,69 @@ async function _runFirstTimeOnboard() {
3543
3550
  process.stdout.write('\n');
3544
3551
  }
3545
3552
 
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
+
3546
3564
  // Direct dispatch from a launcher pick. Replaces the previous
3547
3565
  // `process.argv = [...]; await main()` round-trip so we can reuse
3548
3566
  // 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.
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.
3551
3576
  async function _dispatchMenuChoice(argv) {
3552
3577
  const sub = argv[0];
3553
3578
  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}`);
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;
3612
+ }
3613
+ throw e;
3614
+ } finally {
3615
+ process.exit = realExit;
3574
3616
  }
3575
3617
  }
3576
3618
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lazyclaw",
3
- "version": "3.99.8",
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",