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
package/scripts/loop-worker.mjs
CHANGED
|
@@ -85,7 +85,13 @@ const max = Number(args.max) || loopEngine.LOOP_MAX_DEFAULT;
|
|
|
85
85
|
loops.patchMeta(loopId, { status: 'running', startedAt: new Date().toISOString() }, cfgDir);
|
|
86
86
|
|
|
87
87
|
const ac = new AbortController();
|
|
88
|
+
// When a signal arrives, onTerm writes the authoritative 'killed' result and
|
|
89
|
+
// owns the exit. `terminating` stops the normal-completion path (which the
|
|
90
|
+
// aborted runLoop returns into within the same ~50ms window) from racing a
|
|
91
|
+
// second writeResult onto the same file.
|
|
92
|
+
let terminating = false;
|
|
88
93
|
function onTerm(sig) {
|
|
94
|
+
terminating = true;
|
|
89
95
|
ac.abort();
|
|
90
96
|
loops.patchMeta(loopId, { status: 'killed', finishedAt: new Date().toISOString(), signal: sig }, cfgDir);
|
|
91
97
|
loops.writeResult(loopId, { stoppedBy: 'kill', signal: sig }, cfgDir);
|
|
@@ -149,12 +155,17 @@ try {
|
|
|
149
155
|
onIteration,
|
|
150
156
|
signal: ac.signal,
|
|
151
157
|
});
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
158
|
+
if (!terminating) {
|
|
159
|
+
const finalStatus = result.stoppedBy === 'abort' ? 'killed' : 'completed';
|
|
160
|
+
loops.patchMeta(loopId, { status: finalStatus, finishedAt: new Date().toISOString() }, cfgDir);
|
|
161
|
+
loops.writeResult(loopId, result, cfgDir);
|
|
162
|
+
process.exit(0);
|
|
163
|
+
}
|
|
164
|
+
// else: a signal is terminating us — onTerm wrote the result and owns exit.
|
|
156
165
|
} catch (err) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
166
|
+
if (!terminating) {
|
|
167
|
+
loops.patchMeta(loopId, { status: 'failed', finishedAt: new Date().toISOString() }, cfgDir);
|
|
168
|
+
loops.writeResult(loopId, { error: err?.message || String(err), stack: err?.stack }, cfgDir);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
160
171
|
}
|
package/scripts/migrate-v5.mjs
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import fs from 'node:fs';
|
|
21
21
|
import path from 'node:path';
|
|
22
22
|
import os from 'node:os';
|
|
23
|
-
import {
|
|
23
|
+
import { reindexAll } from '../mas/index_db.mjs';
|
|
24
24
|
import { parseFrontmatter } from '../skills.mjs';
|
|
25
25
|
|
|
26
26
|
function defaultConfigDir() {
|
|
@@ -96,67 +96,11 @@ function upgradeAllSkills(configDir) {
|
|
|
96
96
|
return { upgraded: n };
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
// Rebuild + repopulate the FTS index. The walk now lives in index_db.reindexAll
|
|
100
|
+
// (shared with the daemon POST /index/rebuild route) so a "rebuild" is always a
|
|
101
|
+
// repopulate, never a silent zeroing.
|
|
99
102
|
function rebuildIndex(configDir) {
|
|
100
|
-
|
|
101
|
-
openIndex(configDir);
|
|
102
|
-
|
|
103
|
-
// Sessions.
|
|
104
|
-
const sessDir = path.join(configDir, 'sessions');
|
|
105
|
-
if (fs.existsSync(sessDir)) {
|
|
106
|
-
for (const f of fs.readdirSync(sessDir)) {
|
|
107
|
-
if (!f.endsWith('.jsonl')) continue;
|
|
108
|
-
const id = f.slice(0, -'.jsonl'.length);
|
|
109
|
-
const raw = fs.readFileSync(path.join(sessDir, f), 'utf8');
|
|
110
|
-
let idx = 0;
|
|
111
|
-
for (const line of raw.split('\n')) {
|
|
112
|
-
if (!line) continue;
|
|
113
|
-
try {
|
|
114
|
-
const obj = JSON.parse(line);
|
|
115
|
-
indexSessionTurn({
|
|
116
|
-
session_id: id, turn_idx: idx++, role: obj.role || 'user',
|
|
117
|
-
ts: obj.ts || 0, content: obj.content || '',
|
|
118
|
-
}, configDir);
|
|
119
|
-
} catch { /* skip malformed */ }
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Skills.
|
|
125
|
-
const skillsDir = path.join(configDir, 'skills');
|
|
126
|
-
if (fs.existsSync(skillsDir)) {
|
|
127
|
-
for (const f of fs.readdirSync(skillsDir)) {
|
|
128
|
-
if (!f.endsWith('.md')) continue;
|
|
129
|
-
const name = f.slice(0, -'.md'.length);
|
|
130
|
-
const raw = fs.readFileSync(path.join(skillsDir, f), 'utf8');
|
|
131
|
-
const { meta, body } = parseFrontmatter(raw);
|
|
132
|
-
indexSkill({
|
|
133
|
-
skill_name: name,
|
|
134
|
-
trained_by: meta.trained_by || 'legacy',
|
|
135
|
-
group_name: meta.group || (name.includes('-') ? name.split('-')[0] : 'legacy'),
|
|
136
|
-
content: body,
|
|
137
|
-
}, configDir);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Memory (core + episodic).
|
|
142
|
-
const memDir = path.join(configDir, 'memory');
|
|
143
|
-
if (fs.existsSync(memDir)) {
|
|
144
|
-
const corePath = path.join(memDir, 'core.md');
|
|
145
|
-
if (fs.existsSync(corePath)) {
|
|
146
|
-
indexMemory({ topic: 'core', kind: 'core',
|
|
147
|
-
content: fs.readFileSync(corePath, 'utf8') }, configDir);
|
|
148
|
-
}
|
|
149
|
-
const epi = path.join(memDir, 'episodic');
|
|
150
|
-
if (fs.existsSync(epi)) {
|
|
151
|
-
for (const f of fs.readdirSync(epi)) {
|
|
152
|
-
if (!f.endsWith('.md')) continue;
|
|
153
|
-
indexMemory({
|
|
154
|
-
topic: f.slice(0, -'.md'.length), kind: 'episodic',
|
|
155
|
-
content: fs.readFileSync(path.join(epi, f), 'utf8'),
|
|
156
|
-
}, configDir);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
103
|
+
reindexAll(configDir);
|
|
160
104
|
}
|
|
161
105
|
|
|
162
106
|
export async function migrateV5(opts = {}) {
|
package/sessions.mjs
CHANGED
|
Binary file
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// tui/modal_filter.mjs — pure (react-free) primitives for the Ink modal
|
|
2
|
+
// picker. Split out from modal_picker.mjs so the filtering / windowing /
|
|
3
|
+
// pick-resolution logic is unit-testable without pulling in react + ink
|
|
4
|
+
// (which are only present in the running TUI). modal_picker.mjs re-exports
|
|
5
|
+
// these for back-compat.
|
|
6
|
+
|
|
7
|
+
// Pure filter — prefix > substring > subsequence. Stable order within each
|
|
8
|
+
// tier (original list order is the tiebreaker).
|
|
9
|
+
//
|
|
10
|
+
// `pinned` rows bypass the filter entirely and are always appended after
|
|
11
|
+
// the matches. This keeps sentinel rows (e.g. "↻ fetch live models",
|
|
12
|
+
// "… type a custom model id") on screen while the user types an id that
|
|
13
|
+
// matches no listed model — the typed filter doubles as the custom-id
|
|
14
|
+
// buffer for the free-text row.
|
|
15
|
+
export function filterModalItems(query, items) {
|
|
16
|
+
const q = String(query || '').trim().toLowerCase();
|
|
17
|
+
const list = Array.isArray(items) ? items : [];
|
|
18
|
+
if (!q) return list.slice();
|
|
19
|
+
const prefix = [], substr = [], subseq = [], pinned = [];
|
|
20
|
+
for (const it of list) {
|
|
21
|
+
if (it && it.pinned) { pinned.push(it); continue; }
|
|
22
|
+
const hay = `${it.label || it.id || ''} ${it.desc || ''}`.toLowerCase();
|
|
23
|
+
if (hay.startsWith(q)) prefix.push(it);
|
|
24
|
+
else if (hay.includes(q)) substr.push(it);
|
|
25
|
+
else if (_isSubseq(q, hay)) subseq.push(it);
|
|
26
|
+
}
|
|
27
|
+
return [...prefix, ...substr, ...subseq, ...pinned];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function _isSubseq(needle, hay) {
|
|
31
|
+
let i = 0;
|
|
32
|
+
for (const ch of hay) {
|
|
33
|
+
if (ch === needle[i]) i++;
|
|
34
|
+
if (i === needle.length) return true;
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Pure window computation — slide a window of `maxRows` items so that
|
|
40
|
+
// `selectedIndex` is always visible. Mirrors the pattern in
|
|
41
|
+
// tui/slash_popup.mjs._computeWindow.
|
|
42
|
+
export function _computeWindow(idx, total, maxRows) {
|
|
43
|
+
const n = Math.max(0, total);
|
|
44
|
+
const m = Math.max(1, maxRows);
|
|
45
|
+
if (n <= m) return { start: 0, end: n };
|
|
46
|
+
let start = Math.max(0, Math.min(n - m, idx - Math.floor(m / 2)));
|
|
47
|
+
return { start, end: start + m };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Pure pick resolver — maps the highlighted row + current filter buffer to
|
|
51
|
+
// what openPicker resolves with. A `freeText` row resolves to
|
|
52
|
+
// `{ id, query }` so the caller can use the typed filter as a custom value
|
|
53
|
+
// (e.g. an unlisted model id); every other row resolves to its plain id.
|
|
54
|
+
// No selection resolves to null (caller treats as cancel).
|
|
55
|
+
export function resolveModalPick(pickedItem, query) {
|
|
56
|
+
if (!pickedItem) return null;
|
|
57
|
+
if (pickedItem.freeText) return { id: pickedItem.id, query: String(query || '') };
|
|
58
|
+
return pickedItem.id;
|
|
59
|
+
}
|
package/tui/modal_picker.mjs
CHANGED
|
@@ -15,44 +15,14 @@ import React from 'react';
|
|
|
15
15
|
import { Box, Text } from 'ink';
|
|
16
16
|
import stringWidth from 'string-width';
|
|
17
17
|
import { theme } from './theme.mjs';
|
|
18
|
+
// Pure (react-free) primitives live in modal_filter.mjs so they can be unit
|
|
19
|
+
// tested without the Ink runtime. Re-exported here for back-compat with
|
|
20
|
+
// existing importers (tui/repl.mjs).
|
|
21
|
+
import { filterModalItems, _computeWindow, resolveModalPick } from './modal_filter.mjs';
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// Pure filter — prefix > substring > subsequence. Stable order within
|
|
22
|
-
// each tier (original list order is the tiebreaker).
|
|
23
|
-
export function filterModalItems(query, items) {
|
|
24
|
-
const q = String(query || '').trim().toLowerCase();
|
|
25
|
-
const list = Array.isArray(items) ? items : [];
|
|
26
|
-
if (!q) return list.slice();
|
|
27
|
-
const prefix = [], substr = [], subseq = [];
|
|
28
|
-
for (const it of list) {
|
|
29
|
-
const hay = `${it.label || it.id || ''} ${it.desc || ''}`.toLowerCase();
|
|
30
|
-
if (hay.startsWith(q)) prefix.push(it);
|
|
31
|
-
else if (hay.includes(q)) substr.push(it);
|
|
32
|
-
else if (_isSubseq(q, hay)) subseq.push(it);
|
|
33
|
-
}
|
|
34
|
-
return [...prefix, ...substr, ...subseq];
|
|
35
|
-
}
|
|
23
|
+
export { filterModalItems, _computeWindow, resolveModalPick };
|
|
36
24
|
|
|
37
|
-
|
|
38
|
-
let i = 0;
|
|
39
|
-
for (const ch of hay) {
|
|
40
|
-
if (ch === needle[i]) i++;
|
|
41
|
-
if (i === needle.length) return true;
|
|
42
|
-
}
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Pure window computation — slide a window of `maxRows` items so that
|
|
47
|
-
// `selectedIndex` is always visible. Mirrors the pattern in
|
|
48
|
-
// tui/slash_popup.mjs._computeWindow.
|
|
49
|
-
export function _computeWindow(idx, total, maxRows) {
|
|
50
|
-
const n = Math.max(0, total);
|
|
51
|
-
const m = Math.max(1, maxRows);
|
|
52
|
-
if (n <= m) return { start: 0, end: n };
|
|
53
|
-
let start = Math.max(0, Math.min(n - m, idx - Math.floor(m / 2)));
|
|
54
|
-
return { start, end: start + m };
|
|
55
|
-
}
|
|
25
|
+
const DEFAULT_MAX_ROWS = 12;
|
|
56
26
|
|
|
57
27
|
// Presentational component.
|
|
58
28
|
//
|
|
@@ -126,11 +96,16 @@ export function ModalPicker({
|
|
|
126
96
|
const marker = selected ? '❯ ' : ' ';
|
|
127
97
|
const label = String(it.label || it.id || '').padEnd(labelW);
|
|
128
98
|
const desc = it.desc ? ` ${it.desc}` : '';
|
|
129
|
-
// Render selected row inverse-bold for contrast.
|
|
99
|
+
// Render selected row inverse-bold for contrast. A row `tag` (e.g.
|
|
100
|
+
// "api key" / "no key" / "custom") renders as a dim trailing pill so
|
|
101
|
+
// the picker signals at a glance which providers need a key.
|
|
130
102
|
rows.push(React.createElement(
|
|
131
103
|
Text,
|
|
132
104
|
{ key: `it-${absoluteIdx}`, bold: selected, inverse: selected },
|
|
133
105
|
`${marker}${label}${desc}`,
|
|
106
|
+
it.tag
|
|
107
|
+
? React.createElement(Text, { key: 'tag', dimColor: true }, ` [${it.tag}]`)
|
|
108
|
+
: null,
|
|
134
109
|
));
|
|
135
110
|
}
|
|
136
111
|
if (end < total) {
|