lazyclaw 6.2.0 → 6.3.0
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 +17 -18
- 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/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,9 +296,13 @@ 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
308
|
// Fall through to legacy path on any ink failure (missing import,
|
|
@@ -1110,14 +1113,10 @@ export async function cmdChat(flags = {}) {
|
|
|
1110
1113
|
} catch { /* /exit must never hang or throw */ }
|
|
1111
1114
|
return 'EXIT';
|
|
1112
1115
|
}
|
|
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.
|
|
1116
|
+
case '/config':
|
|
1117
|
+
case '/setup': {
|
|
1118
|
+
// Shared legacySlashRoute wiring (tests/f-config-slash-splash.test.mjs):
|
|
1119
|
+
// sets requestSetup + 'EXIT'; the post-loop guard runs the wizard.
|
|
1121
1120
|
return legacySlashRoute(cmd, _legacyCtx);
|
|
1122
1121
|
}
|
|
1123
1122
|
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.0",
|
|
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/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);
|