lazyclaw 5.4.4 → 6.0.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/channels/handoff.mjs +36 -0
- package/cli.mjs +73 -7399
- package/daemon.mjs +23 -2085
- package/mas/agent_turn.mjs +2 -1
- package/mas/index_db.mjs +82 -0
- package/mas/learning.mjs +17 -1
- package/mas/mention_router.mjs +38 -10
- package/mas/provider_adapters.mjs +28 -4
- package/mas/scrub_env.mjs +34 -0
- package/mas/tool_runner.mjs +23 -7
- package/mas/tools/bash.mjs +10 -5
- package/mas/tools/browser.mjs +18 -0
- package/mas/tools/learning.mjs +24 -14
- package/mas/tools/recall.mjs +5 -1
- package/mas/tools/web.mjs +47 -11
- package/mas/trajectory_store.mjs +7 -4
- package/package.json +3 -2
- package/providers/auth_store.mjs +22 -0
- package/providers/claude_cli.mjs +28 -2
- package/providers/claude_cli_detect.mjs +46 -0
- package/providers/custom_provider.mjs +70 -0
- package/providers/model_catalogue.mjs +86 -0
- package/providers/orchestrator.mjs +30 -9
- package/providers/rates.mjs +12 -2
- package/providers/registry.mjs +10 -7
- package/sandbox/confiners/landlock.mjs +14 -8
- package/sandbox/confiners/seatbelt.mjs +18 -2
- package/scripts/loop-worker.mjs +18 -7
- package/scripts/migrate-v5.mjs +5 -61
- package/sessions.mjs +0 -0
- package/tui/modal_filter.mjs +59 -0
- package/tui/modal_picker.mjs +12 -37
- package/tui/pickers.mjs +917 -0
- package/tui/provider_families.mjs +41 -0
- package/tui/repl.mjs +67 -36
- package/tui/slash_commands.mjs +2 -1
- package/tui/slash_dispatcher.mjs +717 -58
- package/tui/splash.mjs +5 -12
- package/tui/subcommands.mjs +17 -0
- package/tui/terminal_approve.mjs +37 -0
- package/web/dashboard.css +275 -0
- package/web/dashboard.html +2 -1685
- package/web/dashboard.js +1406 -0
- package/workflow/persistent.mjs +13 -6
- package/mas/tools/skill_view.mjs +0 -43
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// tui/provider_families.mjs — pure (react-free) provider bucketing for the
|
|
2
|
+
// drill-in provider picker. The flat "every provider in one list" picker the
|
|
3
|
+
// v5.4 Ink port shipped is overwhelming and even exposed the orchestrator
|
|
4
|
+
// (which must never be a default). This restores the legacy family grouping
|
|
5
|
+
// (API key / CLI-Local / Mock) as shared, unit-testable data so both the
|
|
6
|
+
// readline picker (cli.mjs) and the Ink dispatcher use one bucketing rule.
|
|
7
|
+
|
|
8
|
+
// Bucket every registered provider into one of three auth families.
|
|
9
|
+
// orchestrator is excluded entirely (strictly opt-in; never a wizard
|
|
10
|
+
// default — see cli.mjs v5.3.2 note). Returns { api:[], cli:[], mock:[] }
|
|
11
|
+
// of provider-id strings.
|
|
12
|
+
export function bucketProviders(registry) {
|
|
13
|
+
const info = (registry && registry.PROVIDER_INFO) || {};
|
|
14
|
+
const all = Object.keys((registry && registry.PROVIDERS) || {});
|
|
15
|
+
const out = { api: [], cli: [], mock: [] };
|
|
16
|
+
for (const name of all) {
|
|
17
|
+
if (name === 'mock') out.mock.push(name);
|
|
18
|
+
else if (name === 'orchestrator') continue;
|
|
19
|
+
else if ((info[name] || {}).requiresApiKey) out.api.push(name);
|
|
20
|
+
else out.cli.push(name);
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Full family descriptors (label/desc/plain-text tag/members) for the Ink
|
|
26
|
+
// first-step picker. Tags are plain text — Ink applies its own styling.
|
|
27
|
+
export function providerFamilies(registry) {
|
|
28
|
+
const b = bucketProviders(registry);
|
|
29
|
+
return {
|
|
30
|
+
api: { id: 'api', label: 'API key', desc: 'paste an sk-... key', tag: 'needs key', members: b.api },
|
|
31
|
+
cli: { id: 'cli', label: 'CLI / Local', desc: 'keyless — an existing CLI login or a local daemon', tag: 'no key', members: b.cli },
|
|
32
|
+
mock: { id: 'mock', label: 'Mock', desc: 'offline echo, only useful for testing', tag: 'test', members: b.mock },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Plain-text per-row tag for a provider, from its PROVIDER_INFO meta.
|
|
37
|
+
export function providerTag(meta) {
|
|
38
|
+
const m = meta || {};
|
|
39
|
+
if (m.custom) return 'custom';
|
|
40
|
+
return m.requiresApiKey ? 'api key' : 'no key';
|
|
41
|
+
}
|
package/tui/repl.mjs
CHANGED
|
@@ -38,7 +38,7 @@ import { Splash, renderSplashToString } from './splash.mjs';
|
|
|
38
38
|
import { Editor } from './editor.mjs';
|
|
39
39
|
import { SlashPopup, filterSlashCommands } from './slash_popup.mjs';
|
|
40
40
|
import { SLASH_COMMANDS } from './slash_commands.mjs';
|
|
41
|
-
import { ModalPicker, filterModalItems } from './modal_picker.mjs';
|
|
41
|
+
import { ModalPicker, filterModalItems, resolveModalPick } from './modal_picker.mjs';
|
|
42
42
|
import { theme } from './theme.mjs';
|
|
43
43
|
|
|
44
44
|
// ─── Alt-buffer mount (DEC 1049) ─────────────────────────────────────────
|
|
@@ -63,6 +63,14 @@ export const ALT_BUFFER_ENTER = '\x1b[?1049h';
|
|
|
63
63
|
export const ALT_BUFFER_LEAVE = '\x1b[?1049l';
|
|
64
64
|
export const CURSOR_VISIBLE = '\x1b[?25h';
|
|
65
65
|
|
|
66
|
+
// Rendering-mode decision. Default = Static scrollback (no flicker; splash
|
|
67
|
+
// prints once + scrolls naturally). Alt-buffer fullscreen is opt-in via
|
|
68
|
+
// LAZYCLAW_ALT=1; LAZYCLAW_NO_ALT=1 forces it off. TTY-only either way.
|
|
69
|
+
export function computeAltEnabled(env, hasTTY) {
|
|
70
|
+
const e = env || {};
|
|
71
|
+
return !!hasTTY && !!e.LAZYCLAW_ALT && !e.LAZYCLAW_NO_ALT;
|
|
72
|
+
}
|
|
73
|
+
|
|
66
74
|
export function FullScreen({ enabled, children }) {
|
|
67
75
|
useEffect(() => {
|
|
68
76
|
if (!enabled) return undefined;
|
|
@@ -151,10 +159,10 @@ export function onStreamChunk(state, { chunk }) {
|
|
|
151
159
|
return { ...state, liveAssistant: state.liveAssistant + chunk };
|
|
152
160
|
}
|
|
153
161
|
|
|
154
|
-
export function onTurnComplete(state, { reason } = {}) {
|
|
162
|
+
export function onTurnComplete(state, { reason, error } = {}) {
|
|
155
163
|
const promoted = state.pendingPrepend;
|
|
156
164
|
const suffix = reason === 'aborted' ? ' [aborted]'
|
|
157
|
-
: reason === 'error' ? ' [error]'
|
|
165
|
+
: reason === 'error' ? (error ? ` [error: ${error}]` : ' [error]')
|
|
158
166
|
: '';
|
|
159
167
|
const text = (state.liveAssistant || '') + suffix;
|
|
160
168
|
// Commit any accumulated live text to scrollback. If the turn produced
|
|
@@ -188,13 +196,21 @@ export function consumeNextTurnFirstMessage(state) {
|
|
|
188
196
|
// - runTurnFactory(writeFn) → runTurn(text, signal) (sticky layout)
|
|
189
197
|
// - runTurn(text, signal) (legacy, stdout)
|
|
190
198
|
// Legacy mode is preserved verbatim for the existing cli.mjs callsite.
|
|
191
|
-
export function ReplApp({ splashProps, runTurn, runTurnFactory, slashCommands, onSlashCommand, statusInfo, pickerRef }) {
|
|
192
|
-
// statusInfo
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
const
|
|
199
|
+
export function ReplApp({ splashProps, runTurn, runTurnFactory, slashCommands, onSlashCommand, statusInfo, getStatus, pickerRef }) {
|
|
200
|
+
// statusInfo seeds the StatusBar's provider/model/ctx. getStatus (optional)
|
|
201
|
+
// returns the live values so the bar refreshes after a /provider or /model
|
|
202
|
+
// switch and after each turn (token/ctx gauge) — without it the bar would
|
|
203
|
+
// show whatever was captured at mount (stale after a slash mutates the
|
|
204
|
+
// active provider/model). v5.5.
|
|
205
|
+
const [statusState, setStatusState] = useState(() => statusInfo || splashProps || {});
|
|
206
|
+
const refreshStatus = useCallback(() => {
|
|
207
|
+
if (typeof getStatus !== 'function') return;
|
|
208
|
+
try {
|
|
209
|
+
const s = getStatus();
|
|
210
|
+
if (s) setStatusState((prev) => ({ ...prev, ...s }));
|
|
211
|
+
} catch { /* never let a status read break the turn */ }
|
|
212
|
+
}, [getStatus]);
|
|
213
|
+
const _status = statusState;
|
|
198
214
|
// Splash is rendered ONCE as scrollback[0] via <Static>. Build it lazily
|
|
199
215
|
// so SSR-style imports without a TTY don't crash on process.stdout.
|
|
200
216
|
const splashItemRef = useRef(null);
|
|
@@ -206,12 +222,16 @@ export function ReplApp({ splashProps, runTurn, runTurnFactory, slashCommands, o
|
|
|
206
222
|
const [state, setState] = useState(() => makeReplState({ splashItem: splashItemRef.current }));
|
|
207
223
|
const { exit } = useApp();
|
|
208
224
|
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
//
|
|
225
|
+
// Rendering mode. Default is the Static scrollback (no full-frame redraw,
|
|
226
|
+
// so no typing flicker; the splash prints once to the primary buffer and
|
|
227
|
+
// scrolls naturally — it never hits the alt-canvas vanish/blank bugs). The
|
|
228
|
+
// alt-buffer fullscreen (sticky-bottom Editor) is opt-in via LAZYCLAW_ALT=1,
|
|
229
|
+
// and LAZYCLAW_NO_ALT=1 still forces it off. TTY-only either way.
|
|
230
|
+
// Captured in a ref so the value is stable across renders (isTTY + env are
|
|
231
|
+
// read once on mount; they cannot change at runtime).
|
|
212
232
|
const altEnabledRef = useRef(null);
|
|
213
233
|
if (altEnabledRef.current === null) {
|
|
214
|
-
altEnabledRef.current =
|
|
234
|
+
altEnabledRef.current = computeAltEnabled(process.env, process.stdout && process.stdout.isTTY);
|
|
215
235
|
}
|
|
216
236
|
const altEnabled = altEnabledRef.current;
|
|
217
237
|
|
|
@@ -265,15 +285,20 @@ export function ReplApp({ splashProps, runTurn, runTurnFactory, slashCommands, o
|
|
|
265
285
|
const controller = new AbortController();
|
|
266
286
|
setState((s) => onUserInput(s, { text: trimmed, controller }));
|
|
267
287
|
try {
|
|
268
|
-
|
|
288
|
+
// Pass the signal so the host can abort a long slash op (e.g. /loop)
|
|
289
|
+
// when the user hits Esc (onEscape aborts state.controller).
|
|
290
|
+
const result = await onSlashCommand(trimmed, controller.signal);
|
|
269
291
|
if (result === 'EXIT') { exit(); return; }
|
|
270
292
|
if (typeof result === 'string' && result.length > 0) {
|
|
271
293
|
setState((s) => onStreamChunk(s, { chunk: result }));
|
|
272
294
|
}
|
|
273
295
|
setState((s) => onTurnComplete(s, { reason: 'done' }));
|
|
296
|
+
// A slash like /provider or /model mutates the host's active
|
|
297
|
+
// provider/model — refresh the StatusBar so it isn't stale.
|
|
298
|
+
refreshStatus();
|
|
274
299
|
} catch (err) {
|
|
275
300
|
setState((s) => onTurnComplete(s, {
|
|
276
|
-
reason: err && err.name === 'AbortError' ? 'aborted' : 'error',
|
|
301
|
+
reason: err && err.name === 'AbortError' ? 'aborted' : 'error', error: err?.message || String(err),
|
|
277
302
|
}));
|
|
278
303
|
}
|
|
279
304
|
return;
|
|
@@ -283,12 +308,13 @@ export function ReplApp({ splashProps, runTurn, runTurnFactory, slashCommands, o
|
|
|
283
308
|
try {
|
|
284
309
|
await runTurnRef.current(trimmed, controller.signal);
|
|
285
310
|
setState((s) => onTurnComplete(s, { reason: 'done' }));
|
|
311
|
+
refreshStatus();
|
|
286
312
|
} catch (err) {
|
|
287
313
|
setState((s) => onTurnComplete(s, {
|
|
288
314
|
reason: err && err.name === 'AbortError' ? 'aborted' : 'error',
|
|
289
315
|
}));
|
|
290
316
|
}
|
|
291
|
-
}, [exit, onSlashCommand]);
|
|
317
|
+
}, [exit, onSlashCommand, refreshStatus]);
|
|
292
318
|
|
|
293
319
|
// Auto-submit queued mid-stream-interrupt message (spec §5.8). Read
|
|
294
320
|
// state.nextTurnFirstMessage so the effect re-fires when promoted.
|
|
@@ -425,9 +451,10 @@ export function ReplApp({ splashProps, runTurn, runTurnFactory, slashCommands, o
|
|
|
425
451
|
});
|
|
426
452
|
}, [modalView.length]);
|
|
427
453
|
const onModalConfirm = useCallback(() => {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
454
|
+
// A `freeText` row resolves to { id, query } so the dispatcher can use
|
|
455
|
+
// the typed filter buffer as a custom value (e.g. an unlisted model id).
|
|
456
|
+
closeModal(resolveModalPick(modalView[modalIdx], modalQuery));
|
|
457
|
+
}, [modalView, modalIdx, modalQuery, closeModal]);
|
|
431
458
|
const onModalCancel = useCallback(() => { closeModal(null); }, [closeModal]);
|
|
432
459
|
const onModalQuery = useCallback((next) => {
|
|
433
460
|
setModalQuery(next || '');
|
|
@@ -466,17 +493,16 @@ export function ReplApp({ splashProps, runTurn, runTurnFactory, slashCommands, o
|
|
|
466
493
|
altEnabled
|
|
467
494
|
? React.createElement(
|
|
468
495
|
Box,
|
|
469
|
-
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
//
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
).map((item) =>
|
|
496
|
+
// v5.5 — `justifyContent: 'flex-end'` pins the newest content to
|
|
497
|
+
// the bottom of the fixed-height alt canvas; older lines (the
|
|
498
|
+
// splash / sloth + manual) overflow off the TOP and are clipped
|
|
499
|
+
// naturally, exactly like a real scrollback. This replaces the
|
|
500
|
+
// v5.4.3 hack that hard-dropped the splash after the first turn
|
|
501
|
+
// (which made the manual + character abruptly vanish the moment
|
|
502
|
+
// you ran any command). The splash now scrolls off only once
|
|
503
|
+
// there's enough content to push it past the top edge.
|
|
504
|
+
{ flexDirection: 'column', flexGrow: 1, flexShrink: 1, overflow: 'hidden', justifyContent: 'flex-end' },
|
|
505
|
+
state.scrollback.map((item) =>
|
|
480
506
|
React.createElement(ScrollbackItem, { key: item.id, item })
|
|
481
507
|
),
|
|
482
508
|
// Live region — partial assistant stream (inside the scroll
|
|
@@ -567,10 +593,15 @@ export function ReplApp({ splashProps, runTurn, runTurnFactory, slashCommands, o
|
|
|
567
593
|
);
|
|
568
594
|
}
|
|
569
595
|
|
|
570
|
-
// ScrollbackItem renders each
|
|
571
|
-
//
|
|
572
|
-
//
|
|
573
|
-
|
|
596
|
+
// ScrollbackItem renders each scrollback child. Splash renders via the real
|
|
597
|
+
// <Splash/> component (preserves gradient wordmark colorization); everything
|
|
598
|
+
// else is plain Text.
|
|
599
|
+
//
|
|
600
|
+
// Wrapped in React.memo: scrollback item objects are stable across renders,
|
|
601
|
+
// so when only the editor buffer changes (every keystroke) the memo skips
|
|
602
|
+
// re-rendering every committed line. Without this the whole alt-buffer
|
|
603
|
+
// scrollback re-rendered on each keypress — the source of the typing flicker.
|
|
604
|
+
export const ScrollbackItem = React.memo(function ScrollbackItem({ item }) {
|
|
574
605
|
if (item.kind === 'splash') {
|
|
575
606
|
return React.createElement(Splash, item.splashProps);
|
|
576
607
|
}
|
|
@@ -586,7 +617,7 @@ export function ScrollbackItem({ item }) {
|
|
|
586
617
|
}
|
|
587
618
|
// 'assistant' (default)
|
|
588
619
|
return React.createElement(Text, { color: theme.fg }, item.text);
|
|
589
|
-
}
|
|
620
|
+
});
|
|
590
621
|
|
|
591
622
|
// StatusBar — single row, provider · model · ctx · streaming indicator.
|
|
592
623
|
// Kept intentionally minimal in v5.3; token gauges land separately once
|
package/tui/slash_commands.mjs
CHANGED
|
@@ -25,6 +25,7 @@ export const SLASH_COMMANDS = [
|
|
|
25
25
|
{ cmd: '/trainer', help: 'view or set trainer provider/model: /trainer show|set <p:m>|clear' },
|
|
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
|
+
{ cmd: '/menu', help: 'browse the full subcommand catalog (command palette)' },
|
|
28
29
|
{ cmd: '/loop', help: 'repeat one prompt: /loop "fix lint" [--max N] [--until "<regex>"]' },
|
|
29
30
|
{ cmd: '/goal', help: 'register/switch goal: /goal add NAME | /goal list' },
|
|
30
31
|
{ cmd: '/memory', help: 'show layered memory: /memory [core|recent|episodic [topic]]' },
|
|
@@ -32,7 +33,7 @@ export const SLASH_COMMANDS = [
|
|
|
32
33
|
{ cmd: '/dream', help: 'consolidate recent memory into per-topic episodic files' },
|
|
33
34
|
{ cmd: '/agent', help: 'spawn or switch agent: /agent NAME' },
|
|
34
35
|
{ cmd: '/team', help: 'list, create, or join a team' },
|
|
35
|
-
{ cmd: '/task', help: 'list/show/transcript/abandon/done/remove
|
|
36
|
+
{ cmd: '/task', help: 'multi-agent tasks: start/tick/list/show/transcript/abandon/done/remove' },
|
|
36
37
|
{ cmd: '/handoff', help: 'hand current task off to another agent' },
|
|
37
38
|
{ cmd: '/exit', help: 'leave the chat' },
|
|
38
39
|
{ cmd: '/quit', help: 'alias for /exit' },
|