lazyclaw 4.2.1 → 4.3.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.md +155 -0
- package/agents.mjs +19 -3
- package/channels/matrix.mjs +417 -0
- package/channels/telegram.mjs +362 -0
- package/cli.mjs +381 -60
- package/daemon.mjs +98 -0
- package/gateway/device_auth.mjs +664 -0
- package/gateway/http_gateway.mjs +304 -0
- package/mas/agent_memory.mjs +35 -34
- package/mas/agent_turn.mjs +2 -1
- package/mas/mention_router.mjs +75 -4
- package/mas/provider_adapters.mjs +83 -0
- package/mas/redact.mjs +46 -0
- package/mas/skill_synth.mjs +232 -0
- package/mas/tool_runner.mjs +24 -2
- package/mas/tools/skill_view.mjs +43 -0
- package/package.json +3 -1
- package/skills.mjs +101 -8
- package/skills_curator.mjs +323 -0
- package/workspace.mjs +18 -3
package/cli.mjs
CHANGED
|
@@ -56,6 +56,17 @@ function _resolveAuthKey(cfg, provider) {
|
|
|
56
56
|
return cfg['api-key'] || '';
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// Per-provider base-URL override (used by tests + private gateways).
|
|
60
|
+
// Single source of truth — the reflect / skill-synth / task-tick paths
|
|
61
|
+
// all resolve through here so a new provider's env var lands in one spot.
|
|
62
|
+
function _resolveBaseUrl(provider) {
|
|
63
|
+
return {
|
|
64
|
+
anthropic: process.env.LAZYCLAW_ANTHROPIC_BASE_URL,
|
|
65
|
+
openai: process.env.LAZYCLAW_OPENAI_BASE_URL,
|
|
66
|
+
gemini: process.env.LAZYCLAW_GEMINI_BASE_URL,
|
|
67
|
+
}[provider] || undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
59
70
|
async function importWorkflow(file) {
|
|
60
71
|
const abs = path.resolve(file);
|
|
61
72
|
const url = pathToFileURL(abs).href;
|
|
@@ -924,6 +935,10 @@ const SUBCOMMANDS = [
|
|
|
924
935
|
'loop', 'loops', 'goal', 'memory',
|
|
925
936
|
// v4.0.0 — Slack Socket Mode listener (inbound DM / @-mention)
|
|
926
937
|
'slack',
|
|
938
|
+
// v4.3.0 — Telegram long-poll listener (zero-install mobile control)
|
|
939
|
+
'telegram',
|
|
940
|
+
// v4.3.0 — Matrix /sync long-poll listener
|
|
941
|
+
'matrix',
|
|
927
942
|
// v4.1.0 — multi-agent slack system (Phase 9+)
|
|
928
943
|
'agent', 'team', 'task',
|
|
929
944
|
];
|
|
@@ -931,13 +946,13 @@ const SUBCOMMANDS = [
|
|
|
931
946
|
const SUBCOMMAND_SUBS = {
|
|
932
947
|
config: ['get', 'set', 'list', 'delete', 'unset', 'path', 'edit', 'validate'],
|
|
933
948
|
sessions: ['list', 'show', 'clear', 'export', 'search'],
|
|
934
|
-
skills: ['list', 'show', 'install', 'remove', 'search'],
|
|
949
|
+
skills: ['list', 'show', 'install', 'remove', 'search', 'curate', 'classify'],
|
|
935
950
|
providers: ['list', 'info', 'test', 'add', 'remove', 'models'],
|
|
936
951
|
rates: ['list', 'set', 'delete', 'shape', 'validate', 'copy'],
|
|
937
952
|
completion: ['bash', 'zsh'],
|
|
938
953
|
auth: ['list', 'add', 'remove', 'use', 'rotate'],
|
|
939
954
|
pairing: ['list', 'add', 'remove'],
|
|
940
|
-
nodes: ['list', 'register', 'remove'],
|
|
955
|
+
nodes: ['list', 'register', 'remove', 'pending', 'approve', 'revoke', 'devices'],
|
|
941
956
|
message: ['list', 'add', 'remove', 'send'],
|
|
942
957
|
workspace: ['list', 'init', 'show', 'remove', 'path'],
|
|
943
958
|
cron: ['list', 'add', 'remove', 'show', 'sync', 'run'],
|
|
@@ -946,6 +961,8 @@ const SUBCOMMAND_SUBS = {
|
|
|
946
961
|
goal: ['add', 'list', 'show', 'close', 'switch', 'tick', 'channel'],
|
|
947
962
|
memory: ['show', 'dream', 'edit'],
|
|
948
963
|
slack: ['listen'],
|
|
964
|
+
telegram: ['listen'],
|
|
965
|
+
matrix: ['listen'],
|
|
949
966
|
agent: ['add', 'list', 'show', 'edit', 'remove'],
|
|
950
967
|
team: ['add', 'list', 'show', 'edit', 'remove'],
|
|
951
968
|
task: ['start', 'list', 'show', 'abandon', 'done', 'remove'],
|
|
@@ -1484,28 +1501,28 @@ function _attachGhostAutocomplete(rl) {
|
|
|
1484
1501
|
// Width-management rule: every inner line is forced through
|
|
1485
1502
|
// `.padEnd(W)` so a stray width miscount can't punch the right
|
|
1486
1503
|
// border off the box (which is exactly the bug v3.99.5 shipped:
|
|
1487
|
-
// v4.2.2 —
|
|
1488
|
-
//
|
|
1489
|
-
//
|
|
1490
|
-
//
|
|
1491
|
-
//
|
|
1492
|
-
// the user kept in muscle memory.
|
|
1504
|
+
// v4.2.2 — boxed figlet "lazy" wordmark, single-colour orange. The
|
|
1505
|
+
// previous mixed-colour banner (helmet-red letter art + ink-beige
|
|
1506
|
+
// caption) read as "two banners glued together" because the colour
|
|
1507
|
+
// changed mid-box. We use one warm orange (#F08246) for everything —
|
|
1508
|
+
// border, letter art, caption — so the eye reads it as one badge.
|
|
1493
1509
|
//
|
|
1494
|
-
//
|
|
1495
|
-
//
|
|
1496
|
-
//
|
|
1510
|
+
// Letter art is figlet "standard" (6 rows) rather than the v3.99.11
|
|
1511
|
+
// "small" (4 rows), because small renders as a pixel mush in most
|
|
1512
|
+
// terminal fonts. Standard's strokes are wide enough that the
|
|
1513
|
+
// letters read as `l a z y` even at small terminal sizes.
|
|
1497
1514
|
//
|
|
1498
|
-
//
|
|
1499
|
-
//
|
|
1500
|
-
//
|
|
1501
|
-
//
|
|
1502
|
-
//
|
|
1503
|
-
//
|
|
1515
|
+
// Layout invariant: every inner row is exactly INNER_W visible cells
|
|
1516
|
+
// (no double-width glyphs, all chars are 1 cell in any monospace
|
|
1517
|
+
// font), so the right edge `│` always lands in the same column.
|
|
1518
|
+
//
|
|
1519
|
+
// _renderMascot / _renderMascotTiny are kept as stubs so any leftover
|
|
1520
|
+
// caller doesn't crash; no state-coloured art is produced any more.
|
|
1521
|
+
|
|
1522
|
+
const _ORANGE_RGB = '241;130;70'; // #F18246
|
|
1523
|
+
function _orange(s) { return `\x1b[38;2;${_ORANGE_RGB}m${s}\x1b[0m`; }
|
|
1504
1524
|
|
|
1505
1525
|
function _renderMascot() {
|
|
1506
|
-
// Banner builds its own art; this stub exists so any leftover
|
|
1507
|
-
// caller doesn't crash. One element so .map() and length checks
|
|
1508
|
-
// behave like the legacy shape.
|
|
1509
1526
|
return ['lazyclaw'];
|
|
1510
1527
|
}
|
|
1511
1528
|
|
|
@@ -1513,39 +1530,49 @@ function _renderMascotTiny() {
|
|
|
1513
1530
|
return 'lazyclaw';
|
|
1514
1531
|
}
|
|
1515
1532
|
|
|
1533
|
+
// figlet "standard" "lazy", trimmed of leading blank line. Each row
|
|
1534
|
+
// is left-padded by two spaces inside the box, and every row is then
|
|
1535
|
+
// padded to INNER_W cells.
|
|
1536
|
+
const _LAZY_STANDARD = [
|
|
1537
|
+
' _ ',
|
|
1538
|
+
'| | __ _ _____ _ ',
|
|
1539
|
+
'| |/ _` |_ / | | | ',
|
|
1540
|
+
'| | (_| |/ /| |_| | ',
|
|
1541
|
+
'|_|\\__,_/___|\\__, | ',
|
|
1542
|
+
' |___/ ',
|
|
1543
|
+
];
|
|
1544
|
+
|
|
1545
|
+
const _INNER_W = 32; // 2 left pad + 20 letter art + caption headroom
|
|
1546
|
+
|
|
1516
1547
|
function _renderBanner(version) {
|
|
1517
|
-
const helmet = (s) => `\x1b[38;2;195;61;42m${s}\x1b[0m`;
|
|
1518
|
-
const ink = (s) => `\x1b[38;2;241;234;217m${s}\x1b[0m`;
|
|
1519
1548
|
const v = String(version || '?.?.?');
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
const
|
|
1523
|
-
const top = '╭' + '─'.repeat(
|
|
1524
|
-
const bot = '╰' + '─'.repeat(
|
|
1525
|
-
const wrap = (inner) => helmet('│') + inner + helmet('│');
|
|
1549
|
+
const cap = ` LazyClaw v${v}`;
|
|
1550
|
+
const padInner = (s) => ' ' + s.padEnd(_INNER_W - 2, ' ');
|
|
1551
|
+
const wrap = (inner) => _orange('│') + _orange(inner) + _orange('│');
|
|
1552
|
+
const top = _orange('╭' + '─'.repeat(_INNER_W) + '╮');
|
|
1553
|
+
const bot = _orange('╰' + '─'.repeat(_INNER_W) + '╯');
|
|
1526
1554
|
return [
|
|
1527
|
-
|
|
1528
|
-
wrap(
|
|
1529
|
-
wrap(
|
|
1530
|
-
|
|
1531
|
-
wrap(helmet(` |_\\__,_/__\\_, |_| `)),
|
|
1532
|
-
wrap(ink(cap)),
|
|
1533
|
-
helmet(bot),
|
|
1555
|
+
top,
|
|
1556
|
+
..._LAZY_STANDARD.map((row) => wrap(padInner(row))),
|
|
1557
|
+
wrap(padInner(cap)),
|
|
1558
|
+
bot,
|
|
1534
1559
|
];
|
|
1535
1560
|
}
|
|
1536
1561
|
|
|
1537
1562
|
function _printChatBanner(activeProvName, activeModel, version) {
|
|
1538
1563
|
if (!process.stdout.isTTY) return;
|
|
1539
|
-
|
|
1540
|
-
|
|
1564
|
+
// Single-hue header: labels dim-orange, values/emphasis full-orange, so the
|
|
1565
|
+
// four caption rows below the box read as part of the same warm badge.
|
|
1566
|
+
const dimOrange = (s) => `\x1b[2m\x1b[38;2;${_ORANGE_RGB}m${s}\x1b[0m`;
|
|
1567
|
+
const orange = _orange;
|
|
1541
1568
|
const lines = [
|
|
1542
1569
|
'',
|
|
1543
1570
|
..._renderBanner(version),
|
|
1544
1571
|
'',
|
|
1545
|
-
` ${
|
|
1546
|
-
` ${
|
|
1547
|
-
` ${
|
|
1548
|
-
` ${
|
|
1572
|
+
` ${dimOrange('provider ·')} ${orange(activeProvName)}`,
|
|
1573
|
+
` ${dimOrange('model ·')} ${orange(activeModel || '(default)')}`,
|
|
1574
|
+
` ${dimOrange('slash ·')} ${orange('/help · /model · /provider · /exit')}`,
|
|
1575
|
+
` ${dimOrange('hint ·')} ${orange('→')} ${dimOrange('to accept the suggested command,')} ${orange('Tab')} ${dimOrange('to cycle')}`,
|
|
1549
1576
|
'',
|
|
1550
1577
|
];
|
|
1551
1578
|
process.stdout.write(lines.join('\n') + '\n');
|
|
@@ -3567,8 +3594,51 @@ async function cmdNodes(sub, positional, flags = {}) {
|
|
|
3567
3594
|
console.log(JSON.stringify({ ok: true, removed: id }));
|
|
3568
3595
|
return;
|
|
3569
3596
|
}
|
|
3597
|
+
// Device-gateway pairing (Phase 27) — distinct from the config-based
|
|
3598
|
+
// `nodes register` table above. These drive the Ed25519 PairingStore
|
|
3599
|
+
// a companion node authenticates against via `lazyclaw daemon`.
|
|
3600
|
+
case 'pending': {
|
|
3601
|
+
const { PairingStore } = await import('./gateway/device_auth.mjs');
|
|
3602
|
+
const store = new PairingStore(path.dirname(configPath()));
|
|
3603
|
+
console.log(JSON.stringify(store.pending(), null, 2));
|
|
3604
|
+
return;
|
|
3605
|
+
}
|
|
3606
|
+
case 'devices': {
|
|
3607
|
+
const { PairingStore } = await import('./gateway/device_auth.mjs');
|
|
3608
|
+
const store = new PairingStore(path.dirname(configPath()));
|
|
3609
|
+
console.log(JSON.stringify(store.devicesList(), null, 2));
|
|
3610
|
+
return;
|
|
3611
|
+
}
|
|
3612
|
+
case 'approve': {
|
|
3613
|
+
const requestId = positional[0];
|
|
3614
|
+
if (!requestId) {
|
|
3615
|
+
console.error('Usage: lazyclaw nodes approve <requestId> (see `lazyclaw nodes pending`)');
|
|
3616
|
+
process.exit(2);
|
|
3617
|
+
}
|
|
3618
|
+
const { PairingStore } = await import('./gateway/device_auth.mjs');
|
|
3619
|
+
const store = new PairingStore(path.dirname(configPath()));
|
|
3620
|
+
try {
|
|
3621
|
+
const { deviceId } = store.approve(requestId);
|
|
3622
|
+
// The token is intentionally NOT printed — the device receives its
|
|
3623
|
+
// rotated token on its next /gateway/connect, so it never has to
|
|
3624
|
+
// pass through a terminal / shell history.
|
|
3625
|
+
console.log(JSON.stringify({ ok: true, approved: requestId, deviceId, note: 'device receives its token on next /gateway/connect' }));
|
|
3626
|
+
} catch (e) { console.error(`error: ${e.message}`); process.exit(1); }
|
|
3627
|
+
return;
|
|
3628
|
+
}
|
|
3629
|
+
case 'revoke': {
|
|
3630
|
+
const deviceId = positional[0];
|
|
3631
|
+
if (!deviceId) {
|
|
3632
|
+
console.error('Usage: lazyclaw nodes revoke <deviceId>');
|
|
3633
|
+
process.exit(2);
|
|
3634
|
+
}
|
|
3635
|
+
const { PairingStore } = await import('./gateway/device_auth.mjs');
|
|
3636
|
+
const store = new PairingStore(path.dirname(configPath()));
|
|
3637
|
+
console.log(JSON.stringify(store.revoke(deviceId)));
|
|
3638
|
+
return;
|
|
3639
|
+
}
|
|
3570
3640
|
default:
|
|
3571
|
-
console.error('Usage: lazyclaw nodes <list|register|remove> ...');
|
|
3641
|
+
console.error('Usage: lazyclaw nodes <list|register|remove|pending|approve <requestId>|revoke <deviceId>|devices> ...');
|
|
3572
3642
|
process.exit(2);
|
|
3573
3643
|
}
|
|
3574
3644
|
}
|
|
@@ -4372,7 +4442,7 @@ async function cmdMemory(sub, positional, flags = {}) {
|
|
|
4372
4442
|
}
|
|
4373
4443
|
}
|
|
4374
4444
|
|
|
4375
|
-
const AGENT_REG_SUBS = new Set(['add', 'list', 'show', 'edit', 'remove', 'rm', 'delete', 'memory', 'reflect']);
|
|
4445
|
+
const AGENT_REG_SUBS = new Set(['add', 'list', 'show', 'edit', 'remove', 'rm', 'delete', 'memory', 'reflect', 'skill-synth']);
|
|
4376
4446
|
const TEAM_SUBS = new Set(['add', 'list', 'show', 'edit', 'remove', 'rm', 'delete']);
|
|
4377
4447
|
const TASK_SUBS = new Set(['start', 'tick', 'list', 'show', 'abandon', 'done', 'transcript', 'remove', 'rm', 'delete']);
|
|
4378
4448
|
|
|
@@ -4500,14 +4570,36 @@ async function cmdTask(sub, positional, flags = {}) {
|
|
|
4500
4570
|
const cfg = readConfig();
|
|
4501
4571
|
const leadAgent = agentsById[team.lead];
|
|
4502
4572
|
const apiKey = _resolveAuthKey(cfg, leadAgent.provider);
|
|
4503
|
-
// Per-provider base-url override
|
|
4504
|
-
//
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4573
|
+
// Per-provider base-url override (tests point this at a local mock;
|
|
4574
|
+
// production leaves it unset for the built-in default).
|
|
4575
|
+
const baseUrl = _resolveBaseUrl(leadAgent.provider);
|
|
4576
|
+
// --approve-url turns on remote human-in-the-loop approval for the
|
|
4577
|
+
// sensitive tools (bash/write): each such call long-polls a running
|
|
4578
|
+
// daemon's `POST /exec/request`, which broadcasts to paired devices
|
|
4579
|
+
// over the gateway SSE and resolves when one approves. Fail-closed —
|
|
4580
|
+
// any endpoint error denies the call. Omit the flag → ungated (the
|
|
4581
|
+
// historical behavior).
|
|
4582
|
+
let approve;
|
|
4583
|
+
if (flags['approve-url']) {
|
|
4584
|
+
const approveUrl = String(flags['approve-url']).replace(/\/$/, '');
|
|
4585
|
+
const approveToken = flags['approve-token'] ? String(flags['approve-token']) : '';
|
|
4586
|
+
const approveTimeoutMs = flags['approve-timeout'] ? parseInt(flags['approve-timeout'], 10) : 120000;
|
|
4587
|
+
approve = async ({ tool, args, agent }) => {
|
|
4588
|
+
const summary = `${tool}: ${typeof args === 'object' ? JSON.stringify(args) : String(args)}`.slice(0, 400);
|
|
4589
|
+
try {
|
|
4590
|
+
const r = await fetch(`${approveUrl}/exec/request`, {
|
|
4591
|
+
method: 'POST',
|
|
4592
|
+
headers: { 'content-type': 'application/json', ...(approveToken ? { authorization: `Bearer ${approveToken}` } : {}) },
|
|
4593
|
+
body: JSON.stringify({ tool, agentId: agent, summary, timeoutMs: approveTimeoutMs }),
|
|
4594
|
+
});
|
|
4595
|
+
if (!r.ok) return { approved: false, reason: `approval endpoint HTTP ${r.status}` };
|
|
4596
|
+
const j = await r.json();
|
|
4597
|
+
return { approved: !!j.approved, reason: j.reason || (j.approved ? 'approved' : 'denied') };
|
|
4598
|
+
} catch (err) {
|
|
4599
|
+
return { approved: false, reason: `approval request failed: ${err?.message || err}` };
|
|
4600
|
+
}
|
|
4601
|
+
};
|
|
4602
|
+
}
|
|
4511
4603
|
try {
|
|
4512
4604
|
const result = await router.runTaskTurn({
|
|
4513
4605
|
task, team, agentsById,
|
|
@@ -4517,6 +4609,7 @@ async function cmdTask(sub, positional, flags = {}) {
|
|
|
4517
4609
|
baseUrl,
|
|
4518
4610
|
logger: (line) => process.stderr.write(line),
|
|
4519
4611
|
maxAgentTurns: flags['max-turns'] ? parseInt(flags['max-turns'], 10) : undefined,
|
|
4612
|
+
approve,
|
|
4520
4613
|
});
|
|
4521
4614
|
emitJson({ id: result.task.id, status: result.task.status, iterations: result.iterations, stoppedBy: result.stoppedBy });
|
|
4522
4615
|
} catch (err) {
|
|
@@ -4675,7 +4768,7 @@ async function cmdAgentRegistry(sub, positional, flags = {}) {
|
|
|
4675
4768
|
return;
|
|
4676
4769
|
}
|
|
4677
4770
|
case 'add': {
|
|
4678
|
-
if (!name) { console.error('Usage: lazyclaw agent add <name> [--role "..."] [--provider X] [--model Y] [--display "..."] [--tools bash,read,write,grep] [--tags a,b]'); process.exit(2); }
|
|
4771
|
+
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); }
|
|
4679
4772
|
const tools = agentsMod.parseToolsFlag(flags.tools);
|
|
4680
4773
|
try {
|
|
4681
4774
|
const a = agentsMod.registerAgent({
|
|
@@ -4686,6 +4779,7 @@ async function cmdAgentRegistry(sub, positional, flags = {}) {
|
|
|
4686
4779
|
model: flags.model || '',
|
|
4687
4780
|
tools: tools === null ? undefined : tools,
|
|
4688
4781
|
tags: agentsMod.parseToolsFlag(flags.tags) || [],
|
|
4782
|
+
skillWrite: flags['skill-write'],
|
|
4689
4783
|
}, cfgDir);
|
|
4690
4784
|
emitJson(a);
|
|
4691
4785
|
} catch (err) {
|
|
@@ -4702,7 +4796,7 @@ async function cmdAgentRegistry(sub, positional, flags = {}) {
|
|
|
4702
4796
|
return;
|
|
4703
4797
|
}
|
|
4704
4798
|
case 'edit': {
|
|
4705
|
-
if (!name) { console.error('Usage: lazyclaw agent edit <name> [--role "..."] [--provider X] [--model Y] [--display "..."] [--tools ...]'); process.exit(2); }
|
|
4799
|
+
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); }
|
|
4706
4800
|
const patch = {};
|
|
4707
4801
|
if (flags.role !== undefined) patch.role = String(flags.role);
|
|
4708
4802
|
if (flags.provider !== undefined) patch.provider = String(flags.provider);
|
|
@@ -4711,6 +4805,8 @@ async function cmdAgentRegistry(sub, positional, flags = {}) {
|
|
|
4711
4805
|
if (flags['display-name'] !== undefined) patch.displayName = String(flags['display-name']);
|
|
4712
4806
|
if (flags.tools !== undefined) patch.tools = agentsMod.parseToolsFlag(flags.tools);
|
|
4713
4807
|
if (flags.tags !== undefined) patch.tags = agentsMod.parseToolsFlag(flags.tags);
|
|
4808
|
+
if (flags['skill-write'] !== undefined) patch.skillWrite = String(flags['skill-write']);
|
|
4809
|
+
if (flags['memory-write'] !== undefined) patch.memoryWrite = String(flags['memory-write']);
|
|
4714
4810
|
if (Object.keys(patch).length === 0) {
|
|
4715
4811
|
console.error('agent edit: no fields to update');
|
|
4716
4812
|
process.exit(2);
|
|
@@ -4786,11 +4882,7 @@ async function cmdAgentRegistry(sub, positional, flags = {}) {
|
|
|
4786
4882
|
try { _loadDotenvIfAny(cfgDir); } catch { /* best-effort */ }
|
|
4787
4883
|
const cfg = readConfig();
|
|
4788
4884
|
const apiKey = _resolveAuthKey(cfg, a.provider);
|
|
4789
|
-
const baseUrl =
|
|
4790
|
-
anthropic: process.env.LAZYCLAW_ANTHROPIC_BASE_URL,
|
|
4791
|
-
openai: process.env.LAZYCLAW_OPENAI_BASE_URL,
|
|
4792
|
-
gemini: process.env.LAZYCLAW_GEMINI_BASE_URL,
|
|
4793
|
-
}[a.provider] || undefined;
|
|
4885
|
+
const baseUrl = _resolveBaseUrl(a.provider);
|
|
4794
4886
|
try {
|
|
4795
4887
|
const body = await memMod.reflectOnce({ agent: a, task, apiKey, baseUrl });
|
|
4796
4888
|
if (!body || !body.trim()) {
|
|
@@ -4807,8 +4899,50 @@ async function cmdAgentRegistry(sub, positional, flags = {}) {
|
|
|
4807
4899
|
}
|
|
4808
4900
|
return;
|
|
4809
4901
|
}
|
|
4902
|
+
case 'skill-synth': {
|
|
4903
|
+
const aname = positional[0];
|
|
4904
|
+
const taskId = flags.task || positional[1];
|
|
4905
|
+
if (!aname || !taskId) {
|
|
4906
|
+
console.error('Usage: lazyclaw agent skill-synth <name> --task <id> [--dry-run]');
|
|
4907
|
+
process.exit(2);
|
|
4908
|
+
}
|
|
4909
|
+
const tasksMod = await import('./tasks.mjs');
|
|
4910
|
+
const synthMod = await import('./mas/skill_synth.mjs');
|
|
4911
|
+
const skillsMod = await import('./skills.mjs');
|
|
4912
|
+
const a = agentsMod.getAgent(aname, cfgDir);
|
|
4913
|
+
if (!a) { console.error(`agent skill-synth: no agent "${aname}"`); process.exit(2); }
|
|
4914
|
+
const task = tasksMod.getTask(taskId, cfgDir);
|
|
4915
|
+
if (!task) { console.error(`agent skill-synth: no task "${taskId}"`); process.exit(2); }
|
|
4916
|
+
try { _loadDotenvIfAny(cfgDir); } catch { /* best-effort */ }
|
|
4917
|
+
const cfg = readConfig();
|
|
4918
|
+
const apiKey = _resolveAuthKey(cfg, a.provider);
|
|
4919
|
+
const baseUrl = _resolveBaseUrl(a.provider);
|
|
4920
|
+
try {
|
|
4921
|
+
const result = await synthMod.synthesizeSkill({ agent: a, task, apiKey, baseUrl });
|
|
4922
|
+
if (!result) {
|
|
4923
|
+
process.stderr.write('skill synthesis produced nothing worth saving\n');
|
|
4924
|
+
return;
|
|
4925
|
+
}
|
|
4926
|
+
if (!flags['dry-run']) {
|
|
4927
|
+
// installSynthesized reserves a collision-free name (never
|
|
4928
|
+
// clobbers a human-authored skill) and version-bumps when it
|
|
4929
|
+
// improves its own prior skill.
|
|
4930
|
+
const installed = synthMod.installSynthesized(
|
|
4931
|
+
{ name: result.name, description: result.description, body: result.body, sourceTask: task.id },
|
|
4932
|
+
cfgDir,
|
|
4933
|
+
);
|
|
4934
|
+
emitJson({ skill: installed.skill, description: result.description, version: installed.version, path: installed.path });
|
|
4935
|
+
} else {
|
|
4936
|
+
process.stdout.write(result.doc + (result.doc.endsWith('\n') ? '' : '\n'));
|
|
4937
|
+
}
|
|
4938
|
+
} catch (err) {
|
|
4939
|
+
console.error(`agent skill-synth: ${err?.message || err}`);
|
|
4940
|
+
process.exit(2);
|
|
4941
|
+
}
|
|
4942
|
+
return;
|
|
4943
|
+
}
|
|
4810
4944
|
default:
|
|
4811
|
-
console.error('Usage: lazyclaw agent <add|list|show|edit|remove|memory|reflect> ...');
|
|
4945
|
+
console.error('Usage: lazyclaw agent <add|list|show|edit|remove|memory|reflect|skill-synth> ...');
|
|
4812
4946
|
process.exit(2);
|
|
4813
4947
|
}
|
|
4814
4948
|
}
|
|
@@ -4919,6 +5053,165 @@ async function cmdSlack(sub, positional, flags = {}) {
|
|
|
4919
5053
|
});
|
|
4920
5054
|
}
|
|
4921
5055
|
|
|
5056
|
+
// `lazyclaw telegram listen` — zero-install mobile control surface.
|
|
5057
|
+
// Long-polls the Telegram Bot API (no public URL / webhook needed) and
|
|
5058
|
+
// pipes each inbound message through the active provider, replying in
|
|
5059
|
+
// the same chat. Mirrors `slack listen`. Access is gated by the existing
|
|
5060
|
+
// `pairing` allowlist (Telegram numeric user ids); an empty allowlist
|
|
5061
|
+
// means "reply to anyone who can reach the bot".
|
|
5062
|
+
async function cmdTelegram(sub, positional, flags = {}) {
|
|
5063
|
+
if (sub !== 'listen') {
|
|
5064
|
+
console.error('Usage: lazyclaw telegram listen [--provider X] [--model Y]\n Long-polls the Telegram Bot API. Set TELEGRAM_BOT_TOKEN in ~/.lazyclaw/.env.\n Restrict who can talk to it with `lazyclaw pairing add <telegram-user-id>`.');
|
|
5065
|
+
process.exit(2);
|
|
5066
|
+
}
|
|
5067
|
+
await ensureRegistry();
|
|
5068
|
+
const cfg = readConfig();
|
|
5069
|
+
const cfgDir = path.dirname(configPath());
|
|
5070
|
+
|
|
5071
|
+
const envInfo = _loadDotenvIfAny(cfgDir);
|
|
5072
|
+
process.stderr.write(`[telegram] .env: ${envInfo.loaded} keys loaded from ${envInfo.path}\n`);
|
|
5073
|
+
|
|
5074
|
+
const provName = flags.provider || cfg.provider || 'mock';
|
|
5075
|
+
const prov = _registryMod.PROVIDERS[provName];
|
|
5076
|
+
if (!prov) { console.error(`unknown provider: ${provName}`); process.exit(2); }
|
|
5077
|
+
const model = flags.model || cfg.model;
|
|
5078
|
+
|
|
5079
|
+
const threadMsgs = new Map();
|
|
5080
|
+
const MAX_TURNS = 20;
|
|
5081
|
+
|
|
5082
|
+
const handler = async ({ threadId, text }) => {
|
|
5083
|
+
const cleaned = String(text || '').trim();
|
|
5084
|
+
if (!cleaned) { process.stderr.write('[telegram] dropping empty inbound\n'); return null; }
|
|
5085
|
+
const msgs = threadMsgs.get(threadId) || [];
|
|
5086
|
+
msgs.push({ role: 'user', content: cleaned });
|
|
5087
|
+
let acc = '';
|
|
5088
|
+
try {
|
|
5089
|
+
for await (const chunk of prov.sendMessage(msgs, { apiKey: _resolveAuthKey(cfg, provName), model })) acc += chunk;
|
|
5090
|
+
} catch (err) {
|
|
5091
|
+
msgs.pop();
|
|
5092
|
+
const why = err?.message || String(err);
|
|
5093
|
+
process.stderr.write(`[telegram] provider error: ${why}\n`);
|
|
5094
|
+
return `(provider error: ${why})`;
|
|
5095
|
+
}
|
|
5096
|
+
msgs.push({ role: 'assistant', content: acc });
|
|
5097
|
+
if (msgs.length > MAX_TURNS) msgs.splice(0, msgs.length - MAX_TURNS);
|
|
5098
|
+
threadMsgs.set(threadId, msgs);
|
|
5099
|
+
if (!acc.trim()) { process.stderr.write('[telegram] provider returned empty text — not posting\n'); return null; }
|
|
5100
|
+
return acc;
|
|
5101
|
+
};
|
|
5102
|
+
|
|
5103
|
+
// The pairing allowlist doubles as the Telegram sender allowlist.
|
|
5104
|
+
const allowlist = (cfg.pairing || []).map((p) => String(p.id));
|
|
5105
|
+
const { TelegramChannel } = await import('./channels/telegram.mjs');
|
|
5106
|
+
let ch;
|
|
5107
|
+
try {
|
|
5108
|
+
ch = new TelegramChannel({ allowlist: allowlist.length ? allowlist : null });
|
|
5109
|
+
} catch (err) {
|
|
5110
|
+
console.error(`telegram: ${err?.message || err}`);
|
|
5111
|
+
process.exit(2);
|
|
5112
|
+
}
|
|
5113
|
+
process.stderr.write(`[telegram] provider=${provName} model=${model || '(default)'} allowlist=${allowlist.length || 'open'}\n`);
|
|
5114
|
+
try {
|
|
5115
|
+
await ch.start(handler, { poll: true, logger: (line) => process.stderr.write(line) });
|
|
5116
|
+
} catch (err) {
|
|
5117
|
+
if (err?.code === 'TELEGRAM_MISSING_TOKEN') {
|
|
5118
|
+
console.error('telegram: TELEGRAM_BOT_TOKEN not set');
|
|
5119
|
+
console.error(`hint: add TELEGRAM_BOT_TOKEN=... to ${path.join(cfgDir, '.env')}`);
|
|
5120
|
+
} else {
|
|
5121
|
+
console.error(`telegram: ${err?.message || err}`);
|
|
5122
|
+
}
|
|
5123
|
+
process.exit(2);
|
|
5124
|
+
}
|
|
5125
|
+
process.stderr.write(`[telegram] listening. Ctrl-C to stop.\n`);
|
|
5126
|
+
|
|
5127
|
+
await new Promise((resolve) => {
|
|
5128
|
+
const onSig = async () => {
|
|
5129
|
+
process.stderr.write(`\n[telegram] shutting down…\n`);
|
|
5130
|
+
try { await ch.stop(); } catch { /* best-effort */ }
|
|
5131
|
+
resolve();
|
|
5132
|
+
};
|
|
5133
|
+
process.once('SIGINT', onSig);
|
|
5134
|
+
process.once('SIGTERM', onSig);
|
|
5135
|
+
});
|
|
5136
|
+
}
|
|
5137
|
+
|
|
5138
|
+
// `lazyclaw matrix listen` — Matrix inbound over the client-server API's
|
|
5139
|
+
// long-poll /sync (no SDK). Mirrors `telegram listen`. Set MATRIX_HOMESERVER
|
|
5140
|
+
// + MATRIX_ACCESS_TOKEN (+ MATRIX_USER_ID for self-filtering) in ~/.lazyclaw/.env.
|
|
5141
|
+
async function cmdMatrix(sub, positional, flags = {}) {
|
|
5142
|
+
if (sub !== 'listen') {
|
|
5143
|
+
console.error('Usage: lazyclaw matrix listen [--provider X] [--model Y]\n Long-polls the Matrix /sync API. Set MATRIX_HOMESERVER + MATRIX_ACCESS_TOKEN (+ MATRIX_USER_ID) in ~/.lazyclaw/.env.\n Restrict who can talk to it with `lazyclaw pairing add <@user:server>`.');
|
|
5144
|
+
process.exit(2);
|
|
5145
|
+
}
|
|
5146
|
+
await ensureRegistry();
|
|
5147
|
+
const cfg = readConfig();
|
|
5148
|
+
const cfgDir = path.dirname(configPath());
|
|
5149
|
+
|
|
5150
|
+
const envInfo = _loadDotenvIfAny(cfgDir);
|
|
5151
|
+
process.stderr.write(`[matrix] .env: ${envInfo.loaded} keys loaded from ${envInfo.path}\n`);
|
|
5152
|
+
|
|
5153
|
+
const provName = flags.provider || cfg.provider || 'mock';
|
|
5154
|
+
const prov = _registryMod.PROVIDERS[provName];
|
|
5155
|
+
if (!prov) { console.error(`unknown provider: ${provName}`); process.exit(2); }
|
|
5156
|
+
const model = flags.model || cfg.model;
|
|
5157
|
+
|
|
5158
|
+
const threadMsgs = new Map();
|
|
5159
|
+
const MAX_TURNS = 20;
|
|
5160
|
+
const handler = async ({ threadId, text }) => {
|
|
5161
|
+
const cleaned = String(text || '').trim();
|
|
5162
|
+
if (!cleaned) { process.stderr.write('[matrix] dropping empty inbound\n'); return null; }
|
|
5163
|
+
const msgs = threadMsgs.get(threadId) || [];
|
|
5164
|
+
msgs.push({ role: 'user', content: cleaned });
|
|
5165
|
+
let acc = '';
|
|
5166
|
+
try {
|
|
5167
|
+
for await (const chunk of prov.sendMessage(msgs, { apiKey: _resolveAuthKey(cfg, provName), model })) acc += chunk;
|
|
5168
|
+
} catch (err) {
|
|
5169
|
+
msgs.pop();
|
|
5170
|
+
const why = err?.message || String(err);
|
|
5171
|
+
process.stderr.write(`[matrix] provider error: ${why}\n`);
|
|
5172
|
+
return `(provider error: ${why})`;
|
|
5173
|
+
}
|
|
5174
|
+
msgs.push({ role: 'assistant', content: acc });
|
|
5175
|
+
if (msgs.length > MAX_TURNS) msgs.splice(0, msgs.length - MAX_TURNS);
|
|
5176
|
+
threadMsgs.set(threadId, msgs);
|
|
5177
|
+
if (!acc.trim()) { process.stderr.write('[matrix] provider returned empty text — not posting\n'); return null; }
|
|
5178
|
+
return acc;
|
|
5179
|
+
};
|
|
5180
|
+
|
|
5181
|
+
const allowlist = (cfg.pairing || []).map((p) => String(p.id));
|
|
5182
|
+
const { MatrixChannel } = await import('./channels/matrix.mjs');
|
|
5183
|
+
let ch;
|
|
5184
|
+
try {
|
|
5185
|
+
ch = new MatrixChannel({ allowlist: allowlist.length ? allowlist : null });
|
|
5186
|
+
} catch (err) {
|
|
5187
|
+
console.error(`matrix: ${err?.message || err}`);
|
|
5188
|
+
process.exit(2);
|
|
5189
|
+
}
|
|
5190
|
+
process.stderr.write(`[matrix] provider=${provName} model=${model || '(default)'} allowlist=${allowlist.length || 'open'}\n`);
|
|
5191
|
+
try {
|
|
5192
|
+
await ch.start(handler, { poll: true, logger: (line) => process.stderr.write(line) });
|
|
5193
|
+
} catch (err) {
|
|
5194
|
+
if (err?.code === 'MATRIX_MISSING_TOKEN' || err?.code === 'MATRIX_MISSING_HOMESERVER') {
|
|
5195
|
+
console.error(`matrix: ${err.message}`);
|
|
5196
|
+
console.error(`hint: set MATRIX_HOMESERVER and MATRIX_ACCESS_TOKEN in ${path.join(cfgDir, '.env')}`);
|
|
5197
|
+
} else {
|
|
5198
|
+
console.error(`matrix: ${err?.message || err}`);
|
|
5199
|
+
}
|
|
5200
|
+
process.exit(2);
|
|
5201
|
+
}
|
|
5202
|
+
process.stderr.write(`[matrix] listening. Ctrl-C to stop.\n`);
|
|
5203
|
+
|
|
5204
|
+
await new Promise((resolve) => {
|
|
5205
|
+
const onSig = async () => {
|
|
5206
|
+
process.stderr.write(`\n[matrix] shutting down…\n`);
|
|
5207
|
+
try { await ch.stop(); } catch { /* best-effort */ }
|
|
5208
|
+
resolve();
|
|
5209
|
+
};
|
|
5210
|
+
process.once('SIGINT', onSig);
|
|
5211
|
+
process.once('SIGTERM', onSig);
|
|
5212
|
+
});
|
|
5213
|
+
}
|
|
5214
|
+
|
|
4922
5215
|
async function cmdSkills(sub, positional, flags = {}) {
|
|
4923
5216
|
const skillsMod = await import('./skills.mjs');
|
|
4924
5217
|
const cfgDir = path.dirname(configPath());
|
|
@@ -5110,8 +5403,24 @@ async function cmdSkills(sub, positional, flags = {}) {
|
|
|
5110
5403
|
console.log(JSON.stringify({ query, regex: useRegex, matches }, null, 2));
|
|
5111
5404
|
return;
|
|
5112
5405
|
}
|
|
5406
|
+
case 'curate': {
|
|
5407
|
+
// Lifecycle sweep: agent-authored skills unused >90d move into
|
|
5408
|
+
// skills/.archive/ (recoverable). Human-authored skills are never
|
|
5409
|
+
// moved. The real clock is injected here; the module stays pure.
|
|
5410
|
+
const curator = await import('./skills_curator.mjs');
|
|
5411
|
+
const r = curator.curate(cfgDir, Date.now());
|
|
5412
|
+
console.log(JSON.stringify(r, null, 2));
|
|
5413
|
+
return;
|
|
5414
|
+
}
|
|
5415
|
+
case 'classify': {
|
|
5416
|
+
const name = positional[0];
|
|
5417
|
+
if (!name) { console.error('Usage: lazyclaw skills classify <name>'); process.exit(2); }
|
|
5418
|
+
const curator = await import('./skills_curator.mjs');
|
|
5419
|
+
console.log(JSON.stringify({ name, state: curator.classify(name, cfgDir, Date.now()), usage: curator.usageOf(name, cfgDir) }, null, 2));
|
|
5420
|
+
return;
|
|
5421
|
+
}
|
|
5113
5422
|
default:
|
|
5114
|
-
console.error('Usage: lazyclaw skills <list|show <name>|install <name> [--from path]|remove <name>|search <query> [--regex]
|
|
5423
|
+
console.error('Usage: lazyclaw skills <list|show <name>|install <name> [--from path]|remove <name>|search <query> [--regex]|curate|classify <name>>');
|
|
5115
5424
|
process.exit(2);
|
|
5116
5425
|
}
|
|
5117
5426
|
}
|
|
@@ -5987,6 +6296,8 @@ async function _dispatchMenuChoice(argv) {
|
|
|
5987
6296
|
case 'goal': return await cmdGoal(rest[0], rest.slice(1), {});
|
|
5988
6297
|
case 'memory': return await cmdMemory(rest[0], rest.slice(1), {});
|
|
5989
6298
|
case 'slack': return await cmdSlack(rest[0], rest.slice(1), {});
|
|
6299
|
+
case 'telegram': return await cmdTelegram(rest[0], rest.slice(1), {});
|
|
6300
|
+
case 'matrix': return await cmdMatrix(rest[0], rest.slice(1), {});
|
|
5990
6301
|
case 'team': return await cmdTeam(rest[0], rest.slice(1), {});
|
|
5991
6302
|
case 'task': return await cmdTask(rest[0], rest.slice(1), {});
|
|
5992
6303
|
case 'auth': return await cmdAuth(rest[0], rest.slice(1), {});
|
|
@@ -6538,6 +6849,16 @@ async function main() {
|
|
|
6538
6849
|
await cmdSlack(sub, rest.positional.slice(1), rest.flags);
|
|
6539
6850
|
break;
|
|
6540
6851
|
}
|
|
6852
|
+
case 'telegram': {
|
|
6853
|
+
const sub = rest.positional[0];
|
|
6854
|
+
await cmdTelegram(sub, rest.positional.slice(1), rest.flags);
|
|
6855
|
+
break;
|
|
6856
|
+
}
|
|
6857
|
+
case 'matrix': {
|
|
6858
|
+
const sub = rest.positional[0];
|
|
6859
|
+
await cmdMatrix(sub, rest.positional.slice(1), rest.flags);
|
|
6860
|
+
break;
|
|
6861
|
+
}
|
|
6541
6862
|
case 'team': {
|
|
6542
6863
|
const sub = rest.positional[0];
|
|
6543
6864
|
await cmdTeam(sub, rest.positional.slice(1), rest.flags);
|