lazyclaw 6.3.1 → 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/README.ko.md +5 -1
- package/README.md +17 -13
- package/agents.mjs +54 -4
- package/channels-discord/index.mjs +5 -1
- package/channels-email/index.mjs +4 -3
- package/channels-whatsapp/index.mjs +3 -2
- package/chat_window.mjs +11 -2
- package/cli.mjs +103 -3
- package/commands/agents.mjs +7 -193
- package/commands/agents_registry.mjs +205 -0
- package/commands/auth_nodes.mjs +19 -1
- package/commands/automation.mjs +28 -94
- package/commands/automation_loops.mjs +94 -0
- package/commands/channels.mjs +46 -3
- package/commands/chat.mjs +127 -704
- package/commands/chat_hardening.mjs +23 -0
- package/commands/chat_legacy_slash.mjs +716 -0
- package/commands/config.mjs +36 -2
- package/commands/daemon.mjs +99 -1
- package/commands/gateway.mjs +123 -4
- package/commands/help_text.mjs +78 -0
- package/commands/mcp.mjs +150 -0
- package/commands/misc.mjs +20 -0
- package/commands/sessions.mjs +68 -8
- package/commands/setup.mjs +44 -80
- package/commands/setup_channels.mjs +219 -47
- package/commands/skills.mjs +19 -1
- package/commands/workflow_named.mjs +137 -0
- package/config-validate.mjs +10 -1
- package/config_features.mjs +45 -0
- package/cron.mjs +19 -8
- package/daemon/lib/auth.mjs +25 -0
- package/daemon/lib/cost.mjs +41 -0
- package/daemon/lib/respond.mjs +20 -1
- package/daemon/lib/team_inbound.mjs +69 -0
- package/daemon/route_table.mjs +5 -1
- package/daemon/routes/_deps.mjs +2 -2
- package/daemon/routes/config.mjs +11 -6
- package/daemon/routes/conversation.mjs +103 -40
- package/daemon/routes/events.mjs +45 -0
- package/daemon/routes/meta.mjs +38 -7
- package/daemon/routes/providers.mjs +26 -6
- package/daemon/routes/workflows.mjs +30 -0
- package/daemon.mjs +27 -3
- package/dotenv_min.mjs +15 -3
- package/gateway/challenge_registry.mjs +90 -0
- package/gateway/device_auth.mjs +71 -97
- package/gateway/http_gateway.mjs +29 -3
- package/lib/args.mjs +69 -3
- package/lib/config.mjs +85 -3
- package/lib/render.mjs +43 -0
- package/lib/service_install.mjs +15 -1
- package/mas/agent_turn.mjs +68 -12
- package/mas/confidence.mjs +20 -1
- package/mas/embedder.mjs +100 -0
- package/mas/events.mjs +57 -0
- package/mas/index_db.mjs +103 -10
- package/mas/learning.mjs +96 -72
- package/mas/mention_router.mjs +48 -90
- package/mas/prompt_stack.mjs +60 -3
- package/mas/recall_blend.mjs +56 -0
- package/mas/redact.mjs +55 -0
- package/mas/router_posting.mjs +96 -0
- package/mas/router_termination.mjs +63 -0
- package/mas/scrub_env.mjs +21 -6
- package/mas/skill_synth.mjs +7 -33
- package/mas/tool_runner.mjs +3 -2
- package/mas/tools/bash.mjs +9 -2
- package/mas/tools/coding.mjs +28 -5
- package/mas/tools/delegation.mjs +34 -4
- package/mas/tools/git.mjs +19 -9
- package/mas/tools/ha.mjs +2 -0
- package/mas/tools/learning.mjs +55 -0
- package/mas/tools/media.mjs +21 -3
- package/mas/tools/os.mjs +70 -16
- package/mas/tools/recall.mjs +28 -2
- package/mcp/client.mjs +8 -1
- package/mcp/server_spawn.mjs +5 -1
- package/memory.mjs +24 -0
- package/package.json +3 -1
- package/providers/anthropic.mjs +101 -6
- package/providers/cache.mjs +9 -1
- package/providers/claude_cli.mjs +104 -22
- package/providers/claude_cli_session.mjs +183 -0
- package/providers/cli_error.mjs +38 -0
- package/providers/cli_login.mjs +179 -0
- package/providers/codex_cli.mjs +70 -12
- package/providers/gemini.mjs +104 -3
- package/providers/gemini_cli.mjs +78 -17
- package/providers/model_catalogue.mjs +33 -2
- package/providers/ollama.mjs +104 -3
- package/providers/openai.mjs +110 -8
- package/providers/openai_compat.mjs +97 -6
- package/providers/orchestrator.mjs +112 -12
- package/providers/registry.mjs +15 -9
- package/providers/retry.mjs +14 -2
- package/providers/tool_use/anthropic.mjs +17 -3
- package/providers/tool_use/claude_cli.mjs +72 -20
- package/providers/tool_use/gemini.mjs +29 -2
- package/providers/tool_use/openai.mjs +35 -5
- package/sandbox/confiners/seatbelt.mjs +37 -12
- package/sandbox/index.mjs +49 -0
- package/sandbox/local.mjs +7 -1
- package/sandbox/spawn.mjs +144 -0
- package/sandbox.mjs +5 -1
- package/scripts/loop-worker.mjs +25 -1
- package/sessions.mjs +0 -0
- package/skills/channel-style.md +20 -0
- package/skills/code-review.md +33 -0
- package/skills/commit-message.md +30 -0
- package/skills/concise.md +24 -0
- package/skills/debug-coach.md +25 -0
- package/skills/explain.md +24 -0
- package/skills/korean.md +25 -0
- package/skills/summarize.md +33 -0
- package/skills.mjs +24 -2
- package/skills_curator.mjs +6 -0
- package/skills_install.mjs +10 -2
- package/tasks.mjs +6 -1
- package/teams.mjs +78 -0
- package/tui/banner.mjs +72 -0
- package/tui/chat_mode_slash.mjs +59 -0
- package/tui/config_picker.mjs +1 -0
- package/tui/editor.mjs +0 -0
- package/tui/editor_anchor.mjs +46 -0
- package/tui/editor_keys.mjs +275 -0
- package/tui/hud.mjs +111 -0
- package/tui/login_flow.mjs +113 -0
- package/tui/modal_picker.mjs +10 -1
- package/tui/model_pick.mjs +287 -0
- package/tui/orchestrator_flow.mjs +164 -0
- package/tui/orchestrator_setup.mjs +135 -0
- package/tui/pickers.mjs +218 -248
- package/tui/repl.mjs +118 -209
- package/tui/repl_altbuffer.mjs +63 -0
- package/tui/repl_reducers.mjs +114 -0
- package/tui/repl_reset.mjs +37 -0
- package/tui/run_turn.mjs +228 -26
- package/tui/slash_args.mjs +159 -0
- package/tui/slash_channels.mjs +208 -0
- package/tui/slash_commands.mjs +7 -5
- package/tui/slash_dashboard.mjs +220 -0
- package/tui/slash_dispatcher.mjs +339 -774
- package/tui/slash_helpers.mjs +68 -0
- package/tui/slash_popup.mjs +5 -1
- package/tui/slash_trainer.mjs +173 -0
- package/tui/splash.mjs +15 -2
- package/tui/status_bar.mjs +26 -0
- package/tui/theme.mjs +28 -0
- package/web/avatars/01.png +0 -0
- package/web/avatars/02.png +0 -0
- package/web/avatars/03.png +0 -0
- package/web/avatars/04.png +0 -0
- package/web/avatars/05.png +0 -0
- package/web/avatars/06.png +0 -0
- package/web/avatars/07.png +0 -0
- package/web/avatars/08.png +0 -0
- package/web/avatars/09.png +0 -0
- package/web/avatars/10.png +0 -0
- package/web/avatars/11.png +0 -0
- package/web/avatars/12.png +0 -0
- package/web/avatars/13.png +0 -0
- package/web/avatars/14.png +0 -0
- package/web/avatars/15.png +0 -0
- package/web/avatars/16.png +0 -0
- package/web/avatars/17.png +0 -0
- package/web/avatars/18.png +0 -0
- package/web/avatars/19.png +0 -0
- package/web/avatars/20.png +0 -0
- package/web/dashboard.css +77 -0
- package/web/dashboard.html +29 -2
- package/web/dashboard.js +296 -33
- package/workflow/builtin_caps.mjs +94 -0
- package/workflow/declarative.mjs +101 -0
- package/workflow/named.mjs +71 -0
- package/workflow/named_cron.mjs +50 -0
- package/workflow/nodes.mjs +67 -0
- package/workflow/run_request.mjs +74 -0
- package/workflow/yaml_min.mjs +97 -0
package/commands/agents.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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';
|
|
7
|
+
import { defaultSandboxSpec } from '../sandbox/index.mjs';
|
|
8
8
|
|
|
9
9
|
// Thin .env loader wrapper kept local so the module stays self-contained.
|
|
10
10
|
export function _loadDotenvIfAny(cfgDir) { return _loadDotenvShared(cfgDir); }
|
|
@@ -326,6 +326,9 @@ export async function cmdTask(sub, positional, flags = {}) {
|
|
|
326
326
|
maxAgentTurns: flags['max-turns'] ? parseInt(flags['max-turns'], 10) : undefined,
|
|
327
327
|
approve,
|
|
328
328
|
security: cfg.security,
|
|
329
|
+
// Default-on isolation: confine every tool the team runs (filesystem
|
|
330
|
+
// to cwd, secrets unreadable, net allowed). Opt out via cfg.sandbox.
|
|
331
|
+
sandbox: defaultSandboxSpec(cfg, { cwd: process.cwd(), configDir: cfgDir }),
|
|
329
332
|
});
|
|
330
333
|
emitJson({ id: result.task.id, status: result.task.status, iterations: result.iterations, stoppedBy: result.stoppedBy });
|
|
331
334
|
} catch (err) {
|
|
@@ -470,198 +473,9 @@ export async function cmdTeam(sub, positional, flags = {}) {
|
|
|
470
473
|
}
|
|
471
474
|
}
|
|
472
475
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const name = positional[0];
|
|
477
|
-
|
|
478
|
-
const emitJson = (obj) => process.stdout.write(JSON.stringify(obj, null, 2) + '\n');
|
|
479
|
-
|
|
480
|
-
switch (sub) {
|
|
481
|
-
case undefined:
|
|
482
|
-
case 'list': {
|
|
483
|
-
emitJson(agentsMod.listAgents(cfgDir));
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
case 'add': {
|
|
487
|
-
if (!name) { console.error('Usage: lazyclaw agent add <name> [--role "..."] [--provider X] [--model Y] [--display "..."] [--tools bash,read,write,grep,skill_view] [--tags a,b] [--skill-write auto|manual|off]'); process.exit(2); }
|
|
488
|
-
const tools = agentsMod.parseToolsFlag(flags.tools);
|
|
489
|
-
try {
|
|
490
|
-
const a = agentsMod.registerAgent({
|
|
491
|
-
name,
|
|
492
|
-
displayName: flags.display || flags['display-name'],
|
|
493
|
-
role: flags.role || '',
|
|
494
|
-
provider: flags.provider || 'claude-cli',
|
|
495
|
-
model: flags.model || '',
|
|
496
|
-
tools: tools === null ? undefined : tools,
|
|
497
|
-
tags: agentsMod.parseToolsFlag(flags.tags) || [],
|
|
498
|
-
skillWrite: flags['skill-write'],
|
|
499
|
-
}, cfgDir);
|
|
500
|
-
emitJson(a);
|
|
501
|
-
} catch (err) {
|
|
502
|
-
console.error(`agent add: ${err?.message || err}`);
|
|
503
|
-
process.exit(2);
|
|
504
|
-
}
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
case 'show': {
|
|
508
|
-
if (!name) { console.error('Usage: lazyclaw agent show <name>'); process.exit(2); }
|
|
509
|
-
const a = agentsMod.getAgent(name, cfgDir);
|
|
510
|
-
if (!a) { console.error(`agent show: no agent "${name}"`); process.exit(2); }
|
|
511
|
-
emitJson(a);
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
|
-
case 'edit': {
|
|
515
|
-
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); }
|
|
516
|
-
const patch = {};
|
|
517
|
-
if (flags.role !== undefined) patch.role = String(flags.role);
|
|
518
|
-
if (flags.provider !== undefined) patch.provider = String(flags.provider);
|
|
519
|
-
if (flags.model !== undefined) patch.model = String(flags.model);
|
|
520
|
-
if (flags.display !== undefined) patch.displayName = String(flags.display);
|
|
521
|
-
if (flags['display-name'] !== undefined) patch.displayName = String(flags['display-name']);
|
|
522
|
-
if (flags.tools !== undefined) patch.tools = agentsMod.parseToolsFlag(flags.tools);
|
|
523
|
-
if (flags.tags !== undefined) patch.tags = agentsMod.parseToolsFlag(flags.tags);
|
|
524
|
-
if (flags['skill-write'] !== undefined) patch.skillWrite = String(flags['skill-write']);
|
|
525
|
-
if (flags['memory-write'] !== undefined) patch.memoryWrite = String(flags['memory-write']);
|
|
526
|
-
if (Object.keys(patch).length === 0) {
|
|
527
|
-
console.error('agent edit: no fields to update');
|
|
528
|
-
process.exit(2);
|
|
529
|
-
}
|
|
530
|
-
try { emitJson(agentsMod.patchAgent(name, patch, cfgDir)); }
|
|
531
|
-
catch (err) { console.error(`agent edit: ${err?.message || err}`); process.exit(2); }
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
case 'remove':
|
|
535
|
-
case 'rm':
|
|
536
|
-
case 'delete': {
|
|
537
|
-
if (!name) { console.error('Usage: lazyclaw agent remove <name>'); process.exit(2); }
|
|
538
|
-
try { emitJson(agentsMod.removeAgent(name, cfgDir)); }
|
|
539
|
-
catch (err) { console.error(`agent remove: ${err?.message || err}`); process.exit(2); }
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
case 'memory': {
|
|
543
|
-
// memory <show|edit|clear> <name>
|
|
544
|
-
const op = positional[0];
|
|
545
|
-
const memName = positional[1];
|
|
546
|
-
if (!op || !memName) {
|
|
547
|
-
console.error('Usage: lazyclaw agent memory <show|edit|clear> <name>');
|
|
548
|
-
process.exit(2);
|
|
549
|
-
}
|
|
550
|
-
const memMod = await import('../mas/agent_memory.mjs');
|
|
551
|
-
try {
|
|
552
|
-
if (op === 'show') {
|
|
553
|
-
const max = Number.isFinite(+flags['max-chars']) && +flags['max-chars'] > 0 ? +flags['max-chars'] : memMod.DEFAULT_MAX_CHARS;
|
|
554
|
-
const text = memMod.readMemory(memName, cfgDir, max);
|
|
555
|
-
if (!text) process.stderr.write(`(no memory for "${memName}")\n`);
|
|
556
|
-
else process.stdout.write(text + (text.endsWith('\n') ? '' : '\n'));
|
|
557
|
-
} else if (op === 'edit') {
|
|
558
|
-
const p = memMod.memoryPath(memName, cfgDir);
|
|
559
|
-
// Ensure file exists so $EDITOR doesn't start with a missing
|
|
560
|
-
// file warning.
|
|
561
|
-
if (!fs.existsSync(p)) {
|
|
562
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
563
|
-
fs.writeFileSync(p, `# ${memName} — memory\n\n`);
|
|
564
|
-
}
|
|
565
|
-
const editor = process.env.EDITOR || 'vi';
|
|
566
|
-
const { spawn } = await import('node:child_process');
|
|
567
|
-
await new Promise((resolve) => {
|
|
568
|
-
const ch = spawn(editor, [p], { stdio: 'inherit' });
|
|
569
|
-
ch.on('close', () => resolve());
|
|
570
|
-
});
|
|
571
|
-
process.stdout.write(`edited ${p}\n`);
|
|
572
|
-
} else if (op === 'clear') {
|
|
573
|
-
const removed = memMod.clear(memName, cfgDir);
|
|
574
|
-
process.stdout.write(removed ? `cleared memory for "${memName}"\n` : `(no memory for "${memName}")\n`);
|
|
575
|
-
} else {
|
|
576
|
-
console.error(`Usage: lazyclaw agent memory <show|edit|clear> <name>`);
|
|
577
|
-
process.exit(2);
|
|
578
|
-
}
|
|
579
|
-
} catch (err) {
|
|
580
|
-
console.error(`agent memory ${op}: ${err?.message || err}`);
|
|
581
|
-
process.exit(2);
|
|
582
|
-
}
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
case 'reflect': {
|
|
586
|
-
const aname = positional[0];
|
|
587
|
-
const taskId = flags.task || positional[1];
|
|
588
|
-
if (!aname || !taskId) {
|
|
589
|
-
console.error('Usage: lazyclaw agent reflect <name> --task <id>');
|
|
590
|
-
process.exit(2);
|
|
591
|
-
}
|
|
592
|
-
const tasksMod = await import('../tasks.mjs');
|
|
593
|
-
const memMod = await import('../mas/agent_memory.mjs');
|
|
594
|
-
const a = agentsMod.getAgent(aname, cfgDir);
|
|
595
|
-
if (!a) { console.error(`agent reflect: no agent "${aname}"`); process.exit(2); }
|
|
596
|
-
const task = tasksMod.getTask(taskId, cfgDir);
|
|
597
|
-
if (!task) { console.error(`agent reflect: no task "${taskId}"`); process.exit(2); }
|
|
598
|
-
try { _loadDotenvIfAny(cfgDir); } catch { /* best-effort */ }
|
|
599
|
-
const cfg = readConfig();
|
|
600
|
-
const apiKey = _resolveAuthKey(cfg, a.provider);
|
|
601
|
-
const baseUrl = _resolveBaseUrl(a.provider);
|
|
602
|
-
try {
|
|
603
|
-
const body = await memMod.reflectOnce({ agent: a, task, apiKey, baseUrl });
|
|
604
|
-
if (!body || !body.trim()) {
|
|
605
|
-
process.stderr.write('reflection returned empty body — nothing to write\n');
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
if (!flags['dry-run']) {
|
|
609
|
-
memMod.prependEntry(aname, { taskId: task.id, title: task.title, body }, cfgDir);
|
|
610
|
-
}
|
|
611
|
-
process.stdout.write(body + (body.endsWith('\n') ? '' : '\n'));
|
|
612
|
-
} catch (err) {
|
|
613
|
-
console.error(`agent reflect: ${err?.message || err}`);
|
|
614
|
-
process.exit(2);
|
|
615
|
-
}
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
|
-
case 'skill-synth': {
|
|
619
|
-
const aname = positional[0];
|
|
620
|
-
const taskId = flags.task || positional[1];
|
|
621
|
-
if (!aname || !taskId) {
|
|
622
|
-
console.error('Usage: lazyclaw agent skill-synth <name> --task <id> [--dry-run]');
|
|
623
|
-
process.exit(2);
|
|
624
|
-
}
|
|
625
|
-
const tasksMod = await import('../tasks.mjs');
|
|
626
|
-
const synthMod = await import('../mas/skill_synth.mjs');
|
|
627
|
-
const skillsMod = await import('../skills.mjs');
|
|
628
|
-
const a = agentsMod.getAgent(aname, cfgDir);
|
|
629
|
-
if (!a) { console.error(`agent skill-synth: no agent "${aname}"`); process.exit(2); }
|
|
630
|
-
const task = tasksMod.getTask(taskId, cfgDir);
|
|
631
|
-
if (!task) { console.error(`agent skill-synth: no task "${taskId}"`); process.exit(2); }
|
|
632
|
-
try { _loadDotenvIfAny(cfgDir); } catch { /* best-effort */ }
|
|
633
|
-
const cfg = readConfig();
|
|
634
|
-
const apiKey = _resolveAuthKey(cfg, a.provider);
|
|
635
|
-
const baseUrl = _resolveBaseUrl(a.provider);
|
|
636
|
-
try {
|
|
637
|
-
const result = await synthMod.synthesizeSkill({ agent: a, task, apiKey, baseUrl });
|
|
638
|
-
if (!result) {
|
|
639
|
-
process.stderr.write('skill synthesis produced nothing worth saving\n');
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
if (!flags['dry-run']) {
|
|
643
|
-
// installSynthesized reserves a collision-free name (never
|
|
644
|
-
// clobbers a human-authored skill) and version-bumps when it
|
|
645
|
-
// improves its own prior skill.
|
|
646
|
-
const installed = synthMod.installSynthesized(
|
|
647
|
-
{ name: result.name, description: result.description, body: result.body, sourceTask: task.id },
|
|
648
|
-
cfgDir,
|
|
649
|
-
);
|
|
650
|
-
emitJson({ skill: installed.skill, description: result.description, version: installed.version, path: installed.path });
|
|
651
|
-
} else {
|
|
652
|
-
process.stdout.write(result.doc + (result.doc.endsWith('\n') ? '' : '\n'));
|
|
653
|
-
}
|
|
654
|
-
} catch (err) {
|
|
655
|
-
console.error(`agent skill-synth: ${err?.message || err}`);
|
|
656
|
-
process.exit(2);
|
|
657
|
-
}
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
default:
|
|
661
|
-
console.error('Usage: lazyclaw agent <add|list|show|edit|remove|memory|reflect|skill-synth> ...');
|
|
662
|
-
process.exit(2);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
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';
|
|
665
479
|
|
|
666
480
|
// Best-effort .env loader for ~/.lazyclaw/.env. Only sets keys that are
|
|
667
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/auth_nodes.mjs
CHANGED
|
@@ -204,8 +204,26 @@ export async function cmdNodes(sub, positional, flags = {}) {
|
|
|
204
204
|
console.log(JSON.stringify(store.revoke(deviceId)));
|
|
205
205
|
return;
|
|
206
206
|
}
|
|
207
|
+
case 'rotate': {
|
|
208
|
+
const deviceId = positional[0];
|
|
209
|
+
if (!deviceId) {
|
|
210
|
+
console.error('Usage: lazyclaw nodes rotate <deviceId> [--ttl <ms>]');
|
|
211
|
+
process.exit(2);
|
|
212
|
+
}
|
|
213
|
+
const ttlRaw = flags.ttl;
|
|
214
|
+
const ttlMs = (ttlRaw !== undefined && ttlRaw !== true) ? Number(ttlRaw) : undefined;
|
|
215
|
+
const { PairingStore } = await import('../gateway/device_auth.mjs');
|
|
216
|
+
const store = new PairingStore(path.dirname(configPath()));
|
|
217
|
+
try {
|
|
218
|
+
const { expiresAt } = store.rotate(deviceId, { ttlMs });
|
|
219
|
+
// Like approve, the token is intentionally NOT printed — the device
|
|
220
|
+
// receives its rotated token on its next /gateway/connect.
|
|
221
|
+
console.log(JSON.stringify({ ok: true, rotated: deviceId, ...(expiresAt ? { expiresAt } : {}), note: 'device receives its new token on next /gateway/connect' }));
|
|
222
|
+
} catch (e) { console.error(`error: ${e.message}`); process.exit(1); }
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
207
225
|
default:
|
|
208
|
-
console.error('Usage: lazyclaw nodes <list|register|remove|pending|approve <requestId>|revoke <deviceId>|devices> ...');
|
|
226
|
+
console.error('Usage: lazyclaw nodes <list|register|remove|pending|approve <requestId>|revoke <deviceId>|rotate <deviceId>|devices> ...');
|
|
209
227
|
process.exit(2);
|
|
210
228
|
}
|
|
211
229
|
}
|
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';
|
|
@@ -10,6 +11,21 @@ import { loadDotenvIfAny as _loadDotenvShared } from '../dotenv_min.mjs';
|
|
|
10
11
|
// Thin .env loader wrapper kept local so the module stays self-contained.
|
|
11
12
|
export function _loadDotenvIfAny(cfgDir) { return _loadDotenvShared(cfgDir); }
|
|
12
13
|
|
|
14
|
+
// Build the detached loop-worker argv. Pure + exported so the flag-forwarding
|
|
15
|
+
// contract is unit-testable: --use-memory / --recall MUST reach the worker, or
|
|
16
|
+
// `loop --detach --use-memory` silently runs with no memory (the worker only
|
|
17
|
+
// builds a system message when it sees the flags).
|
|
18
|
+
export function buildDetachArgv(worker, { loopId, prompt, max, provName, cfgDir, until, requestedSession, model, useMemory, recall } = {}) {
|
|
19
|
+
const argv = [worker, '--loop-id', loopId, '--prompt', prompt,
|
|
20
|
+
'--max', String(max), '--provider', provName, '--cfg-dir', cfgDir];
|
|
21
|
+
if (until) { argv.push('--until', String(until)); }
|
|
22
|
+
if (requestedSession) { argv.push('--session-existing', requestedSession); }
|
|
23
|
+
if (model) { argv.push('--model', String(model)); }
|
|
24
|
+
if (useMemory) { argv.push('--use-memory'); }
|
|
25
|
+
if (recall) { argv.push('--recall', String(recall)); }
|
|
26
|
+
return argv;
|
|
27
|
+
}
|
|
28
|
+
|
|
13
29
|
export async function cmdCron(sub, positional, flags = {}) {
|
|
14
30
|
const cron = await import('../cron.mjs');
|
|
15
31
|
const cfg = readConfig();
|
|
@@ -169,11 +185,11 @@ export async function cmdLoop(prompt, flags = {}) {
|
|
|
169
185
|
// <repo>/scripts/loop-worker.mjs (was a sibling when cmdLoop was in cli.mjs).
|
|
170
186
|
const here = path.dirname(new URL(import.meta.url).pathname);
|
|
171
187
|
const worker = path.join(here, '..', 'scripts', 'loop-worker.mjs');
|
|
172
|
-
const argv =
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
188
|
+
const argv = buildDetachArgv(worker, {
|
|
189
|
+
loopId, prompt, max, provName, cfgDir,
|
|
190
|
+
until: flags.until, requestedSession, model,
|
|
191
|
+
useMemory: flags['use-memory'], recall: flags.recall,
|
|
192
|
+
});
|
|
177
193
|
const child = spawn(process.execPath, argv, {
|
|
178
194
|
detached: true,
|
|
179
195
|
stdio: 'ignore',
|
|
@@ -251,93 +267,11 @@ export async function cmdLoop(prompt, flags = {}) {
|
|
|
251
267
|
}
|
|
252
268
|
}
|
|
253
269
|
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
const _killLog = new Map();
|
|
260
|
-
const KILL_ESCALATE_MS = 5000;
|
|
261
|
-
|
|
262
|
-
export async function cmdLoops(sub, positional, flags = {}) {
|
|
263
|
-
const loopsMod = await import('../loops.mjs');
|
|
264
|
-
const cfgDir = path.dirname(configPath());
|
|
265
|
-
switch (sub) {
|
|
266
|
-
case undefined:
|
|
267
|
-
case 'list': {
|
|
268
|
-
const items = loopsMod.listLoops(cfgDir).map(loopsMod.reconcileStatus);
|
|
269
|
-
console.log(JSON.stringify(items, null, 2));
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
case 'show': {
|
|
273
|
-
const id = positional[0];
|
|
274
|
-
if (!id) { console.error('Usage: lazyclaw loops show <id>'); process.exit(2); }
|
|
275
|
-
const meta = loopsMod.reconcileStatus(loopsMod.readMeta(id, cfgDir));
|
|
276
|
-
if (!meta) { console.error(`no loop "${id}"`); process.exit(1); }
|
|
277
|
-
const iterations = loopsMod.readIterations(id, cfgDir);
|
|
278
|
-
const result = loopsMod.readResult(id, cfgDir);
|
|
279
|
-
console.log(JSON.stringify({ id, meta, iterations, result }, null, 2));
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
case 'kill': {
|
|
283
|
-
const id = positional[0];
|
|
284
|
-
if (!id) { console.error('Usage: lazyclaw loops kill <id>'); process.exit(2); }
|
|
285
|
-
const meta = loopsMod.readMeta(id, cfgDir);
|
|
286
|
-
if (!meta) { console.error(`no loop "${id}"`); process.exit(1); }
|
|
287
|
-
if (!meta.pid) { console.error(`loop "${id}" has no pid`); process.exit(1); }
|
|
288
|
-
const last = _killLog.get(id) || 0;
|
|
289
|
-
const now = Date.now();
|
|
290
|
-
const escalate = (now - last) < KILL_ESCALATE_MS && last > 0;
|
|
291
|
-
const sig = escalate ? 'SIGKILL' : 'SIGTERM';
|
|
292
|
-
try { process.kill(meta.pid, sig); }
|
|
293
|
-
catch (e) {
|
|
294
|
-
if (e?.code !== 'ESRCH') throw e;
|
|
295
|
-
// Already gone — reconcile and report.
|
|
296
|
-
loopsMod.patchMeta(id, { status: 'killed', finishedAt: new Date().toISOString() }, cfgDir);
|
|
297
|
-
console.log(JSON.stringify({ id, pid: meta.pid, signal: sig, status: 'already_gone' }));
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
_killLog.set(id, now);
|
|
301
|
-
console.log(JSON.stringify({ id, pid: meta.pid, signal: sig, escalated: escalate }));
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
case 'tail': {
|
|
305
|
-
const id = positional[0];
|
|
306
|
-
if (!id) { console.error('Usage: lazyclaw loops tail <id>'); process.exit(2); }
|
|
307
|
-
const dir = loopsMod.loopDir(id, cfgDir);
|
|
308
|
-
const logPath = path.join(dir, 'iterations.log');
|
|
309
|
-
const fs = await import('node:fs');
|
|
310
|
-
if (!fs.existsSync(dir)) { console.error(`no loop "${id}"`); process.exit(1); }
|
|
311
|
-
// Print everything already on disk first, then poll for new lines
|
|
312
|
-
// until the worker exits / status is no longer "running".
|
|
313
|
-
let offset = 0;
|
|
314
|
-
if (fs.existsSync(logPath)) {
|
|
315
|
-
const buf = fs.readFileSync(logPath, 'utf8');
|
|
316
|
-
process.stdout.write(buf);
|
|
317
|
-
offset = buf.length;
|
|
318
|
-
}
|
|
319
|
-
const pollMs = Number(flags['poll-ms']) || 250;
|
|
320
|
-
const maxMs = Number(flags['max-wait-ms']) || 0; // 0 = wait indefinitely
|
|
321
|
-
const startedAt = Date.now();
|
|
322
|
-
while (true) {
|
|
323
|
-
await new Promise(r => setTimeout(r, pollMs));
|
|
324
|
-
let cur = '';
|
|
325
|
-
try { cur = fs.readFileSync(logPath, 'utf8'); } catch { /* file may briefly not exist */ }
|
|
326
|
-
if (cur.length > offset) {
|
|
327
|
-
process.stdout.write(cur.slice(offset));
|
|
328
|
-
offset = cur.length;
|
|
329
|
-
}
|
|
330
|
-
const meta = loopsMod.reconcileStatus(loopsMod.readMeta(id, cfgDir));
|
|
331
|
-
if (!meta || meta.status !== 'running') break;
|
|
332
|
-
if (maxMs > 0 && Date.now() - startedAt > maxMs) break;
|
|
333
|
-
}
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
default:
|
|
337
|
-
console.error('Usage: lazyclaw loops <list|show|kill|tail> ...');
|
|
338
|
-
process.exit(2);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
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';
|
|
341
275
|
|
|
342
276
|
// Install (or refresh) the system scheduler entry that fires
|
|
343
277
|
// `lazyclaw goal tick <name>` on a schedule. Writes to cfg.cron and to
|