lazyclaw 6.2.0 → 6.3.1
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/commands/chat.mjs +22 -22
- package/commands/config_step.mjs +39 -0
- package/package.json +1 -1
- package/tui/config_picker.mjs +54 -0
- package/tui/editor.mjs +17 -1
- package/tui/pickers.mjs +14 -15
- package/tui/slash_commands.mjs +2 -1
- package/tui/slash_dispatcher.mjs +8 -9
package/commands/chat.mjs
CHANGED
|
@@ -23,17 +23,16 @@ import { SLASH_COMMANDS } from '../tui/slash_commands.mjs';
|
|
|
23
23
|
|
|
24
24
|
// Legacy (non-Ink) slash routing for dispatcher-style, ctx-only commands.
|
|
25
25
|
// The Ink REPL routes every slash through _dispatchSlash/SLASH_HANDLERS, but
|
|
26
|
-
// the legacy readline path uses a hand-written switch (in cmdChat).
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
// drive — this is the seam that proves /config reaches setup on the legacy
|
|
30
|
-
// path (the post-loop guard re-runs cmdSetup when ctx.requestSetup is set).
|
|
31
|
-
// Returns 'EXIT' when the loop must break, or undefined when the command is
|
|
32
|
-
// not one this helper owns (caller falls through to its own handling).
|
|
26
|
+
// the legacy readline path uses a hand-written switch (in cmdChat). This
|
|
27
|
+
// exported helper is the wiring BOTH that switch and the regression test
|
|
28
|
+
// drive. Returns 'EXIT' to break the loop, undefined when not owned here.
|
|
33
29
|
export function legacySlashRoute(cmd, ctx) {
|
|
34
30
|
switch (cmd) {
|
|
31
|
+
// Legacy readline path has no modal picker, so BOTH /setup and /config
|
|
32
|
+
// route to the full wizard here (the Ink path gives /config its
|
|
33
|
+
// single-setting picker via tui/config_picker.mjs).
|
|
35
34
|
case '/config':
|
|
36
|
-
|
|
35
|
+
case '/setup':
|
|
37
36
|
ctx.requestSetup = true;
|
|
38
37
|
return 'EXIT';
|
|
39
38
|
default:
|
|
@@ -297,18 +296,23 @@ export async function cmdChat(flags = {}) {
|
|
|
297
296
|
pickerRef: _inkPickerRef,
|
|
298
297
|
}), { exitOnCtrlC: true, patchConsole: true });
|
|
299
298
|
await ink.waitUntilExit();
|
|
300
|
-
// /
|
|
301
|
-
//
|
|
299
|
+
// /setup → full wizard (then shell). /config single step → run JUST
|
|
300
|
+
// that step now that Ink released stdin, then re-enter chat.
|
|
302
301
|
if (_inkCtx.requestSetup) await (await import('./setup.mjs')).cmdSetup(undefined, [], {});
|
|
302
|
+
else if (_inkCtx.requestConfigStep) {
|
|
303
|
+
await (await import('./config_step.mjs')).runConfigStep(_inkCtx.requestConfigStep);
|
|
304
|
+
return cmdChat(flags);
|
|
305
|
+
}
|
|
303
306
|
return;
|
|
304
307
|
} catch (e) {
|
|
305
|
-
// Fall through to legacy path on any ink failure
|
|
306
|
-
//
|
|
307
|
-
|
|
308
|
+
// Fall through to the legacy readline path on any ink failure. ALWAYS
|
|
309
|
+
// say why, in one dim line — the silent downgrade made real-terminal
|
|
310
|
+
// failures (node incompat, <60-col windows) undiagnosable from reports.
|
|
311
|
+
process.stderr.write(`\x1b[2m(ink UI unavailable: ${e?.message || e} — using the legacy reader)\x1b[0m\n`);
|
|
308
312
|
}
|
|
309
313
|
}
|
|
310
314
|
// ─── legacy v4 path (unchanged) ─────────────────────────────────
|
|
311
|
-
_printChatBanner(activeProvName, activeModel, readVersionFromRepo());
|
|
315
|
+
await _printChatBanner(activeProvName, activeModel, readVersionFromRepo());
|
|
312
316
|
|
|
313
317
|
const readline = await import('node:readline');
|
|
314
318
|
// Use terminal:true when we're attached to a TTY so the prompt shows
|
|
@@ -1110,14 +1114,10 @@ export async function cmdChat(flags = {}) {
|
|
|
1110
1114
|
} catch { /* /exit must never hang or throw */ }
|
|
1111
1115
|
return 'EXIT';
|
|
1112
1116
|
}
|
|
1113
|
-
case '/config':
|
|
1114
|
-
|
|
1115
|
-
// tests/f-config-slash-splash.test.mjs)
|
|
1116
|
-
//
|
|
1117
|
-
// _legacyCtx.requestSetup and returns 'EXIT'; the post-loop guard at the
|
|
1118
|
-
// bottom of cmdChat re-runs the setup wizard when requestSetup is set.
|
|
1119
|
-
// Without this case the legacy readline path fell through to `default:`
|
|
1120
|
-
// and printed "unknown slash" instead of launching setup.
|
|
1117
|
+
case '/config':
|
|
1118
|
+
case '/setup': {
|
|
1119
|
+
// Shared legacySlashRoute wiring (tests/f-config-slash-splash.test.mjs):
|
|
1120
|
+
// sets requestSetup + 'EXIT'; the post-loop guard runs the wizard.
|
|
1121
1121
|
return legacySlashRoute(cmd, _legacyCtx);
|
|
1122
1122
|
}
|
|
1123
1123
|
default: {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// commands/config_step.mjs — run ONE setup step outside the full wizard.
|
|
2
|
+
//
|
|
3
|
+
// Backs the in-chat `/config` picker: credential steps (channel tokens,
|
|
4
|
+
// outbound webhook) need raw readline prompts that can't run inside the Ink
|
|
5
|
+
// REPL, so the REPL unmounts with ctx.requestConfigStep set, chat.mjs calls
|
|
6
|
+
// runConfigStep(step) here, and then re-enters chat — the user changes one
|
|
7
|
+
// value (e.g. a webhook URL) without re-walking every wizard step.
|
|
8
|
+
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { configPath } from '../lib/config.mjs';
|
|
11
|
+
import { _quickPrompt } from '../tui/pickers.mjs';
|
|
12
|
+
import { runChannelStep, runWebhookStep } from './setup_channels.mjs';
|
|
13
|
+
|
|
14
|
+
const COLORS = {
|
|
15
|
+
accent: (s) => `\x1b[38;2;217;179;90m${s}\x1b[0m`,
|
|
16
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
17
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
18
|
+
ok: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
19
|
+
warn: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export async function runConfigStep(step, deps = {}) {
|
|
23
|
+
const prompt = deps.prompt || _quickPrompt;
|
|
24
|
+
const colors = deps.colors || COLORS;
|
|
25
|
+
const write = deps.write || ((s) => process.stdout.write(s));
|
|
26
|
+
const cfgDir = deps.cfgDir || path.dirname(configPath());
|
|
27
|
+
|
|
28
|
+
write(`\n ${colors.bold(`⚙ config — ${step}`)}\n`);
|
|
29
|
+
if (step === 'channel') {
|
|
30
|
+
await runChannelStep({ cfgDir, prompt, colors, write });
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
if (step === 'webhook') {
|
|
34
|
+
await runWebhookStep({ prompt, colors, write });
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
write(` ${colors.warn(`unknown config step: ${step}`)}\n`);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lazyclaw",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.1",
|
|
4
4
|
"description": "Lazy, elegant terminal CLI for chatting with Claude / OpenAI / Gemini / Ollama, orchestrating multi-step LLM workflows, and running multi-agent Slack teams with cross-task memory. Banner-on-launch, slash-command ghost autocomplete, persistent sessions, local HTTP gateway.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// tui/config_picker.mjs — the `/config` slash: change ONE setting without
|
|
2
|
+
// re-running the whole wizard.
|
|
3
|
+
//
|
|
4
|
+
// Split of duties (user-requested):
|
|
5
|
+
// /setup — first-run / full re-setup: leaves chat and runs EVERY wizard
|
|
6
|
+
// step (the behavior /config used to have).
|
|
7
|
+
// /config — settings editor: pick a single item. In-chat items (provider /
|
|
8
|
+
// model / context / trainer / orchestrator) delegate to their
|
|
9
|
+
// existing slash handlers and stay inside chat; credential items
|
|
10
|
+
// (channel tokens, outbound webhook) need readline prompts, so
|
|
11
|
+
// they unmount, run JUST that step, and re-enter chat.
|
|
12
|
+
//
|
|
13
|
+
// On the legacy readline path (no ctx.openPicker modal) /config falls back
|
|
14
|
+
// to the full wizard — same as before, no silent degradation.
|
|
15
|
+
|
|
16
|
+
const CONFIG_ITEMS = [
|
|
17
|
+
{ id: 'provider', label: 'provider', desc: 'switch the chat provider (family → vendor picker)' },
|
|
18
|
+
{ id: 'model', label: 'model', desc: 'switch the model (live list when the provider supports it)' },
|
|
19
|
+
{ id: 'context', label: 'context window', desc: 'history turns / token budget sent per turn' },
|
|
20
|
+
{ id: 'trainer', label: 'trainer', desc: 'learning-loop provider/model (auto = $0 on claude-cli)' },
|
|
21
|
+
{ id: 'orchestrator', label: 'orchestrator', desc: 'multi-agent on/off, planner, workers' },
|
|
22
|
+
{ id: 'channel', label: 'channel credentials', desc: 'Slack/Telegram/Matrix tokens — leaves chat for the prompts, then returns' },
|
|
23
|
+
{ id: 'webhook', label: 'outbound webhook', desc: 'message-send webhook URL — leaves chat, then returns' },
|
|
24
|
+
{ id: 'wizard', label: 'everything (full wizard)', desc: 'rerun all setup steps — same as /setup' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export async function runConfigSlash(_args, ctx, handlers) {
|
|
28
|
+
if (typeof ctx.openPicker !== 'function') {
|
|
29
|
+
// Legacy readline path has no modal picker — keep the old /config
|
|
30
|
+
// behavior there (full wizard) rather than failing.
|
|
31
|
+
ctx.requestSetup = true;
|
|
32
|
+
return 'EXIT';
|
|
33
|
+
}
|
|
34
|
+
const picked = await ctx.openPicker({
|
|
35
|
+
kind: 'config-item',
|
|
36
|
+
title: 'config — change one setting',
|
|
37
|
+
subtitle: 'Enter to edit · Esc to cancel · /setup reruns the whole wizard',
|
|
38
|
+
items: CONFIG_ITEMS.map((i) => ({ id: i.id, label: i.label, desc: i.desc })),
|
|
39
|
+
});
|
|
40
|
+
const id = typeof picked === 'string' ? picked : (picked && picked.id);
|
|
41
|
+
if (!id || id === 'CANCEL') return 'config: cancelled';
|
|
42
|
+
if (id === 'wizard') { ctx.requestSetup = true; return 'EXIT'; }
|
|
43
|
+
if (id === 'channel' || id === 'webhook') {
|
|
44
|
+
// These steps need raw readline prompts (secrets), so the REPL unmounts,
|
|
45
|
+
// chat.mjs runs the single step, and chat restarts automatically.
|
|
46
|
+
ctx.requestConfigStep = id;
|
|
47
|
+
return 'EXIT';
|
|
48
|
+
}
|
|
49
|
+
const handler = handlers && handlers.get && handlers.get(`/${id}`);
|
|
50
|
+
if (!handler) return `config: no in-chat editor for "${id}"`;
|
|
51
|
+
return handler('', ctx);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { CONFIG_ITEMS };
|
package/tui/editor.mjs
CHANGED
|
@@ -422,7 +422,12 @@ export function Editor({
|
|
|
422
422
|
try {
|
|
423
423
|
process.stdout.write(`${undo}\x1b[${rowsUp}A\x1b[${colTarget}G\x1b[?25h`);
|
|
424
424
|
} catch { /* stdout closed — swallow */ }
|
|
425
|
-
}
|
|
425
|
+
});
|
|
426
|
+
// ^ NO dependency array — re-anchor after EVERY commit. Renders triggered
|
|
427
|
+
// by OTHER components (status-bar ticks, streaming output) redraw the
|
|
428
|
+
// frame and park the terminal cursor below it; with buffer-only deps the
|
|
429
|
+
// anchor didn't re-run and the cursor drifted out of the box whenever the
|
|
430
|
+
// user wasn't typing. The pending-offset undo above makes repeats safe.
|
|
426
431
|
|
|
427
432
|
const lines = state.buffer.split('\n');
|
|
428
433
|
// Ink's <Text wrap="wrap"> uses wrap-ansi (string-width aware) but the
|
|
@@ -446,6 +451,17 @@ export function Editor({
|
|
|
446
451
|
});
|
|
447
452
|
}
|
|
448
453
|
}
|
|
454
|
+
// Always-visible caret: an inverse-video cell drawn AT the cursor (the
|
|
455
|
+
// editor has no mid-line cursor movement, so the cursor is always at the
|
|
456
|
+
// end of the buffer — including column 0 of an empty box). The real
|
|
457
|
+
// terminal cursor is anchored to the same cell for IME pre-edit; this
|
|
458
|
+
// glyph keeps the position visible even between anchor writes (e.g.
|
|
459
|
+
// while another component renders). Hidden while a modal picker owns
|
|
460
|
+
// the keyboard so it can't masquerade as an active prompt.
|
|
461
|
+
if (!modalOpen && renderedLines.length > 0) {
|
|
462
|
+
const last = renderedLines[renderedLines.length - 1];
|
|
463
|
+
last.text = `${last.text}\x1b[7m \x1b[27m`;
|
|
464
|
+
}
|
|
449
465
|
return React.createElement(
|
|
450
466
|
Box,
|
|
451
467
|
{
|
package/tui/pickers.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// Interactive TUI helpers — readline pickers, banner
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// are siblings (./banner.generated.mjs, ./wordmark.mjs).
|
|
1
|
+
// Interactive TUI helpers — readline pickers, banner renderers, arrow-key
|
|
2
|
+
// menu, provider/model selection, _quickPrompt. Extracted from cli.mjs (D4);
|
|
3
|
+
// lives in tui/ so banner asset imports are siblings.
|
|
5
4
|
import { readConfig, writeConfig, _resolveAuthKey } from '../lib/config.mjs';
|
|
5
|
+
import { SLASH_COMMANDS } from './slash_commands.mjs';
|
|
6
6
|
import { ensureRegistry, getRegistry } from '../lib/registry_boot.mjs';
|
|
7
7
|
import { bucketProviders as _bucketProviders } from './provider_families.mjs';
|
|
8
8
|
import { addCustomProvider } from '../providers/custom_provider.mjs';
|
|
@@ -13,13 +13,11 @@ import {
|
|
|
13
13
|
} from '../providers/model_catalogue.mjs';
|
|
14
14
|
|
|
15
15
|
export function _attachGhostAutocomplete(rl) {
|
|
16
|
-
// Returns `{ dispose, suspend, resume }`. Dispose detaches the
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// ghost-render escapes — that interleaving is what surfaces as
|
|
22
|
-
// visible gaps between Korean characters in long replies.
|
|
16
|
+
// Returns `{ dispose, suspend, resume }`. Dispose detaches the keypress +
|
|
17
|
+
// rl 'line' listeners (leaking them is the v3.92 slow-exit bug). Suspend /
|
|
18
|
+
// resume gate the keypress handler so streaming chat output isn't
|
|
19
|
+
// interleaved with `\x1b[s\x1b[K\x1b[u` ghost-render escapes — that
|
|
20
|
+
// interleaving surfaced as visible gaps between Korean characters.
|
|
23
21
|
const noop = () => {};
|
|
24
22
|
if (!process.stdout.isTTY) return { dispose: noop, suspend: noop, resume: noop };
|
|
25
23
|
const cmds = SLASH_COMMANDS.map((c) => c.cmd);
|
|
@@ -219,15 +217,16 @@ export async function _renderV5Banner(version) {
|
|
|
219
217
|
return rows;
|
|
220
218
|
}
|
|
221
219
|
|
|
222
|
-
export function _printChatBanner(activeProvName, activeModel, version) {
|
|
220
|
+
export async function _printChatBanner(activeProvName, activeModel, version) {
|
|
223
221
|
if (!process.stdout.isTTY) return;
|
|
224
|
-
// Single-hue header: labels dim-orange, values/emphasis full-orange
|
|
225
|
-
//
|
|
222
|
+
// Single-hue header: labels dim-orange, values/emphasis full-orange. Uses
|
|
223
|
+
// the v5 sloth splash (NOT the retired v4 figlet box — see _renderV5Banner;
|
|
224
|
+
// figlet remains only as the missing-asset last resort).
|
|
226
225
|
const dimOrange = (s) => `\x1b[2m\x1b[38;2;${_ORANGE_RGB}m${s}\x1b[0m`;
|
|
227
226
|
const orange = _orange;
|
|
228
227
|
const lines = [
|
|
229
228
|
'',
|
|
230
|
-
...
|
|
229
|
+
...(await _renderV5Banner(version)),
|
|
231
230
|
'',
|
|
232
231
|
` ${dimOrange('provider ·')} ${orange(activeProvName)}`,
|
|
233
232
|
` ${dimOrange('model ·')} ${orange(activeModel || '(default)')}`,
|
package/tui/slash_commands.mjs
CHANGED
|
@@ -26,7 +26,8 @@ export const SLASH_COMMANDS = [
|
|
|
26
26
|
{ cmd: '/personality', help: 'pick a personality (or sub: list|show|install|remove|use)' },
|
|
27
27
|
{ cmd: '/dashboard', help: 'open the lazyclaw web UI in your browser' },
|
|
28
28
|
{ cmd: '/menu', help: 'browse the full subcommand catalog (command palette)' },
|
|
29
|
-
{ cmd: '/
|
|
29
|
+
{ cmd: '/setup', help: 'first-run / full re-setup: leave chat and run every wizard step' },
|
|
30
|
+
{ cmd: '/config', help: 'change ONE setting: provider, model, context, channel creds, webhook, …' },
|
|
30
31
|
{ cmd: '/channels', help: 'view configured channels; /channels <name> on|off to toggle' },
|
|
31
32
|
{ cmd: '/orchestrator', help: 'multi-agent: status | on | off | planner <spec> | worker add|remove <spec>' },
|
|
32
33
|
{ cmd: '/context', help: 'chat history window: status | turns <N> | tokens <N>' },
|
package/tui/slash_dispatcher.mjs
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// tui/slash_dispatcher.mjs — single source of slash-command routing for the
|
|
2
2
|
// Ink chat REPL (v5.4). Lifted from cli.mjs's legacy readline handler so the
|
|
3
|
-
// Ink branch
|
|
4
|
-
// channels (Slack/Telegram inline commands, /handoff cross-channel control)
|
|
5
|
-
// can share one dispatch table.
|
|
3
|
+
// Ink branch and future channel surfaces share one dispatch table.
|
|
6
4
|
//
|
|
7
5
|
// Contract:
|
|
8
6
|
// dispatchSlash(cmd, args, ctx, write) → Promise<string|'EXIT'|void>
|
|
@@ -1814,17 +1812,18 @@ export const SLASH_HANDLERS = new Map([
|
|
|
1814
1812
|
['/channels', _channels],
|
|
1815
1813
|
['/orchestrator', _orchestrator],
|
|
1816
1814
|
['/context', _context],
|
|
1817
|
-
// /
|
|
1818
|
-
|
|
1815
|
+
// /setup — full wizard (every step); /config — pick ONE setting to change
|
|
1816
|
+
// (in-chat where possible; credential steps unmount, run, re-enter chat).
|
|
1817
|
+
['/setup', async (_a, ctx) => { ctx.requestSetup = true; return 'EXIT'; }],
|
|
1818
|
+
['/config', async (a, ctx) => (await import('./config_picker.mjs')).runConfigSlash(a, ctx, SLASH_HANDLERS)],
|
|
1819
1819
|
['/exit', async () => 'EXIT'],
|
|
1820
1820
|
['/quit', async () => 'EXIT'],
|
|
1821
1821
|
]);
|
|
1822
1822
|
|
|
1823
1823
|
/**
|
|
1824
|
-
* Primary entry point. Resolves the command name to a
|
|
1825
|
-
*
|
|
1826
|
-
*
|
|
1827
|
-
* so the user sees feedback instead of an error toast.
|
|
1824
|
+
* Primary entry point. Resolves the command name to a SLASH_HANDLERS entry
|
|
1825
|
+
* and invokes it. Unknown commands return a friendly "unknown" string
|
|
1826
|
+
* (rendered to scrollback) rather than throwing.
|
|
1828
1827
|
*/
|
|
1829
1828
|
export async function dispatchSlash(cmd, args, ctx, write) {
|
|
1830
1829
|
const handler = SLASH_HANDLERS.get(cmd);
|