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
package/agents.mjs
CHANGED
|
@@ -90,6 +90,16 @@ function defaultShape(name) {
|
|
|
90
90
|
tools: [...DEFAULT_TOOLS],
|
|
91
91
|
tags: [],
|
|
92
92
|
iconEmoji: '',
|
|
93
|
+
// Explicit Team Live sprite choice: an integer 1..20 picks one of the 20
|
|
94
|
+
// built-in pixel-art avatars (web/avatars/NN.png); null lets the dashboard
|
|
95
|
+
// keep inferring one from the agent's name/role/tags. (dashboard.js
|
|
96
|
+
// avatarIndexFor already honours rec.avatar — this is the registry side.)
|
|
97
|
+
avatar: null,
|
|
98
|
+
// Optional custom character image: a ready-to-use <img src> — either a
|
|
99
|
+
// remote http(s) URL or a daemon-served '/agent-avatars/<file>' path for a
|
|
100
|
+
// photo the user supplied (copied under <configDir>/agent-avatars/). null =
|
|
101
|
+
// none. Takes precedence over `avatar` and the keyword inference.
|
|
102
|
+
avatarImage: null,
|
|
93
103
|
// Optional parent agent (hierarchy). '' = top-level. A team's org tree is
|
|
94
104
|
// derived from members' manager links (see teams.teamTree). Validated to
|
|
95
105
|
// reference a registered agent and to never form a cycle.
|
|
@@ -124,6 +134,18 @@ function writeAtomic(filePath, obj) {
|
|
|
124
134
|
const VALID_MEMORY_WRITE = ['auto', 'manual', 'off'];
|
|
125
135
|
const VALID_SKILL_WRITE = ['auto', 'manual', 'off'];
|
|
126
136
|
|
|
137
|
+
// Normalise an explicit avatar choice. null/''/undefined → null (keep the
|
|
138
|
+
// dashboard's keyword inference). A value that parses to an integer 1..20 picks
|
|
139
|
+
// that built-in sprite. Anything else (0, 21, fractional, non-numeric) throws.
|
|
140
|
+
function validateAvatar(v) {
|
|
141
|
+
if (v === undefined || v === null || v === '') return null;
|
|
142
|
+
const n = typeof v === 'number' ? v : Number(String(v).trim());
|
|
143
|
+
if (!Number.isInteger(n) || n < 1 || n > 20) {
|
|
144
|
+
throw new AgentError('avatar must be an integer 1..20 (or null to clear)', 'AGENT_BAD_AVATAR');
|
|
145
|
+
}
|
|
146
|
+
return n;
|
|
147
|
+
}
|
|
148
|
+
|
|
127
149
|
// Validate a proposed `manager` (parent agent) for `name`: it must reference a
|
|
128
150
|
// registered agent, may not be the agent itself, and may not close a cycle
|
|
129
151
|
// (i.e. `name` must not already be an ancestor of the proposed manager).
|
|
@@ -151,7 +173,7 @@ function validateManager(name, manager, configDir) {
|
|
|
151
173
|
return mgr;
|
|
152
174
|
}
|
|
153
175
|
|
|
154
|
-
export function registerAgent({ name, displayName, role = '', provider = 'claude-cli', model = '', tools, tags = [], iconEmoji = '', memoryWrite, memoryMaxChars, skillWrite, manager } = {}, configDir = defaultConfigDir()) {
|
|
176
|
+
export function registerAgent({ name, displayName, role = '', provider = 'claude-cli', model = '', tools, tags = [], iconEmoji = '', avatar, memoryWrite, memoryMaxChars, skillWrite, manager } = {}, configDir = defaultConfigDir()) {
|
|
155
177
|
ensureValidName(name);
|
|
156
178
|
const p = agentPath(name, configDir);
|
|
157
179
|
if (fs.existsSync(p)) {
|
|
@@ -175,6 +197,7 @@ export function registerAgent({ name, displayName, role = '', provider = 'claude
|
|
|
175
197
|
tools: toolsClean,
|
|
176
198
|
tags: Array.isArray(tags) ? tags : [],
|
|
177
199
|
iconEmoji: String(iconEmoji || ''),
|
|
200
|
+
avatar: validateAvatar(avatar),
|
|
178
201
|
memoryWrite: mw,
|
|
179
202
|
memoryMaxChars: Number.isFinite(+memoryMaxChars) && +memoryMaxChars > 0 ? +memoryMaxChars : 12 * 1024,
|
|
180
203
|
skillWrite: sw,
|
|
@@ -220,6 +243,12 @@ export function patchAgent(name, patch, configDir = defaultConfigDir()) {
|
|
|
220
243
|
if (patch.skillWrite !== undefined && !VALID_SKILL_WRITE.includes(patch.skillWrite)) {
|
|
221
244
|
throw new AgentError(`skillWrite must be one of ${VALID_SKILL_WRITE.join(', ')}`, 'AGENT_BAD_SKILL_WRITE');
|
|
222
245
|
}
|
|
246
|
+
if (patch.avatar !== undefined) {
|
|
247
|
+
next.avatar = validateAvatar(patch.avatar);
|
|
248
|
+
}
|
|
249
|
+
if (patch.avatarImage !== undefined) {
|
|
250
|
+
next.avatarImage = (patch.avatarImage == null || patch.avatarImage === '') ? null : String(patch.avatarImage);
|
|
251
|
+
}
|
|
223
252
|
if (patch.manager !== undefined) {
|
|
224
253
|
next.manager = validateManager(name, patch.manager, configDir);
|
|
225
254
|
}
|
|
@@ -227,6 +256,44 @@ export function patchAgent(name, patch, configDir = defaultConfigDir()) {
|
|
|
227
256
|
return next;
|
|
228
257
|
}
|
|
229
258
|
|
|
259
|
+
// Supported custom-avatar image types → their served content-type. svg is
|
|
260
|
+
// deliberately excluded (inline-script XSS risk in an <img>/object context).
|
|
261
|
+
export const AVATAR_IMAGE_TYPES = {
|
|
262
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
263
|
+
'.gif': 'image/gif', '.webp': 'image/webp',
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Point an agent at a custom character image. `src` is either a remote http(s)
|
|
267
|
+
// URL (stored verbatim) or a path to a local image file (copied into
|
|
268
|
+
// <configDir>/agent-avatars/<name><ext> and stored as a daemon-served
|
|
269
|
+
// '/agent-avatars/<name><ext>' path). Returns the patched record. The image
|
|
270
|
+
// takes precedence over the numeric sprite + keyword inference in the dashboard.
|
|
271
|
+
export function setAgentAvatarImage(name, src, configDir = defaultConfigDir()) {
|
|
272
|
+
if (!getAgent(name, configDir)) throw new AgentError(`no agent "${name}"`, 'AGENT_NO_AGENT');
|
|
273
|
+
const s = String(src ?? '').trim();
|
|
274
|
+
if (!s) throw new AgentError('avatar image source required (a file path or http(s) URL)', 'AGENT_BAD_AVATAR_IMAGE');
|
|
275
|
+
if (/^https?:\/\//i.test(s)) {
|
|
276
|
+
return patchAgent(name, { avatarImage: s }, configDir);
|
|
277
|
+
}
|
|
278
|
+
const ext = path.extname(s).toLowerCase();
|
|
279
|
+
if (!AVATAR_IMAGE_TYPES[ext]) {
|
|
280
|
+
throw new AgentError(`unsupported image type "${ext || '(none)'}" — use png/jpg/jpeg/gif/webp or an http(s) URL`, 'AGENT_BAD_AVATAR_IMAGE');
|
|
281
|
+
}
|
|
282
|
+
if (!fs.existsSync(s) || !fs.statSync(s).isFile()) {
|
|
283
|
+
throw new AgentError(`no such image file: ${s}`, 'AGENT_BAD_AVATAR_IMAGE');
|
|
284
|
+
}
|
|
285
|
+
const destDir = path.join(configDir, 'agent-avatars');
|
|
286
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
287
|
+
// Drop any previously stored image for this agent (it may have a different
|
|
288
|
+
// extension) so a re-point doesn't leave a stale file shadowing the new one.
|
|
289
|
+
for (const e of Object.keys(AVATAR_IMAGE_TYPES)) {
|
|
290
|
+
const old = path.join(destDir, `${name}${e}`);
|
|
291
|
+
if (fs.existsSync(old)) fs.unlinkSync(old);
|
|
292
|
+
}
|
|
293
|
+
fs.copyFileSync(s, path.join(destDir, `${name}${ext}`));
|
|
294
|
+
return patchAgent(name, { avatarImage: `/agent-avatars/${name}${ext}` }, configDir);
|
|
295
|
+
}
|
|
296
|
+
|
|
230
297
|
export function removeAgent(name, configDir = defaultConfigDir()) {
|
|
231
298
|
const p = agentPath(name, configDir);
|
|
232
299
|
if (!fs.existsSync(p)) {
|
package/commands/agents.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Multi-agent commands: cmdAgent (one-shot run), cmdTask, cmdTeam, and the
|
|
2
2
|
// agent registry (cmdAgentRegistry), extracted from cli.mjs (Phase D3).
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import fs from 'node:fs';
|
|
5
4
|
import { configPath, readConfig, _resolveAuthKey, _resolveBaseUrl } from '../lib/config.mjs';
|
|
6
5
|
import { ensureRegistry, getRegistry } from '../lib/registry_boot.mjs';
|
|
7
6
|
import { loadDotenvIfAny as _loadDotenvShared } from '../dotenv_min.mjs';
|
|
@@ -474,199 +473,9 @@ export async function cmdTeam(sub, positional, flags = {}) {
|
|
|
474
473
|
}
|
|
475
474
|
}
|
|
476
475
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
const name = positional[0];
|
|
481
|
-
|
|
482
|
-
const emitJson = (obj) => process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
|
|
483
|
-
|
|
484
|
-
switch (sub) {
|
|
485
|
-
case undefined:
|
|
486
|
-
case 'list': {
|
|
487
|
-
emitJson(agentsMod.listAgents(cfgDir));
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
case 'add': {
|
|
491
|
-
if (!name) { console.error('Usage: lazyclaw agent add <name> [--role "..."] [--provider X] [--model Y] [--display "..."] [--manager <agent>] [--tools bash,read,write,grep,skill_view] [--tags a,b] [--skill-write auto|manual|off]'); process.exit(2); }
|
|
492
|
-
const tools = agentsMod.parseToolsFlag(flags.tools);
|
|
493
|
-
try {
|
|
494
|
-
const a = agentsMod.registerAgent({
|
|
495
|
-
name,
|
|
496
|
-
displayName: flags.display || flags['display-name'],
|
|
497
|
-
role: flags.role || '',
|
|
498
|
-
provider: flags.provider || 'claude-cli',
|
|
499
|
-
model: flags.model || '',
|
|
500
|
-
tools: tools === null ? undefined : tools,
|
|
501
|
-
tags: agentsMod.parseToolsFlag(flags.tags) || [],
|
|
502
|
-
skillWrite: flags['skill-write'],
|
|
503
|
-
manager: flags.manager,
|
|
504
|
-
}, cfgDir);
|
|
505
|
-
emitJson(a);
|
|
506
|
-
} catch (err) {
|
|
507
|
-
console.error(`agent add: ${err?.message || err}`);
|
|
508
|
-
process.exit(2);
|
|
509
|
-
}
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
case 'show': {
|
|
513
|
-
if (!name) { console.error('Usage: lazyclaw agent show <name>'); process.exit(2); }
|
|
514
|
-
const a = agentsMod.getAgent(name, cfgDir);
|
|
515
|
-
if (!a) { console.error(`agent show: no agent "${name}"`); process.exit(2); }
|
|
516
|
-
emitJson(a);
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
519
|
-
case 'edit': {
|
|
520
|
-
if (!name) { console.error('Usage: lazyclaw agent edit <name> [--role "..."] [--provider X] [--model Y] [--display "..."] [--tools ...] [--skill-write auto|manual|off] [--memory-write auto|manual|off]'); process.exit(2); }
|
|
521
|
-
const patch = {};
|
|
522
|
-
if (flags.role !== undefined) patch.role = String(flags.role);
|
|
523
|
-
if (flags.provider !== undefined) patch.provider = String(flags.provider);
|
|
524
|
-
if (flags.model !== undefined) patch.model = String(flags.model);
|
|
525
|
-
if (flags.display !== undefined) patch.displayName = String(flags.display);
|
|
526
|
-
if (flags['display-name'] !== undefined) patch.displayName = String(flags['display-name']);
|
|
527
|
-
if (flags.tools !== undefined) patch.tools = agentsMod.parseToolsFlag(flags.tools);
|
|
528
|
-
if (flags.tags !== undefined) patch.tags = agentsMod.parseToolsFlag(flags.tags);
|
|
529
|
-
if (flags['skill-write'] !== undefined) patch.skillWrite = String(flags['skill-write']);
|
|
530
|
-
if (flags['memory-write'] !== undefined) patch.memoryWrite = String(flags['memory-write']);
|
|
531
|
-
if (Object.keys(patch).length === 0) {
|
|
532
|
-
console.error('agent edit: no fields to update');
|
|
533
|
-
process.exit(2);
|
|
534
|
-
}
|
|
535
|
-
try { emitJson(agentsMod.patchAgent(name, patch, cfgDir)); }
|
|
536
|
-
catch (err) { console.error(`agent edit: ${err?.message || err}`); process.exit(2); }
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
case 'remove':
|
|
540
|
-
case 'rm':
|
|
541
|
-
case 'delete': {
|
|
542
|
-
if (!name) { console.error('Usage: lazyclaw agent remove <name>'); process.exit(2); }
|
|
543
|
-
try { emitJson(agentsMod.removeAgent(name, cfgDir)); }
|
|
544
|
-
catch (err) { console.error(`agent remove: ${err?.message || err}`); process.exit(2); }
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
case 'memory': {
|
|
548
|
-
// memory <show|edit|clear> <name>
|
|
549
|
-
const op = positional[0];
|
|
550
|
-
const memName = positional[1];
|
|
551
|
-
if (!op || !memName) {
|
|
552
|
-
console.error('Usage: lazyclaw agent memory <show|edit|clear> <name>');
|
|
553
|
-
process.exit(2);
|
|
554
|
-
}
|
|
555
|
-
const memMod = await import('../mas/agent_memory.mjs');
|
|
556
|
-
try {
|
|
557
|
-
if (op === 'show') {
|
|
558
|
-
const max = Number.isFinite(+flags['max-chars']) && +flags['max-chars'] > 0 ? +flags['max-chars'] : memMod.DEFAULT_MAX_CHARS;
|
|
559
|
-
const text = memMod.readMemory(memName, cfgDir, max);
|
|
560
|
-
if (!text) process.stderr.write(`(no memory for "${memName}")\n`);
|
|
561
|
-
else process.stdout.write(text + (text.endsWith('\n') ? '' : '\n'));
|
|
562
|
-
} else if (op === 'edit') {
|
|
563
|
-
const p = memMod.memoryPath(memName, cfgDir);
|
|
564
|
-
// Ensure file exists so $EDITOR doesn't start with a missing
|
|
565
|
-
// file warning.
|
|
566
|
-
if (!fs.existsSync(p)) {
|
|
567
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
568
|
-
fs.writeFileSync(p, `# ${memName} — memory\n\n`);
|
|
569
|
-
}
|
|
570
|
-
const editor = process.env.EDITOR || 'vi';
|
|
571
|
-
const { spawn } = await import('node:child_process');
|
|
572
|
-
await new Promise((resolve) => {
|
|
573
|
-
const ch = spawn(editor, [p], { stdio: 'inherit' });
|
|
574
|
-
ch.on('close', () => resolve());
|
|
575
|
-
});
|
|
576
|
-
process.stdout.write(`edited ${p}\n`);
|
|
577
|
-
} else if (op === 'clear') {
|
|
578
|
-
const removed = memMod.clear(memName, cfgDir);
|
|
579
|
-
process.stdout.write(removed ? `cleared memory for "${memName}"\n` : `(no memory for "${memName}")\n`);
|
|
580
|
-
} else {
|
|
581
|
-
console.error(`Usage: lazyclaw agent memory <show|edit|clear> <name>`);
|
|
582
|
-
process.exit(2);
|
|
583
|
-
}
|
|
584
|
-
} catch (err) {
|
|
585
|
-
console.error(`agent memory ${op}: ${err?.message || err}`);
|
|
586
|
-
process.exit(2);
|
|
587
|
-
}
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
case 'reflect': {
|
|
591
|
-
const aname = positional[0];
|
|
592
|
-
const taskId = flags.task || positional[1];
|
|
593
|
-
if (!aname || !taskId) {
|
|
594
|
-
console.error('Usage: lazyclaw agent reflect <name> --task <id>');
|
|
595
|
-
process.exit(2);
|
|
596
|
-
}
|
|
597
|
-
const tasksMod = await import('../tasks.mjs');
|
|
598
|
-
const memMod = await import('../mas/agent_memory.mjs');
|
|
599
|
-
const a = agentsMod.getAgent(aname, cfgDir);
|
|
600
|
-
if (!a) { console.error(`agent reflect: no agent "${aname}"`); process.exit(2); }
|
|
601
|
-
const task = tasksMod.getTask(taskId, cfgDir);
|
|
602
|
-
if (!task) { console.error(`agent reflect: no task "${taskId}"`); process.exit(2); }
|
|
603
|
-
try { _loadDotenvIfAny(cfgDir); } catch { /* best-effort */ }
|
|
604
|
-
const cfg = readConfig();
|
|
605
|
-
const apiKey = _resolveAuthKey(cfg, a.provider);
|
|
606
|
-
const baseUrl = _resolveBaseUrl(a.provider);
|
|
607
|
-
try {
|
|
608
|
-
const body = await memMod.reflectOnce({ agent: a, task, apiKey, baseUrl });
|
|
609
|
-
if (!body || !body.trim()) {
|
|
610
|
-
process.stderr.write('reflection returned empty body — nothing to write\n');
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
if (!flags['dry-run']) {
|
|
614
|
-
memMod.prependEntry(aname, { taskId: task.id, title: task.title, body }, cfgDir);
|
|
615
|
-
}
|
|
616
|
-
process.stdout.write(body + (body.endsWith('\n') ? '' : '\n'));
|
|
617
|
-
} catch (err) {
|
|
618
|
-
console.error(`agent reflect: ${err?.message || err}`);
|
|
619
|
-
process.exit(2);
|
|
620
|
-
}
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
case 'skill-synth': {
|
|
624
|
-
const aname = positional[0];
|
|
625
|
-
const taskId = flags.task || positional[1];
|
|
626
|
-
if (!aname || !taskId) {
|
|
627
|
-
console.error('Usage: lazyclaw agent skill-synth <name> --task <id> [--dry-run]');
|
|
628
|
-
process.exit(2);
|
|
629
|
-
}
|
|
630
|
-
const tasksMod = await import('../tasks.mjs');
|
|
631
|
-
const synthMod = await import('../mas/skill_synth.mjs');
|
|
632
|
-
const skillsMod = await import('../skills.mjs');
|
|
633
|
-
const a = agentsMod.getAgent(aname, cfgDir);
|
|
634
|
-
if (!a) { console.error(`agent skill-synth: no agent "${aname}"`); process.exit(2); }
|
|
635
|
-
const task = tasksMod.getTask(taskId, cfgDir);
|
|
636
|
-
if (!task) { console.error(`agent skill-synth: no task "${taskId}"`); process.exit(2); }
|
|
637
|
-
try { _loadDotenvIfAny(cfgDir); } catch { /* best-effort */ }
|
|
638
|
-
const cfg = readConfig();
|
|
639
|
-
const apiKey = _resolveAuthKey(cfg, a.provider);
|
|
640
|
-
const baseUrl = _resolveBaseUrl(a.provider);
|
|
641
|
-
try {
|
|
642
|
-
const result = await synthMod.synthesizeSkill({ agent: a, task, apiKey, baseUrl });
|
|
643
|
-
if (!result) {
|
|
644
|
-
process.stderr.write('skill synthesis produced nothing worth saving\n');
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
if (!flags['dry-run']) {
|
|
648
|
-
// installSynthesized reserves a collision-free name (never
|
|
649
|
-
// clobbers a human-authored skill) and version-bumps when it
|
|
650
|
-
// improves its own prior skill.
|
|
651
|
-
const installed = synthMod.installSynthesized(
|
|
652
|
-
{ name: result.name, description: result.description, body: result.body, sourceTask: task.id },
|
|
653
|
-
cfgDir,
|
|
654
|
-
);
|
|
655
|
-
emitJson({ skill: installed.skill, description: result.description, version: installed.version, path: installed.path });
|
|
656
|
-
} else {
|
|
657
|
-
process.stdout.write(result.doc + (result.doc.endsWith('\n') ? '' : '\n'));
|
|
658
|
-
}
|
|
659
|
-
} catch (err) {
|
|
660
|
-
console.error(`agent skill-synth: ${err?.message || err}`);
|
|
661
|
-
process.exit(2);
|
|
662
|
-
}
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
default:
|
|
666
|
-
console.error('Usage: lazyclaw agent <add|list|show|edit|remove|memory|reflect|skill-synth> ...');
|
|
667
|
-
process.exit(2);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
476
|
+
// cmdAgentRegistry lives in a sibling module to keep this file under the
|
|
477
|
+
// file-size gate. Re-exported here so cli.mjs's named import is unchanged.
|
|
478
|
+
export { cmdAgentRegistry } from './agents_registry.mjs';
|
|
670
479
|
|
|
671
480
|
// Best-effort .env loader for ~/.lazyclaw/.env. Only sets keys that are
|
|
672
481
|
// not already present in process.env (so a shell-level export wins).
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Agent registry command (cmdAgentRegistry): the agent
|
|
2
|
+
// add/list/show/edit/remove/memory/reflect/skill-synth handler. Extracted
|
|
3
|
+
// from commands/agents.mjs as a sibling module for the file-size gate.
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import { configPath, readConfig, _resolveAuthKey, _resolveBaseUrl } from '../lib/config.mjs';
|
|
7
|
+
import { loadDotenvIfAny } from '../dotenv_min.mjs';
|
|
8
|
+
|
|
9
|
+
// Thin .env loader wrapper kept local so the module stays self-contained
|
|
10
|
+
// (importing the wrapper back from agents.mjs would create a cycle).
|
|
11
|
+
function _loadDotenvIfAny(cfgDir) { return loadDotenvIfAny(cfgDir); }
|
|
12
|
+
|
|
13
|
+
export async function cmdAgentRegistry(sub, positional, flags = {}) {
|
|
14
|
+
const agentsMod = await import('../agents.mjs');
|
|
15
|
+
const cfgDir = path.dirname(configPath());
|
|
16
|
+
const name = positional[0];
|
|
17
|
+
|
|
18
|
+
const emitJson = (obj) => process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
|
|
19
|
+
|
|
20
|
+
switch (sub) {
|
|
21
|
+
case undefined:
|
|
22
|
+
case 'list': {
|
|
23
|
+
emitJson(agentsMod.listAgents(cfgDir));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
case 'add': {
|
|
27
|
+
if (!name) { console.error('Usage: lazyclaw agent add <name> [--role "..."] [--provider X] [--model Y] [--display "..."] [--manager <agent>] [--tools bash,read,write,grep,skill_view] [--tags a,b] [--skill-write auto|manual|off]'); process.exit(2); }
|
|
28
|
+
const tools = agentsMod.parseToolsFlag(flags.tools);
|
|
29
|
+
try {
|
|
30
|
+
const a = agentsMod.registerAgent({
|
|
31
|
+
name,
|
|
32
|
+
displayName: flags.display || flags['display-name'],
|
|
33
|
+
role: flags.role || '',
|
|
34
|
+
provider: flags.provider || 'claude-cli',
|
|
35
|
+
model: flags.model || '',
|
|
36
|
+
tools: tools === null ? undefined : tools,
|
|
37
|
+
tags: agentsMod.parseToolsFlag(flags.tags) || [],
|
|
38
|
+
avatar: flags.avatar,
|
|
39
|
+
skillWrite: flags['skill-write'],
|
|
40
|
+
manager: flags.manager,
|
|
41
|
+
}, cfgDir);
|
|
42
|
+
emitJson(a);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error(`agent add: ${err?.message || err}`);
|
|
45
|
+
process.exit(2);
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
case 'show': {
|
|
50
|
+
if (!name) { console.error('Usage: lazyclaw agent show <name>'); process.exit(2); }
|
|
51
|
+
const a = agentsMod.getAgent(name, cfgDir);
|
|
52
|
+
if (!a) { console.error(`agent show: no agent "${name}"`); process.exit(2); }
|
|
53
|
+
emitJson(a);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
case 'edit': {
|
|
57
|
+
if (!name) { console.error('Usage: lazyclaw agent edit <name> [--role "..."] [--provider X] [--model Y] [--display "..."] [--tools ...] [--skill-write auto|manual|off] [--memory-write auto|manual|off]'); process.exit(2); }
|
|
58
|
+
const patch = {};
|
|
59
|
+
if (flags.role !== undefined) patch.role = String(flags.role);
|
|
60
|
+
if (flags.provider !== undefined) patch.provider = String(flags.provider);
|
|
61
|
+
if (flags.model !== undefined) patch.model = String(flags.model);
|
|
62
|
+
if (flags.display !== undefined) patch.displayName = String(flags.display);
|
|
63
|
+
if (flags['display-name'] !== undefined) patch.displayName = String(flags['display-name']);
|
|
64
|
+
if (flags.tools !== undefined) patch.tools = agentsMod.parseToolsFlag(flags.tools);
|
|
65
|
+
if (flags.tags !== undefined) patch.tags = agentsMod.parseToolsFlag(flags.tags);
|
|
66
|
+
if (flags['skill-write'] !== undefined) patch.skillWrite = String(flags['skill-write']);
|
|
67
|
+
if (flags['memory-write'] !== undefined) patch.memoryWrite = String(flags['memory-write']);
|
|
68
|
+
if (flags.avatar !== undefined) patch.avatar = /^(none|clear|off)$/i.test(String(flags.avatar)) ? null : flags.avatar;
|
|
69
|
+
if (Object.keys(patch).length === 0) {
|
|
70
|
+
console.error('agent edit: no fields to update');
|
|
71
|
+
process.exit(2);
|
|
72
|
+
}
|
|
73
|
+
try { emitJson(agentsMod.patchAgent(name, patch, cfgDir)); }
|
|
74
|
+
catch (err) { console.error(`agent edit: ${err?.message || err}`); process.exit(2); }
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
case 'set-avatar': {
|
|
78
|
+
const val = positional[1];
|
|
79
|
+
if (!name || val === undefined) {
|
|
80
|
+
console.error('Usage: lazyclaw agent set-avatar <name> <1-20 | none | <image-file> | http(s)://url>\n 1-20 pick a built-in pixel-art sprite\n none keep the role-inferred default\n file/url use a custom character photo');
|
|
81
|
+
process.exit(2);
|
|
82
|
+
}
|
|
83
|
+
const v = String(val).trim();
|
|
84
|
+
try {
|
|
85
|
+
if (/^(none|clear|off)$/i.test(v)) {
|
|
86
|
+
emitJson(agentsMod.patchAgent(name, { avatar: null, avatarImage: null }, cfgDir));
|
|
87
|
+
} else if (/^\d+$/.test(v)) {
|
|
88
|
+
// a built-in sprite index — clear any custom image so it shows through
|
|
89
|
+
emitJson(agentsMod.patchAgent(name, { avatar: v, avatarImage: null }, cfgDir));
|
|
90
|
+
} else {
|
|
91
|
+
// a local image file or a remote URL
|
|
92
|
+
emitJson(agentsMod.setAgentAvatarImage(name, val, cfgDir));
|
|
93
|
+
}
|
|
94
|
+
} catch (err) { console.error(`agent set-avatar: ${err?.message || err}`); process.exit(2); }
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
case 'remove':
|
|
98
|
+
case 'rm':
|
|
99
|
+
case 'delete': {
|
|
100
|
+
if (!name) { console.error('Usage: lazyclaw agent remove <name>'); process.exit(2); }
|
|
101
|
+
try { emitJson(agentsMod.removeAgent(name, cfgDir)); }
|
|
102
|
+
catch (err) { console.error(`agent remove: ${err?.message || err}`); process.exit(2); }
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
case 'memory': {
|
|
106
|
+
// memory <show|edit|clear> <name>
|
|
107
|
+
const op = positional[0];
|
|
108
|
+
const memName = positional[1];
|
|
109
|
+
if (!op || !memName) {
|
|
110
|
+
console.error('Usage: lazyclaw agent memory <show|edit|clear> <name>');
|
|
111
|
+
process.exit(2);
|
|
112
|
+
}
|
|
113
|
+
const memMod = await import('../mas/agent_memory.mjs');
|
|
114
|
+
try {
|
|
115
|
+
if (op === 'show') {
|
|
116
|
+
const max = Number.isFinite(+flags['max-chars']) && +flags['max-chars'] > 0 ? +flags['max-chars'] : memMod.DEFAULT_MAX_CHARS;
|
|
117
|
+
const text = memMod.readMemory(memName, cfgDir, max);
|
|
118
|
+
if (!text) process.stderr.write(`(no memory for "${memName}")\n`);
|
|
119
|
+
else process.stdout.write(text + (text.endsWith('\n') ? '' : '\n'));
|
|
120
|
+
} else if (op === 'edit') {
|
|
121
|
+
const p = memMod.memoryPath(memName, cfgDir);
|
|
122
|
+
// Ensure file exists so $EDITOR doesn't start with a missing
|
|
123
|
+
// file warning.
|
|
124
|
+
if (!fs.existsSync(p)) {
|
|
125
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
126
|
+
fs.writeFileSync(p, `# ${memName} — memory\n\n`);
|
|
127
|
+
}
|
|
128
|
+
const editor = process.env.EDITOR || 'vi';
|
|
129
|
+
const { spawn } = await import('node:child_process');
|
|
130
|
+
await new Promise((resolve) => {
|
|
131
|
+
const ch = spawn(editor, [p], { stdio: 'inherit' });
|
|
132
|
+
ch.on('close', () => resolve());
|
|
133
|
+
});
|
|
134
|
+
process.stdout.write(`edited ${p}\n`);
|
|
135
|
+
} else if (op === 'clear') {
|
|
136
|
+
const removed = memMod.clear(memName, cfgDir);
|
|
137
|
+
process.stdout.write(removed ? `cleared memory for "${memName}"\n` : `(no memory for "${memName}")\n`);
|
|
138
|
+
} else {
|
|
139
|
+
console.error(`Usage: lazyclaw agent memory <show|edit|clear> <name>`);
|
|
140
|
+
process.exit(2);
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error(`agent memory ${op}: ${err?.message || err}`);
|
|
144
|
+
process.exit(2);
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
case 'reflect': {
|
|
149
|
+
const aname = positional[0];
|
|
150
|
+
const taskId = flags.task || positional[1];
|
|
151
|
+
if (!aname || !taskId) {
|
|
152
|
+
console.error('Usage: lazyclaw agent reflect <name> --task <id>');
|
|
153
|
+
process.exit(2);
|
|
154
|
+
}
|
|
155
|
+
const tasksMod = await import('../tasks.mjs');
|
|
156
|
+
const memMod = await import('../mas/agent_memory.mjs');
|
|
157
|
+
const a = agentsMod.getAgent(aname, cfgDir);
|
|
158
|
+
if (!a) { console.error(`agent reflect: no agent "${aname}"`); process.exit(2); }
|
|
159
|
+
const task = tasksMod.getTask(taskId, cfgDir);
|
|
160
|
+
if (!task) { console.error(`agent reflect: no task "${taskId}"`); process.exit(2); }
|
|
161
|
+
try { _loadDotenvIfAny(cfgDir); } catch { /* best-effort */ }
|
|
162
|
+
const cfg = readConfig();
|
|
163
|
+
const apiKey = _resolveAuthKey(cfg, a.provider);
|
|
164
|
+
const baseUrl = _resolveBaseUrl(a.provider);
|
|
165
|
+
try {
|
|
166
|
+
const body = await memMod.reflectOnce({ agent: a, task, apiKey, baseUrl });
|
|
167
|
+
if (!body || !body.trim()) {
|
|
168
|
+
process.stderr.write('reflection returned empty body — nothing to write\n');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (!flags['dry-run']) {
|
|
172
|
+
memMod.prependEntry(aname, { taskId: task.id, title: task.title, body }, cfgDir);
|
|
173
|
+
}
|
|
174
|
+
process.stdout.write(body + (body.endsWith('\n') ? '' : '\n'));
|
|
175
|
+
} catch (err) {
|
|
176
|
+
console.error(`agent reflect: ${err?.message || err}`);
|
|
177
|
+
process.exit(2);
|
|
178
|
+
}
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
case 'skill-synth': {
|
|
182
|
+
const aname = positional[0];
|
|
183
|
+
const taskId = flags.task || positional[1];
|
|
184
|
+
if (!aname || !taskId) {
|
|
185
|
+
console.error('Usage: lazyclaw agent skill-synth <name> --task <id> [--dry-run]');
|
|
186
|
+
process.exit(2);
|
|
187
|
+
}
|
|
188
|
+
const tasksMod = await import('../tasks.mjs');
|
|
189
|
+
const synthMod = await import('../mas/skill_synth.mjs');
|
|
190
|
+
const skillsMod = await import('../skills.mjs');
|
|
191
|
+
const a = agentsMod.getAgent(aname, cfgDir);
|
|
192
|
+
if (!a) { console.error(`agent skill-synth: no agent "${aname}"`); process.exit(2); }
|
|
193
|
+
const task = tasksMod.getTask(taskId, cfgDir);
|
|
194
|
+
if (!task) { console.error(`agent skill-synth: no task "${taskId}"`); process.exit(2); }
|
|
195
|
+
try { _loadDotenvIfAny(cfgDir); } catch { /* best-effort */ }
|
|
196
|
+
const cfg = readConfig();
|
|
197
|
+
const apiKey = _resolveAuthKey(cfg, a.provider);
|
|
198
|
+
const baseUrl = _resolveBaseUrl(a.provider);
|
|
199
|
+
try {
|
|
200
|
+
const result = await synthMod.synthesizeSkill({ agent: a, task, apiKey, baseUrl });
|
|
201
|
+
if (!result) {
|
|
202
|
+
process.stderr.write('skill synthesis produced nothing worth saving\n');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (!flags['dry-run']) {
|
|
206
|
+
// installSynthesized reserves a collision-free name (never
|
|
207
|
+
// clobbers a human-authored skill) and version-bumps when it
|
|
208
|
+
// improves its own prior skill.
|
|
209
|
+
const installed = synthMod.installSynthesized(
|
|
210
|
+
{ name: result.name, description: result.description, body: result.body, sourceTask: task.id },
|
|
211
|
+
cfgDir,
|
|
212
|
+
);
|
|
213
|
+
emitJson({ skill: installed.skill, description: result.description, version: installed.version, path: installed.path });
|
|
214
|
+
} else {
|
|
215
|
+
process.stdout.write(result.doc + (result.doc.endsWith('\n') ? '' : '\n'));
|
|
216
|
+
}
|
|
217
|
+
} catch (err) {
|
|
218
|
+
console.error(`agent skill-synth: ${err?.message || err}`);
|
|
219
|
+
process.exit(2);
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
default:
|
|
224
|
+
console.error('Usage: lazyclaw agent <add|list|show|edit|remove|memory|reflect|skill-synth> ...');
|
|
225
|
+
process.exit(2);
|
|
226
|
+
}
|
|
227
|
+
}
|
package/commands/automation.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Automation commands: cron schedules, detached loop workers, and goals,
|
|
2
|
-
// extracted from cli.mjs (Phase D3).
|
|
3
|
-
//
|
|
2
|
+
// extracted from cli.mjs (Phase D3). The `loops` subcommands and their
|
|
3
|
+
// _killLog/KILL_ESCALATE_MS escalation state live in ./automation_loops.mjs
|
|
4
|
+
// (re-exported below) to keep this file under the size gate.
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { configPath, readConfig, writeConfig, _resolveAuthKey } from '../lib/config.mjs';
|
|
6
7
|
import { ensureRegistry, getRegistry } from '../lib/registry_boot.mjs';
|
|
@@ -266,93 +267,11 @@ export async function cmdLoop(prompt, flags = {}) {
|
|
|
266
267
|
}
|
|
267
268
|
}
|
|
268
269
|
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
const _killLog = new Map();
|
|
275
|
-
const KILL_ESCALATE_MS = 5000;
|
|
276
|
-
|
|
277
|
-
export async function cmdLoops(sub, positional, flags = {}) {
|
|
278
|
-
const loopsMod = await import('../loops.mjs');
|
|
279
|
-
const cfgDir = path.dirname(configPath());
|
|
280
|
-
switch (sub) {
|
|
281
|
-
case undefined:
|
|
282
|
-
case 'list': {
|
|
283
|
-
const items = loopsMod.listLoops(cfgDir).map(loopsMod.reconcileStatus);
|
|
284
|
-
console.log(JSON.stringify(items, null, 2));
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
case 'show': {
|
|
288
|
-
const id = positional[0];
|
|
289
|
-
if (!id) { console.error('Usage: lazyclaw loops show <id>'); process.exit(2); }
|
|
290
|
-
const meta = loopsMod.reconcileStatus(loopsMod.readMeta(id, cfgDir));
|
|
291
|
-
if (!meta) { console.error(`no loop "${id}"`); process.exit(1); }
|
|
292
|
-
const iterations = loopsMod.readIterations(id, cfgDir);
|
|
293
|
-
const result = loopsMod.readResult(id, cfgDir);
|
|
294
|
-
console.log(JSON.stringify({ id, meta, iterations, result }, null, 2));
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
case 'kill': {
|
|
298
|
-
const id = positional[0];
|
|
299
|
-
if (!id) { console.error('Usage: lazyclaw loops kill <id>'); process.exit(2); }
|
|
300
|
-
const meta = loopsMod.readMeta(id, cfgDir);
|
|
301
|
-
if (!meta) { console.error(`no loop "${id}"`); process.exit(1); }
|
|
302
|
-
if (!meta.pid) { console.error(`loop "${id}" has no pid`); process.exit(1); }
|
|
303
|
-
const last = _killLog.get(id) || 0;
|
|
304
|
-
const now = Date.now();
|
|
305
|
-
const escalate = (now - last) < KILL_ESCALATE_MS && last > 0;
|
|
306
|
-
const sig = escalate ? 'SIGKILL' : 'SIGTERM';
|
|
307
|
-
try { process.kill(meta.pid, sig); }
|
|
308
|
-
catch (e) {
|
|
309
|
-
if (e?.code !== 'ESRCH') throw e;
|
|
310
|
-
// Already gone — reconcile and report.
|
|
311
|
-
loopsMod.patchMeta(id, { status: 'killed', finishedAt: new Date().toISOString() }, cfgDir);
|
|
312
|
-
console.log(JSON.stringify({ id, pid: meta.pid, signal: sig, status: 'already_gone' }));
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
_killLog.set(id, now);
|
|
316
|
-
console.log(JSON.stringify({ id, pid: meta.pid, signal: sig, escalated: escalate }));
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
case 'tail': {
|
|
320
|
-
const id = positional[0];
|
|
321
|
-
if (!id) { console.error('Usage: lazyclaw loops tail <id>'); process.exit(2); }
|
|
322
|
-
const dir = loopsMod.loopDir(id, cfgDir);
|
|
323
|
-
const logPath = path.join(dir, 'iterations.log');
|
|
324
|
-
const fs = await import('node:fs');
|
|
325
|
-
if (!fs.existsSync(dir)) { console.error(`no loop "${id}"`); process.exit(1); }
|
|
326
|
-
// Print everything already on disk first, then poll for new lines
|
|
327
|
-
// until the worker exits / status is no longer "running".
|
|
328
|
-
let offset = 0;
|
|
329
|
-
if (fs.existsSync(logPath)) {
|
|
330
|
-
const buf = fs.readFileSync(logPath, 'utf8');
|
|
331
|
-
process.stdout.write(buf);
|
|
332
|
-
offset = buf.length;
|
|
333
|
-
}
|
|
334
|
-
const pollMs = Number(flags['poll-ms']) || 250;
|
|
335
|
-
const maxMs = Number(flags['max-wait-ms']) || 0; // 0 = wait indefinitely
|
|
336
|
-
const startedAt = Date.now();
|
|
337
|
-
while (true) {
|
|
338
|
-
await new Promise(r => setTimeout(r, pollMs));
|
|
339
|
-
let cur = '';
|
|
340
|
-
try { cur = fs.readFileSync(logPath, 'utf8'); } catch { /* file may briefly not exist */ }
|
|
341
|
-
if (cur.length > offset) {
|
|
342
|
-
process.stdout.write(cur.slice(offset));
|
|
343
|
-
offset = cur.length;
|
|
344
|
-
}
|
|
345
|
-
const meta = loopsMod.reconcileStatus(loopsMod.readMeta(id, cfgDir));
|
|
346
|
-
if (!meta || meta.status !== 'running') break;
|
|
347
|
-
if (maxMs > 0 && Date.now() - startedAt > maxMs) break;
|
|
348
|
-
}
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
default:
|
|
352
|
-
console.error('Usage: lazyclaw loops <list|show|kill|tail> ...');
|
|
353
|
-
process.exit(2);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
270
|
+
// `lazyclaw loops <list|show|kill|tail>` and its private _killLog/
|
|
271
|
+
// KILL_ESCALATE_MS escalation state live in a sibling module to keep this
|
|
272
|
+
// file under the size gate. Re-exported so cli.mjs's named import keeps
|
|
273
|
+
// resolving against ./commands/automation.mjs.
|
|
274
|
+
export { cmdLoops } from './automation_loops.mjs';
|
|
356
275
|
|
|
357
276
|
// Install (or refresh) the system scheduler entry that fires
|
|
358
277
|
// `lazyclaw goal tick <name>` on a schedule. Writes to cfg.cron and to
|