lazyclaw 6.4.0 → 6.5.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/agents.mjs +3 -194
- package/commands/agents_registry.mjs +205 -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.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/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,205 @@
|
|
|
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
|
+
skillWrite: flags['skill-write'],
|
|
39
|
+
manager: flags.manager,
|
|
40
|
+
}, cfgDir);
|
|
41
|
+
emitJson(a);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error(`agent add: ${err?.message || err}`);
|
|
44
|
+
process.exit(2);
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
case 'show': {
|
|
49
|
+
if (!name) { console.error('Usage: lazyclaw agent show <name>'); process.exit(2); }
|
|
50
|
+
const a = agentsMod.getAgent(name, cfgDir);
|
|
51
|
+
if (!a) { console.error(`agent show: no agent "${name}"`); process.exit(2); }
|
|
52
|
+
emitJson(a);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
case 'edit': {
|
|
56
|
+
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); }
|
|
57
|
+
const patch = {};
|
|
58
|
+
if (flags.role !== undefined) patch.role = String(flags.role);
|
|
59
|
+
if (flags.provider !== undefined) patch.provider = String(flags.provider);
|
|
60
|
+
if (flags.model !== undefined) patch.model = String(flags.model);
|
|
61
|
+
if (flags.display !== undefined) patch.displayName = String(flags.display);
|
|
62
|
+
if (flags['display-name'] !== undefined) patch.displayName = String(flags['display-name']);
|
|
63
|
+
if (flags.tools !== undefined) patch.tools = agentsMod.parseToolsFlag(flags.tools);
|
|
64
|
+
if (flags.tags !== undefined) patch.tags = agentsMod.parseToolsFlag(flags.tags);
|
|
65
|
+
if (flags['skill-write'] !== undefined) patch.skillWrite = String(flags['skill-write']);
|
|
66
|
+
if (flags['memory-write'] !== undefined) patch.memoryWrite = String(flags['memory-write']);
|
|
67
|
+
if (Object.keys(patch).length === 0) {
|
|
68
|
+
console.error('agent edit: no fields to update');
|
|
69
|
+
process.exit(2);
|
|
70
|
+
}
|
|
71
|
+
try { emitJson(agentsMod.patchAgent(name, patch, cfgDir)); }
|
|
72
|
+
catch (err) { console.error(`agent edit: ${err?.message || err}`); process.exit(2); }
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
case 'remove':
|
|
76
|
+
case 'rm':
|
|
77
|
+
case 'delete': {
|
|
78
|
+
if (!name) { console.error('Usage: lazyclaw agent remove <name>'); process.exit(2); }
|
|
79
|
+
try { emitJson(agentsMod.removeAgent(name, cfgDir)); }
|
|
80
|
+
catch (err) { console.error(`agent remove: ${err?.message || err}`); process.exit(2); }
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
case 'memory': {
|
|
84
|
+
// memory <show|edit|clear> <name>
|
|
85
|
+
const op = positional[0];
|
|
86
|
+
const memName = positional[1];
|
|
87
|
+
if (!op || !memName) {
|
|
88
|
+
console.error('Usage: lazyclaw agent memory <show|edit|clear> <name>');
|
|
89
|
+
process.exit(2);
|
|
90
|
+
}
|
|
91
|
+
const memMod = await import('../mas/agent_memory.mjs');
|
|
92
|
+
try {
|
|
93
|
+
if (op === 'show') {
|
|
94
|
+
const max = Number.isFinite(+flags['max-chars']) && +flags['max-chars'] > 0 ? +flags['max-chars'] : memMod.DEFAULT_MAX_CHARS;
|
|
95
|
+
const text = memMod.readMemory(memName, cfgDir, max);
|
|
96
|
+
if (!text) process.stderr.write(`(no memory for "${memName}")\n`);
|
|
97
|
+
else process.stdout.write(text + (text.endsWith('\n') ? '' : '\n'));
|
|
98
|
+
} else if (op === 'edit') {
|
|
99
|
+
const p = memMod.memoryPath(memName, cfgDir);
|
|
100
|
+
// Ensure file exists so $EDITOR doesn't start with a missing
|
|
101
|
+
// file warning.
|
|
102
|
+
if (!fs.existsSync(p)) {
|
|
103
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
104
|
+
fs.writeFileSync(p, `# ${memName} — memory\n\n`);
|
|
105
|
+
}
|
|
106
|
+
const editor = process.env.EDITOR || 'vi';
|
|
107
|
+
const { spawn } = await import('node:child_process');
|
|
108
|
+
await new Promise((resolve) => {
|
|
109
|
+
const ch = spawn(editor, [p], { stdio: 'inherit' });
|
|
110
|
+
ch.on('close', () => resolve());
|
|
111
|
+
});
|
|
112
|
+
process.stdout.write(`edited ${p}\n`);
|
|
113
|
+
} else if (op === 'clear') {
|
|
114
|
+
const removed = memMod.clear(memName, cfgDir);
|
|
115
|
+
process.stdout.write(removed ? `cleared memory for "${memName}"\n` : `(no memory for "${memName}")\n`);
|
|
116
|
+
} else {
|
|
117
|
+
console.error(`Usage: lazyclaw agent memory <show|edit|clear> <name>`);
|
|
118
|
+
process.exit(2);
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error(`agent memory ${op}: ${err?.message || err}`);
|
|
122
|
+
process.exit(2);
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
case 'reflect': {
|
|
127
|
+
const aname = positional[0];
|
|
128
|
+
const taskId = flags.task || positional[1];
|
|
129
|
+
if (!aname || !taskId) {
|
|
130
|
+
console.error('Usage: lazyclaw agent reflect <name> --task <id>');
|
|
131
|
+
process.exit(2);
|
|
132
|
+
}
|
|
133
|
+
const tasksMod = await import('../tasks.mjs');
|
|
134
|
+
const memMod = await import('../mas/agent_memory.mjs');
|
|
135
|
+
const a = agentsMod.getAgent(aname, cfgDir);
|
|
136
|
+
if (!a) { console.error(`agent reflect: no agent "${aname}"`); process.exit(2); }
|
|
137
|
+
const task = tasksMod.getTask(taskId, cfgDir);
|
|
138
|
+
if (!task) { console.error(`agent reflect: no task "${taskId}"`); process.exit(2); }
|
|
139
|
+
try { _loadDotenvIfAny(cfgDir); } catch { /* best-effort */ }
|
|
140
|
+
const cfg = readConfig();
|
|
141
|
+
const apiKey = _resolveAuthKey(cfg, a.provider);
|
|
142
|
+
const baseUrl = _resolveBaseUrl(a.provider);
|
|
143
|
+
try {
|
|
144
|
+
const body = await memMod.reflectOnce({ agent: a, task, apiKey, baseUrl });
|
|
145
|
+
if (!body || !body.trim()) {
|
|
146
|
+
process.stderr.write('reflection returned empty body — nothing to write\n');
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (!flags['dry-run']) {
|
|
150
|
+
memMod.prependEntry(aname, { taskId: task.id, title: task.title, body }, cfgDir);
|
|
151
|
+
}
|
|
152
|
+
process.stdout.write(body + (body.endsWith('\n') ? '' : '\n'));
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.error(`agent reflect: ${err?.message || err}`);
|
|
155
|
+
process.exit(2);
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
case 'skill-synth': {
|
|
160
|
+
const aname = positional[0];
|
|
161
|
+
const taskId = flags.task || positional[1];
|
|
162
|
+
if (!aname || !taskId) {
|
|
163
|
+
console.error('Usage: lazyclaw agent skill-synth <name> --task <id> [--dry-run]');
|
|
164
|
+
process.exit(2);
|
|
165
|
+
}
|
|
166
|
+
const tasksMod = await import('../tasks.mjs');
|
|
167
|
+
const synthMod = await import('../mas/skill_synth.mjs');
|
|
168
|
+
const skillsMod = await import('../skills.mjs');
|
|
169
|
+
const a = agentsMod.getAgent(aname, cfgDir);
|
|
170
|
+
if (!a) { console.error(`agent skill-synth: no agent "${aname}"`); process.exit(2); }
|
|
171
|
+
const task = tasksMod.getTask(taskId, cfgDir);
|
|
172
|
+
if (!task) { console.error(`agent skill-synth: no task "${taskId}"`); process.exit(2); }
|
|
173
|
+
try { _loadDotenvIfAny(cfgDir); } catch { /* best-effort */ }
|
|
174
|
+
const cfg = readConfig();
|
|
175
|
+
const apiKey = _resolveAuthKey(cfg, a.provider);
|
|
176
|
+
const baseUrl = _resolveBaseUrl(a.provider);
|
|
177
|
+
try {
|
|
178
|
+
const result = await synthMod.synthesizeSkill({ agent: a, task, apiKey, baseUrl });
|
|
179
|
+
if (!result) {
|
|
180
|
+
process.stderr.write('skill synthesis produced nothing worth saving\n');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (!flags['dry-run']) {
|
|
184
|
+
// installSynthesized reserves a collision-free name (never
|
|
185
|
+
// clobbers a human-authored skill) and version-bumps when it
|
|
186
|
+
// improves its own prior skill.
|
|
187
|
+
const installed = synthMod.installSynthesized(
|
|
188
|
+
{ name: result.name, description: result.description, body: result.body, sourceTask: task.id },
|
|
189
|
+
cfgDir,
|
|
190
|
+
);
|
|
191
|
+
emitJson({ skill: installed.skill, description: result.description, version: installed.version, path: installed.path });
|
|
192
|
+
} else {
|
|
193
|
+
process.stdout.write(result.doc + (result.doc.endsWith('\n') ? '' : '\n'));
|
|
194
|
+
}
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.error(`agent skill-synth: ${err?.message || err}`);
|
|
197
|
+
process.exit(2);
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
default:
|
|
202
|
+
console.error('Usage: lazyclaw agent <add|list|show|edit|remove|memory|reflect|skill-synth> ...');
|
|
203
|
+
process.exit(2);
|
|
204
|
+
}
|
|
205
|
+
}
|
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
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Detached loop-worker management commands, extracted from
|
|
2
|
+
// commands/automation.mjs to keep that file under the size gate. Owns the
|
|
3
|
+
// _killLog/KILL_ESCALATE_MS loop-kill escalation state, which is private to
|
|
4
|
+
// cmdLoops.
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { configPath } from '../lib/config.mjs';
|
|
7
|
+
|
|
8
|
+
// Kill registry — `lazyclaw loops kill <id>` SIGTERMs once and SIGKILLs
|
|
9
|
+
// on a second invocation within KILL_ESCALATE_MS. Module-scoped so two
|
|
10
|
+
// rapid invocations of `cmd loops kill <id>` from the same process see
|
|
11
|
+
// each other; for separate processes the worker also handles SIGKILL by
|
|
12
|
+
// the OS, so the escalation is a UX nicety rather than a correctness gate.
|
|
13
|
+
const _killLog = new Map();
|
|
14
|
+
const KILL_ESCALATE_MS = 5000;
|
|
15
|
+
|
|
16
|
+
export async function cmdLoops(sub, positional, flags = {}) {
|
|
17
|
+
const loopsMod = await import('../loops.mjs');
|
|
18
|
+
const cfgDir = path.dirname(configPath());
|
|
19
|
+
switch (sub) {
|
|
20
|
+
case undefined:
|
|
21
|
+
case 'list': {
|
|
22
|
+
const items = loopsMod.listLoops(cfgDir).map(loopsMod.reconcileStatus);
|
|
23
|
+
console.log(JSON.stringify(items, null, 2));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
case 'show': {
|
|
27
|
+
const id = positional[0];
|
|
28
|
+
if (!id) { console.error('Usage: lazyclaw loops show <id>'); process.exit(2); }
|
|
29
|
+
const meta = loopsMod.reconcileStatus(loopsMod.readMeta(id, cfgDir));
|
|
30
|
+
if (!meta) { console.error(`no loop "${id}"`); process.exit(1); }
|
|
31
|
+
const iterations = loopsMod.readIterations(id, cfgDir);
|
|
32
|
+
const result = loopsMod.readResult(id, cfgDir);
|
|
33
|
+
console.log(JSON.stringify({ id, meta, iterations, result }, null, 2));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
case 'kill': {
|
|
37
|
+
const id = positional[0];
|
|
38
|
+
if (!id) { console.error('Usage: lazyclaw loops kill <id>'); process.exit(2); }
|
|
39
|
+
const meta = loopsMod.readMeta(id, cfgDir);
|
|
40
|
+
if (!meta) { console.error(`no loop "${id}"`); process.exit(1); }
|
|
41
|
+
if (!meta.pid) { console.error(`loop "${id}" has no pid`); process.exit(1); }
|
|
42
|
+
const last = _killLog.get(id) || 0;
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
const escalate = (now - last) < KILL_ESCALATE_MS && last > 0;
|
|
45
|
+
const sig = escalate ? 'SIGKILL' : 'SIGTERM';
|
|
46
|
+
try { process.kill(meta.pid, sig); }
|
|
47
|
+
catch (e) {
|
|
48
|
+
if (e?.code !== 'ESRCH') throw e;
|
|
49
|
+
// Already gone — reconcile and report.
|
|
50
|
+
loopsMod.patchMeta(id, { status: 'killed', finishedAt: new Date().toISOString() }, cfgDir);
|
|
51
|
+
console.log(JSON.stringify({ id, pid: meta.pid, signal: sig, status: 'already_gone' }));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
_killLog.set(id, now);
|
|
55
|
+
console.log(JSON.stringify({ id, pid: meta.pid, signal: sig, escalated: escalate }));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
case 'tail': {
|
|
59
|
+
const id = positional[0];
|
|
60
|
+
if (!id) { console.error('Usage: lazyclaw loops tail <id>'); process.exit(2); }
|
|
61
|
+
const dir = loopsMod.loopDir(id, cfgDir);
|
|
62
|
+
const logPath = path.join(dir, 'iterations.log');
|
|
63
|
+
const fs = await import('node:fs');
|
|
64
|
+
if (!fs.existsSync(dir)) { console.error(`no loop "${id}"`); process.exit(1); }
|
|
65
|
+
// Print everything already on disk first, then poll for new lines
|
|
66
|
+
// until the worker exits / status is no longer "running".
|
|
67
|
+
let offset = 0;
|
|
68
|
+
if (fs.existsSync(logPath)) {
|
|
69
|
+
const buf = fs.readFileSync(logPath, 'utf8');
|
|
70
|
+
process.stdout.write(buf);
|
|
71
|
+
offset = buf.length;
|
|
72
|
+
}
|
|
73
|
+
const pollMs = Number(flags['poll-ms']) || 250;
|
|
74
|
+
const maxMs = Number(flags['max-wait-ms']) || 0; // 0 = wait indefinitely
|
|
75
|
+
const startedAt = Date.now();
|
|
76
|
+
while (true) {
|
|
77
|
+
await new Promise(r => setTimeout(r, pollMs));
|
|
78
|
+
let cur = '';
|
|
79
|
+
try { cur = fs.readFileSync(logPath, 'utf8'); } catch { /* file may briefly not exist */ }
|
|
80
|
+
if (cur.length > offset) {
|
|
81
|
+
process.stdout.write(cur.slice(offset));
|
|
82
|
+
offset = cur.length;
|
|
83
|
+
}
|
|
84
|
+
const meta = loopsMod.reconcileStatus(loopsMod.readMeta(id, cfgDir));
|
|
85
|
+
if (!meta || meta.status !== 'running') break;
|
|
86
|
+
if (maxMs > 0 && Date.now() - startedAt > maxMs) break;
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
default:
|
|
91
|
+
console.error('Usage: lazyclaw loops <list|show|kill|tail> ...');
|
|
92
|
+
process.exit(2);
|
|
93
|
+
}
|
|
94
|
+
}
|