lazyclaw 6.4.0 → 6.6.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/agents.mjs +68 -1
- package/commands/agents.mjs +3 -194
- package/commands/agents_registry.mjs +227 -0
- package/commands/automation.mjs +8 -89
- package/commands/automation_loops.mjs +94 -0
- package/commands/chat.mjs +46 -680
- package/commands/chat_legacy_slash.mjs +716 -0
- package/commands/config.mjs +36 -2
- package/commands/help_text.mjs +78 -0
- package/commands/setup.mjs +1 -73
- package/daemon/lib/cost.mjs +5 -0
- package/daemon/route_table.mjs +1 -0
- package/daemon/routes/meta.mjs +29 -0
- package/daemon.mjs +1 -1
- package/lib/args.mjs +1 -1
- package/mas/agent_turn.mjs +7 -0
- package/package.json +1 -1
- package/providers/claude_cli.mjs +1 -1
- package/providers/claude_cli_session.mjs +21 -4
- package/providers/codex_cli.mjs +8 -5
- package/providers/gemini.mjs +4 -1
- package/providers/gemini_cli.mjs +17 -3
- package/providers/tool_use/gemini.mjs +12 -1
- package/providers/tool_use/openai.mjs +7 -0
- package/tui/banner.mjs +72 -0
- package/tui/editor.mjs +0 -0
- package/tui/editor_anchor.mjs +46 -0
- package/tui/orchestrator_setup.mjs +135 -0
- package/tui/pickers.mjs +34 -180
- package/tui/repl.mjs +12 -165
- package/tui/repl_altbuffer.mjs +63 -0
- package/tui/repl_reducers.mjs +114 -0
- package/tui/slash_channels.mjs +208 -0
- package/tui/slash_dashboard.mjs +220 -0
- package/tui/slash_dispatcher.mjs +12 -640
- package/tui/slash_helpers.mjs +68 -0
- package/tui/slash_trainer.mjs +173 -0
- package/web/dashboard.js +7 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// tui/slash_helpers.mjs — shared leaf helpers for the slash-command dispatcher
|
|
2
|
+
// and its sibling handler modules. Extracted verbatim from slash_dispatcher.mjs
|
|
3
|
+
// so the dispatcher, dashboard, channels, and trainer modules share one set of
|
|
4
|
+
// pure utilities without a circular import. These import nothing from the
|
|
5
|
+
// dispatcher.
|
|
6
|
+
|
|
7
|
+
// Tiny utility — split args on whitespace, drop empties. Used by sub-command
|
|
8
|
+
// handlers that don't need the loop-engine's full quote-aware splitter.
|
|
9
|
+
export function splitWhitespace(s) {
|
|
10
|
+
return (s || '').split(/\s+/).filter(Boolean);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Parse a "provider[:model]" spec, preferring the registry's parser.
|
|
14
|
+
export function _parseProvModel(registry, spec) {
|
|
15
|
+
if (registry && typeof registry.parseProviderModel === 'function') return registry.parseProviderModel(spec);
|
|
16
|
+
const s = String(spec || '');
|
|
17
|
+
const i = s.indexOf(':');
|
|
18
|
+
if (i < 0) return { provider: s || null, model: null };
|
|
19
|
+
return { provider: s.slice(0, i) || null, model: s.slice(i + 1) || null };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Best-effort dynamic import. Returns the resolved ctx field if the caller
|
|
23
|
+
// pre-injected it (test hot path), else loads the real module. Throwing is
|
|
24
|
+
// fine — handlers wrap calls in try/catch where appropriate.
|
|
25
|
+
export async function _mod(ctx, key, importer) {
|
|
26
|
+
if (ctx && ctx[key]) return ctx[key];
|
|
27
|
+
return importer();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Single free-text prompt reusing the modal's filter buffer (no dedicated
|
|
31
|
+
// input widget). Returns the typed value, '' (only when allowEmpty), or null
|
|
32
|
+
// on cancel / required-but-empty.
|
|
33
|
+
export async function _promptText(ctx, { title, subtitle, allowEmpty, secret } = {}) {
|
|
34
|
+
if (typeof ctx.openPicker !== 'function') return null;
|
|
35
|
+
const picked = await ctx.openPicker({
|
|
36
|
+
kind: 'text',
|
|
37
|
+
title,
|
|
38
|
+
// `secret` masks the typed query on screen (api-key / token entry) while
|
|
39
|
+
// the real value still reaches the caller — the modal picker honors it.
|
|
40
|
+
secret: !!secret,
|
|
41
|
+
subtitle: subtitle || 'type into the filter, then pick the row · Esc cancels',
|
|
42
|
+
items: [{ id: '__text__', label: '✓ use what I typed above', desc: '', pinned: true, freeText: true }],
|
|
43
|
+
});
|
|
44
|
+
if (picked == null) return null;
|
|
45
|
+
if (typeof picked === 'object') {
|
|
46
|
+
const v = String(picked.query || '').trim();
|
|
47
|
+
if (!v && !allowEmpty) return null;
|
|
48
|
+
return v;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Yes/no confirmation modal for sensitive-tool approval. Esc (or no modal
|
|
54
|
+
// available) DENIES — approval is never granted by omission.
|
|
55
|
+
export async function _promptConfirm(ctx, { title, subtitle } = {}) {
|
|
56
|
+
if (typeof ctx.openPicker !== 'function') return false;
|
|
57
|
+
const picked = await ctx.openPicker({
|
|
58
|
+
kind: 'menu',
|
|
59
|
+
title: title || 'Approve sensitive tool?',
|
|
60
|
+
subtitle: subtitle || 'Enter selects · Esc denies',
|
|
61
|
+
items: [
|
|
62
|
+
{ id: 'approve', label: '✓ approve once', desc: 'run this tool call' },
|
|
63
|
+
{ id: 'deny', label: '✗ deny', desc: 'block this tool call' },
|
|
64
|
+
],
|
|
65
|
+
});
|
|
66
|
+
const id = picked && typeof picked === 'object' ? picked.id : picked;
|
|
67
|
+
return id === 'approve';
|
|
68
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// tui/slash_trainer.mjs — the /trainer slash-command handler, extracted verbatim
|
|
2
|
+
// from slash_dispatcher.mjs. Drives the synthesis / learning model selection
|
|
3
|
+
// (set / fallback / show / clear) plus the bare-menu picker. Imports its leaf
|
|
4
|
+
// helpers from slash_helpers.mjs and the shared provider/model picker from
|
|
5
|
+
// model_pick.mjs, never the dispatcher (no cycle). Recurses internally
|
|
6
|
+
// (_trainer('show', ctx)) — that recursion stays inside this module.
|
|
7
|
+
|
|
8
|
+
import { pickProviderModel, providerLookup as _providerLookup } from './model_pick.mjs';
|
|
9
|
+
import { renderRecord } from '../lib/render.mjs';
|
|
10
|
+
import { splitWhitespace, _mod, _parseProvModel, _promptConfirm } from './slash_helpers.mjs';
|
|
11
|
+
|
|
12
|
+
export async function _trainer(args, ctx) {
|
|
13
|
+
const registry = await _mod(ctx, 'registryMod', () => import('../providers/registry.mjs'));
|
|
14
|
+
const tokens = splitWhitespace(args);
|
|
15
|
+
|
|
16
|
+
// Bare /trainer with a modal available → an action menu (mirrors the bare
|
|
17
|
+
// /orchestrator menu). "Set"/"Fallback" re-enter and drill the shared
|
|
18
|
+
// provider→model picker; "Clear"/"Show" run their subcommands. Typed forms
|
|
19
|
+
// (/trainer set <p:m>, etc.) still work and skip the menu.
|
|
20
|
+
if (tokens.length === 0 && typeof ctx.openPicker === 'function') {
|
|
21
|
+
let cur = '';
|
|
22
|
+
try {
|
|
23
|
+
if (typeof registry.resolveTrainer === 'function') {
|
|
24
|
+
const e = registry.resolveTrainer(ctx.cfg || {});
|
|
25
|
+
cur = `now: ${e.provider}${e.model ? ':' + e.model : ':(default)'}`;
|
|
26
|
+
}
|
|
27
|
+
} catch { /* show menu without the current hint */ }
|
|
28
|
+
const picked = await ctx.openPicker({
|
|
29
|
+
kind: 'menu',
|
|
30
|
+
title: 'Trainer — synthesis / learning model',
|
|
31
|
+
subtitle: cur || 'pick provider + model for trainer turns',
|
|
32
|
+
items: [
|
|
33
|
+
{ id: 'set', label: 'Set trainer…', desc: 'pick provider + model (or auto / provider default)' },
|
|
34
|
+
{ id: 'fallback', label: 'Set fallback…', desc: 'pick a fallback provider + model' },
|
|
35
|
+
{ id: 'clear', label: 'Clear', desc: 'unset — mirror the chat provider/model' },
|
|
36
|
+
{ id: 'show', label: 'Show', desc: 'print the effective + configured trainer' },
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
const id = picked && typeof picked === 'object' ? picked.id : picked;
|
|
40
|
+
if (!id || typeof id !== 'string') return _trainer('show', ctx); // cancelled → show status
|
|
41
|
+
return _trainer(id, ctx); // re-enter; set/fallback open the picker (no spec)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const sub = tokens[0] || 'show';
|
|
45
|
+
|
|
46
|
+
if (sub === 'show') {
|
|
47
|
+
let effective = { provider: ctx.getActiveProvName ? ctx.getActiveProvName() : null,
|
|
48
|
+
model: ctx.getActiveModel ? ctx.getActiveModel() : null };
|
|
49
|
+
try {
|
|
50
|
+
if (typeof registry.resolveTrainer === 'function') {
|
|
51
|
+
effective = registry.resolveTrainer(ctx.cfg || {});
|
|
52
|
+
}
|
|
53
|
+
} catch { /* fall through */ }
|
|
54
|
+
const configured = (ctx.cfg && ctx.cfg.trainer) || null;
|
|
55
|
+
const cfgRender = configured
|
|
56
|
+
? renderRecord(configured, { fields: ['provider', 'model', 'fallback'] }).split('\n').map((l) => ' ' + l).join('\n')
|
|
57
|
+
: '(unset — trainer mirrors the chat provider/model)';
|
|
58
|
+
return [
|
|
59
|
+
'trainer (effective):',
|
|
60
|
+
` provider: ${effective.provider}`,
|
|
61
|
+
` model: ${effective.model || '(default)'}`,
|
|
62
|
+
'trainer (configured):',
|
|
63
|
+
cfgRender,
|
|
64
|
+
].join('\n');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (sub === 'set') {
|
|
68
|
+
let spec = tokens[1];
|
|
69
|
+
let _setFromPicker = false;
|
|
70
|
+
// No spec + a modal available → drill the shared picker (with an "auto"
|
|
71
|
+
// row and a "provider default" row) instead of requiring a typed spec.
|
|
72
|
+
if (!spec && typeof ctx.openPicker === 'function') {
|
|
73
|
+
const r = await pickProviderModel(ctx, registry, { includeAuto: true, includeDefault: true });
|
|
74
|
+
if (!r || r.model == null) return 'trainer set: cancelled';
|
|
75
|
+
spec = r.provider === 'auto' ? 'auto' : (r.model ? `${r.provider}:${r.model}` : r.provider);
|
|
76
|
+
_setFromPicker = true;
|
|
77
|
+
}
|
|
78
|
+
if (!spec) return 'usage: /trainer set <provider>[:<model>] (or `auto` for orchestrator-managed)';
|
|
79
|
+
const parsed = typeof registry.parseProviderModel === 'function'
|
|
80
|
+
? registry.parseProviderModel(spec)
|
|
81
|
+
: { provider: spec.split(':')[0], model: spec.split(':')[1] || null };
|
|
82
|
+
if (!parsed || !parsed.provider) return `/trainer set: could not parse "${spec}"`;
|
|
83
|
+
if (parsed.provider !== 'auto') {
|
|
84
|
+
const next = _providerLookup(registry, parsed.provider);
|
|
85
|
+
if (!next) return `/trainer set: unknown provider "${parsed.provider}"`;
|
|
86
|
+
}
|
|
87
|
+
// Optional `--fallback <provider[:model]>` — resolveTrainer routes here
|
|
88
|
+
// when opts.useFallback is set. Validate before persisting.
|
|
89
|
+
let fallbackSpec = null;
|
|
90
|
+
const fi = tokens.indexOf('--fallback');
|
|
91
|
+
if (fi >= 0) {
|
|
92
|
+
fallbackSpec = tokens[fi + 1];
|
|
93
|
+
if (!fallbackSpec) return 'usage: /trainer set <p:m> --fallback <p:m>';
|
|
94
|
+
const fp = _parseProvModel(registry, fallbackSpec);
|
|
95
|
+
if (!fp.provider) return `/trainer set: could not parse fallback "${fallbackSpec}"`;
|
|
96
|
+
if (fp.provider !== 'auto' && !_providerLookup(registry, fp.provider)) {
|
|
97
|
+
return `/trainer set: unknown provider "${fp.provider}"`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Picker-driven set with no --fallback flag → offer an optional fallback
|
|
101
|
+
// pick (the flag form stays the escape hatch for typed callers).
|
|
102
|
+
if (_setFromPicker && !fallbackSpec && typeof ctx.openPicker === 'function') {
|
|
103
|
+
const addFb = await _promptConfirm(ctx, { title: 'Add a fallback trainer?', subtitle: 'used when the primary is unavailable · Esc / deny to skip' });
|
|
104
|
+
if (addFb) {
|
|
105
|
+
const fr = await pickProviderModel(ctx, registry, { includeAuto: true, includeDefault: true });
|
|
106
|
+
if (fr && fr.model != null) {
|
|
107
|
+
fallbackSpec = fr.provider === 'auto' ? 'auto' : (fr.model ? `${fr.provider}:${fr.model}` : fr.provider);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Read-merge-write so unrelated cfg keys survive.
|
|
112
|
+
const fs = await import('node:fs');
|
|
113
|
+
const path = await import('node:path');
|
|
114
|
+
const cfgPath = path.join(ctx.cfgDir, 'config.json');
|
|
115
|
+
let diskCfg = {};
|
|
116
|
+
try { diskCfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8')); } catch { /* fresh */ }
|
|
117
|
+
diskCfg.trainer = { ...(diskCfg.trainer || {}), provider: parsed.provider };
|
|
118
|
+
if (parsed.model) diskCfg.trainer.model = parsed.model;
|
|
119
|
+
else delete diskCfg.trainer.model;
|
|
120
|
+
if (fallbackSpec) diskCfg.trainer.fallback = fallbackSpec;
|
|
121
|
+
try { fs.mkdirSync(ctx.cfgDir, { recursive: true }); } catch {}
|
|
122
|
+
fs.writeFileSync(cfgPath, JSON.stringify(diskCfg, null, 2));
|
|
123
|
+
if (ctx.cfg) ctx.cfg.trainer = { ...diskCfg.trainer };
|
|
124
|
+
return `✓ trainer → ${parsed.provider}${parsed.model ? ':' + parsed.model : ''}${fallbackSpec ? ` (fallback: ${fallbackSpec})` : ''}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (sub === 'fallback') {
|
|
128
|
+
let spec = tokens[1];
|
|
129
|
+
if (!spec && typeof ctx.openPicker === 'function') {
|
|
130
|
+
const r = await pickProviderModel(ctx, registry, { includeAuto: true, includeDefault: true });
|
|
131
|
+
if (!r || r.model == null) return 'trainer fallback: cancelled';
|
|
132
|
+
spec = r.provider === 'auto' ? 'auto' : (r.model ? `${r.provider}:${r.model}` : r.provider);
|
|
133
|
+
}
|
|
134
|
+
if (!spec) return 'usage: /trainer fallback <provider>[:<model>] | clear';
|
|
135
|
+
const fs = await import('node:fs');
|
|
136
|
+
const path = await import('node:path');
|
|
137
|
+
const cfgPath = path.join(ctx.cfgDir, 'config.json');
|
|
138
|
+
let diskCfg = {};
|
|
139
|
+
try { diskCfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8')); } catch { /* fresh */ }
|
|
140
|
+
if (spec === 'clear' || spec === 'unset') {
|
|
141
|
+
if (diskCfg.trainer) delete diskCfg.trainer.fallback;
|
|
142
|
+
try { fs.mkdirSync(ctx.cfgDir, { recursive: true }); } catch {}
|
|
143
|
+
fs.writeFileSync(cfgPath, JSON.stringify(diskCfg, null, 2));
|
|
144
|
+
if (ctx.cfg && ctx.cfg.trainer) delete ctx.cfg.trainer.fallback;
|
|
145
|
+
return '✓ trainer fallback cleared';
|
|
146
|
+
}
|
|
147
|
+
const fp = _parseProvModel(registry, spec);
|
|
148
|
+
if (!fp.provider) return `/trainer fallback: could not parse "${spec}"`;
|
|
149
|
+
if (fp.provider !== 'auto' && !_providerLookup(registry, fp.provider)) {
|
|
150
|
+
return `/trainer fallback: unknown provider "${fp.provider}"`;
|
|
151
|
+
}
|
|
152
|
+
diskCfg.trainer = { ...(diskCfg.trainer || {}), fallback: spec };
|
|
153
|
+
try { fs.mkdirSync(ctx.cfgDir, { recursive: true }); } catch {}
|
|
154
|
+
fs.writeFileSync(cfgPath, JSON.stringify(diskCfg, null, 2));
|
|
155
|
+
if (ctx.cfg) ctx.cfg.trainer = { ...diskCfg.trainer };
|
|
156
|
+
return `✓ trainer fallback → ${spec}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (sub === 'clear' || sub === 'unset') {
|
|
160
|
+
const fs = await import('node:fs');
|
|
161
|
+
const path = await import('node:path');
|
|
162
|
+
const cfgPath = path.join(ctx.cfgDir, 'config.json');
|
|
163
|
+
let diskCfg = {};
|
|
164
|
+
try { diskCfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8')); } catch { /* fresh */ }
|
|
165
|
+
delete diskCfg.trainer;
|
|
166
|
+
try { fs.mkdirSync(ctx.cfgDir, { recursive: true }); } catch {}
|
|
167
|
+
fs.writeFileSync(cfgPath, JSON.stringify(diskCfg, null, 2));
|
|
168
|
+
if (ctx.cfg) delete ctx.cfg.trainer;
|
|
169
|
+
return '✓ trainer cleared (will mirror chat provider/model)';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return `/trainer: unknown sub "${sub}" — show|set <p:m>|clear`;
|
|
173
|
+
}
|
package/web/dashboard.js
CHANGED
|
@@ -1409,7 +1409,13 @@
|
|
|
1409
1409
|
}
|
|
1410
1410
|
return 1; // generic PM look
|
|
1411
1411
|
}
|
|
1412
|
-
|
|
1412
|
+
// A user-supplied custom image (set via `lazyclaw agent set-avatar`) wins
|
|
1413
|
+
// over the picked/inferred built-in sprite. rec.avatarImage is already a
|
|
1414
|
+
// ready-to-use src (a remote URL or a daemon-served /agent-avatars/ path).
|
|
1415
|
+
function avatarSrc(rec) {
|
|
1416
|
+
if (rec && rec.avatarImage) return rec.avatarImage;
|
|
1417
|
+
return `/avatars/${String(avatarIndexFor(rec)).padStart(2, '0')}.png`;
|
|
1418
|
+
}
|
|
1413
1419
|
// Build the { name, children[] } tree rooted at the lead (mirrors teamTree).
|
|
1414
1420
|
function buildTeamTree(team, byId) {
|
|
1415
1421
|
const lead = team.lead;
|