lazyclaw 5.4.4 → 6.0.1

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.
Files changed (91) hide show
  1. package/channels/handoff.mjs +36 -0
  2. package/channels-discord/index.mjs +76 -0
  3. package/channels-discord/package.json +14 -0
  4. package/channels-email/index.mjs +109 -0
  5. package/channels-email/package.json +14 -0
  6. package/channels-signal/index.mjs +69 -0
  7. package/channels-signal/package.json +9 -0
  8. package/channels-voice/index.mjs +81 -0
  9. package/channels-voice/package.json +9 -0
  10. package/channels-whatsapp/index.mjs +70 -0
  11. package/channels-whatsapp/package.json +13 -0
  12. package/cli.mjs +73 -7399
  13. package/commands/agents.mjs +669 -0
  14. package/commands/auth_nodes.mjs +323 -0
  15. package/commands/automation.mjs +582 -0
  16. package/commands/channels.mjs +255 -0
  17. package/commands/chat.mjs +1217 -0
  18. package/commands/config.mjs +315 -0
  19. package/commands/daemon.mjs +260 -0
  20. package/commands/misc.mjs +128 -0
  21. package/commands/providers.mjs +511 -0
  22. package/commands/sessions.mjs +343 -0
  23. package/commands/setup.mjs +741 -0
  24. package/commands/skills.mjs +218 -0
  25. package/commands/workflow.mjs +661 -0
  26. package/daemon/lib/auth.mjs +58 -0
  27. package/daemon/lib/cost.mjs +30 -0
  28. package/daemon/lib/provider.mjs +69 -0
  29. package/daemon/lib/respond.mjs +83 -0
  30. package/daemon/route_table.mjs +96 -0
  31. package/daemon/routes/_deps.mjs +36 -0
  32. package/daemon/routes/config.mjs +99 -0
  33. package/daemon/routes/conversation.mjs +371 -0
  34. package/daemon/routes/meta.mjs +239 -0
  35. package/daemon/routes/ops.mjs +185 -0
  36. package/daemon/routes/providers.mjs +211 -0
  37. package/daemon/routes/rates.mjs +90 -0
  38. package/daemon/routes/registry.mjs +223 -0
  39. package/daemon/routes/sessions.mjs +213 -0
  40. package/daemon/routes/skills.mjs +260 -0
  41. package/daemon/routes/workflows.mjs +224 -0
  42. package/daemon.mjs +23 -2085
  43. package/dotenv_min.mjs +23 -0
  44. package/first_run.mjs +15 -0
  45. package/goals_cron.mjs +37 -0
  46. package/lib/args.mjs +216 -0
  47. package/lib/config.mjs +113 -0
  48. package/lib/registry_boot.mjs +55 -0
  49. package/mas/agent_turn.mjs +2 -1
  50. package/mas/index_db.mjs +82 -0
  51. package/mas/learning.mjs +17 -1
  52. package/mas/mention_router.mjs +38 -10
  53. package/mas/provider_adapters.mjs +28 -4
  54. package/mas/scrub_env.mjs +34 -0
  55. package/mas/tool_runner.mjs +23 -7
  56. package/mas/tools/bash.mjs +10 -5
  57. package/mas/tools/browser.mjs +18 -0
  58. package/mas/tools/learning.mjs +24 -14
  59. package/mas/tools/recall.mjs +5 -1
  60. package/mas/tools/web.mjs +47 -11
  61. package/mas/trajectory_store.mjs +7 -4
  62. package/package.json +16 -2
  63. package/providers/auth_store.mjs +22 -0
  64. package/providers/claude_cli.mjs +28 -2
  65. package/providers/claude_cli_detect.mjs +46 -0
  66. package/providers/custom_provider.mjs +70 -0
  67. package/providers/model_catalogue.mjs +86 -0
  68. package/providers/orchestrator.mjs +30 -9
  69. package/providers/rates.mjs +12 -2
  70. package/providers/registry.mjs +10 -7
  71. package/sandbox/confiners/landlock.mjs +14 -8
  72. package/sandbox/confiners/seatbelt.mjs +18 -2
  73. package/scripts/loop-worker.mjs +18 -7
  74. package/scripts/migrate-v5.mjs +5 -61
  75. package/secure_write.mjs +46 -0
  76. package/sessions.mjs +0 -0
  77. package/tui/modal_filter.mjs +59 -0
  78. package/tui/modal_picker.mjs +12 -37
  79. package/tui/pickers.mjs +917 -0
  80. package/tui/provider_families.mjs +41 -0
  81. package/tui/repl.mjs +67 -36
  82. package/tui/slash_commands.mjs +2 -1
  83. package/tui/slash_dispatcher.mjs +717 -58
  84. package/tui/splash.mjs +5 -12
  85. package/tui/subcommands.mjs +17 -0
  86. package/tui/terminal_approve.mjs +37 -0
  87. package/web/dashboard.css +275 -0
  88. package/web/dashboard.html +2 -1685
  89. package/web/dashboard.js +1406 -0
  90. package/workflow/persistent.mjs +13 -6
  91. package/mas/tools/skill_view.mjs +0 -43
@@ -0,0 +1,255 @@
1
+ // Slack / Telegram / Matrix listener commands, extracted from cli.mjs (D3).
2
+ import path from 'node:path';
3
+ import { configPath, readConfig, _resolveAuthKey } from '../lib/config.mjs';
4
+ import { ensureRegistry, getRegistry } from '../lib/registry_boot.mjs';
5
+ import { loadDotenvIfAny as _loadDotenvShared } from '../dotenv_min.mjs';
6
+
7
+ // Thin .env loader wrapper kept local so the module stays self-contained.
8
+ export function _loadDotenvIfAny(cfgDir) { return _loadDotenvShared(cfgDir); }
9
+
10
+ export async function cmdSlack(sub, positional, flags = {}) {
11
+ if (sub !== 'listen') {
12
+ console.error('Usage: lazyclaw slack listen [--provider X] [--model Y]');
13
+ process.exit(2);
14
+ }
15
+ await ensureRegistry();
16
+ const cfg = readConfig();
17
+ const cfgDir = path.dirname(configPath());
18
+
19
+ const envInfo = _loadDotenvIfAny(cfgDir);
20
+ process.stderr.write(`[slack] .env: ${envInfo.loaded} keys loaded from ${envInfo.path}\n`);
21
+
22
+ const provName = flags.provider || cfg.provider || 'mock';
23
+ const prov = getRegistry().PROVIDERS[provName];
24
+ if (!prov) { console.error(`unknown provider: ${provName}`); process.exit(2); }
25
+ const model = flags.model || cfg.model;
26
+
27
+ // Per-thread rolling chat history so multi-turn coherence works
28
+ // without committing to on-disk sessions. Capped at MAX_TURNS to
29
+ // bound the prompt size.
30
+ const threadMsgs = new Map();
31
+ const MAX_TURNS = 20;
32
+
33
+ const handler = async ({ threadId, text }) => {
34
+ const cleaned = String(text || '').replace(/<@[A-Z0-9]+>/g, '').trim();
35
+ // Phase 19.2: never post a placeholder ("(empty message)" / "(empty
36
+ // reply)") into the thread — those leaked through as visible noise
37
+ // when listener self-message echoes happened. Return null and let
38
+ // _simulateInbound's guard drop the send. Real provider errors
39
+ // still surface so the operator knows something went wrong.
40
+ if (!cleaned) {
41
+ process.stderr.write('[slack] dropping empty inbound (after mention strip)\n');
42
+ return null;
43
+ }
44
+ const msgs = threadMsgs.get(threadId) || [];
45
+ msgs.push({ role: 'user', content: cleaned });
46
+ let acc = '';
47
+ try {
48
+ for await (const chunk of prov.sendMessage(msgs, {
49
+ apiKey: _resolveAuthKey(cfg, provName),
50
+ model,
51
+ })) acc += chunk;
52
+ } catch (err) {
53
+ msgs.pop();
54
+ const why = err?.message || String(err);
55
+ process.stderr.write(`[slack] provider error: ${why}\n`);
56
+ return `(provider error: ${why})`;
57
+ }
58
+ msgs.push({ role: 'assistant', content: acc });
59
+ if (msgs.length > MAX_TURNS) msgs.splice(0, msgs.length - MAX_TURNS);
60
+ threadMsgs.set(threadId, msgs);
61
+ if (!acc.trim()) {
62
+ process.stderr.write('[slack] provider returned empty text — not posting\n');
63
+ return null;
64
+ }
65
+ return acc;
66
+ };
67
+
68
+ const { SlackChannel } = await import('../channels/slack.mjs');
69
+ const ch = new SlackChannel();
70
+ process.stderr.write(`[slack] provider=${provName} model=${model || '(default)'}\n`);
71
+ try {
72
+ await ch.start(handler);
73
+ await ch._connectSocketMode({ logger: (line) => process.stderr.write(line) });
74
+ } catch (err) {
75
+ if (err?.code === 'SLACK_MISSING_ENV') {
76
+ console.error(`slack: missing env vars: ${(err.missing || []).join(', ')}`);
77
+ console.error(`hint: set them in ${path.join(cfgDir, '.env')} (uncomment SLACK_APP_TOKEN / SLACK_SIGNING_SECRET)`);
78
+ } else {
79
+ console.error(`slack: ${err?.message || err}`);
80
+ }
81
+ process.exit(2);
82
+ }
83
+ process.stderr.write(`[slack] listening. Ctrl-C to stop.\n`);
84
+
85
+ await new Promise((resolve) => {
86
+ const onSig = async () => {
87
+ process.stderr.write(`\n[slack] shutting down…\n`);
88
+ try { await ch.stop(); } catch { /* best-effort */ }
89
+ resolve();
90
+ };
91
+ process.once('SIGINT', onSig);
92
+ process.once('SIGTERM', onSig);
93
+ });
94
+ }
95
+
96
+ // `lazyclaw telegram listen` — zero-install mobile control surface.
97
+ // Long-polls the Telegram Bot API (no public URL / webhook needed) and
98
+ // pipes each inbound message through the active provider, replying in
99
+ // the same chat. Mirrors `slack listen`. Access is gated by the existing
100
+ // `pairing` allowlist (Telegram numeric user ids); an empty allowlist
101
+ // means "reply to anyone who can reach the bot".
102
+ export async function cmdTelegram(sub, positional, flags = {}) {
103
+ if (sub !== 'listen') {
104
+ 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>`.');
105
+ process.exit(2);
106
+ }
107
+ await ensureRegistry();
108
+ const cfg = readConfig();
109
+ const cfgDir = path.dirname(configPath());
110
+
111
+ const envInfo = _loadDotenvIfAny(cfgDir);
112
+ process.stderr.write(`[telegram] .env: ${envInfo.loaded} keys loaded from ${envInfo.path}\n`);
113
+
114
+ const provName = flags.provider || cfg.provider || 'mock';
115
+ const prov = getRegistry().PROVIDERS[provName];
116
+ if (!prov) { console.error(`unknown provider: ${provName}`); process.exit(2); }
117
+ const model = flags.model || cfg.model;
118
+
119
+ const threadMsgs = new Map();
120
+ const MAX_TURNS = 20;
121
+
122
+ const handler = async ({ threadId, text }) => {
123
+ const cleaned = String(text || '').trim();
124
+ if (!cleaned) { process.stderr.write('[telegram] dropping empty inbound\n'); return null; }
125
+ const msgs = threadMsgs.get(threadId) || [];
126
+ msgs.push({ role: 'user', content: cleaned });
127
+ let acc = '';
128
+ try {
129
+ for await (const chunk of prov.sendMessage(msgs, { apiKey: _resolveAuthKey(cfg, provName), model })) acc += chunk;
130
+ } catch (err) {
131
+ msgs.pop();
132
+ const why = err?.message || String(err);
133
+ process.stderr.write(`[telegram] provider error: ${why}\n`);
134
+ return `(provider error: ${why})`;
135
+ }
136
+ msgs.push({ role: 'assistant', content: acc });
137
+ if (msgs.length > MAX_TURNS) msgs.splice(0, msgs.length - MAX_TURNS);
138
+ threadMsgs.set(threadId, msgs);
139
+ if (!acc.trim()) { process.stderr.write('[telegram] provider returned empty text — not posting\n'); return null; }
140
+ return acc;
141
+ };
142
+
143
+ // The pairing allowlist doubles as the Telegram sender allowlist.
144
+ const allowlist = (cfg.pairing || []).map((p) => String(p.id));
145
+ const { TelegramChannel } = await import('../channels/telegram.mjs');
146
+ let ch;
147
+ try {
148
+ ch = new TelegramChannel({ allowlist: allowlist.length ? allowlist : null });
149
+ } catch (err) {
150
+ console.error(`telegram: ${err?.message || err}`);
151
+ process.exit(2);
152
+ }
153
+ process.stderr.write(`[telegram] provider=${provName} model=${model || '(default)'} allowlist=${allowlist.length || 'open'}\n`);
154
+ try {
155
+ await ch.start(handler, { poll: true, logger: (line) => process.stderr.write(line) });
156
+ } catch (err) {
157
+ if (err?.code === 'TELEGRAM_MISSING_TOKEN') {
158
+ console.error('telegram: TELEGRAM_BOT_TOKEN not set');
159
+ console.error(`hint: add TELEGRAM_BOT_TOKEN=... to ${path.join(cfgDir, '.env')}`);
160
+ } else {
161
+ console.error(`telegram: ${err?.message || err}`);
162
+ }
163
+ process.exit(2);
164
+ }
165
+ process.stderr.write(`[telegram] listening. Ctrl-C to stop.\n`);
166
+
167
+ await new Promise((resolve) => {
168
+ const onSig = async () => {
169
+ process.stderr.write(`\n[telegram] shutting down…\n`);
170
+ try { await ch.stop(); } catch { /* best-effort */ }
171
+ resolve();
172
+ };
173
+ process.once('SIGINT', onSig);
174
+ process.once('SIGTERM', onSig);
175
+ });
176
+ }
177
+
178
+ // `lazyclaw matrix listen` — Matrix inbound over the client-server API's
179
+ // long-poll /sync (no SDK). Mirrors `telegram listen`. Set MATRIX_HOMESERVER
180
+ // + MATRIX_ACCESS_TOKEN (+ MATRIX_USER_ID for self-filtering) in ~/.lazyclaw/.env.
181
+ export async function cmdMatrix(sub, positional, flags = {}) {
182
+ if (sub !== 'listen') {
183
+ 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>`.');
184
+ process.exit(2);
185
+ }
186
+ await ensureRegistry();
187
+ const cfg = readConfig();
188
+ const cfgDir = path.dirname(configPath());
189
+
190
+ const envInfo = _loadDotenvIfAny(cfgDir);
191
+ process.stderr.write(`[matrix] .env: ${envInfo.loaded} keys loaded from ${envInfo.path}\n`);
192
+
193
+ const provName = flags.provider || cfg.provider || 'mock';
194
+ const prov = getRegistry().PROVIDERS[provName];
195
+ if (!prov) { console.error(`unknown provider: ${provName}`); process.exit(2); }
196
+ const model = flags.model || cfg.model;
197
+
198
+ const threadMsgs = new Map();
199
+ const MAX_TURNS = 20;
200
+ const handler = async ({ threadId, text }) => {
201
+ const cleaned = String(text || '').trim();
202
+ if (!cleaned) { process.stderr.write('[matrix] dropping empty inbound\n'); return null; }
203
+ const msgs = threadMsgs.get(threadId) || [];
204
+ msgs.push({ role: 'user', content: cleaned });
205
+ let acc = '';
206
+ try {
207
+ for await (const chunk of prov.sendMessage(msgs, { apiKey: _resolveAuthKey(cfg, provName), model })) acc += chunk;
208
+ } catch (err) {
209
+ msgs.pop();
210
+ const why = err?.message || String(err);
211
+ process.stderr.write(`[matrix] provider error: ${why}\n`);
212
+ return `(provider error: ${why})`;
213
+ }
214
+ msgs.push({ role: 'assistant', content: acc });
215
+ if (msgs.length > MAX_TURNS) msgs.splice(0, msgs.length - MAX_TURNS);
216
+ threadMsgs.set(threadId, msgs);
217
+ if (!acc.trim()) { process.stderr.write('[matrix] provider returned empty text — not posting\n'); return null; }
218
+ return acc;
219
+ };
220
+
221
+ const allowlist = (cfg.pairing || []).map((p) => String(p.id));
222
+ const { MatrixChannel } = await import('../channels/matrix.mjs');
223
+ let ch;
224
+ try {
225
+ ch = new MatrixChannel({ allowlist: allowlist.length ? allowlist : null });
226
+ } catch (err) {
227
+ console.error(`matrix: ${err?.message || err}`);
228
+ process.exit(2);
229
+ }
230
+ process.stderr.write(`[matrix] provider=${provName} model=${model || '(default)'} allowlist=${allowlist.length || 'open'}\n`);
231
+ try {
232
+ await ch.start(handler, { poll: true, logger: (line) => process.stderr.write(line) });
233
+ } catch (err) {
234
+ if (err?.code === 'MATRIX_MISSING_TOKEN' || err?.code === 'MATRIX_MISSING_HOMESERVER') {
235
+ console.error(`matrix: ${err.message}`);
236
+ console.error(`hint: set MATRIX_HOMESERVER and MATRIX_ACCESS_TOKEN in ${path.join(cfgDir, '.env')}`);
237
+ } else {
238
+ console.error(`matrix: ${err?.message || err}`);
239
+ }
240
+ process.exit(2);
241
+ }
242
+ process.stderr.write(`[matrix] listening. Ctrl-C to stop.\n`);
243
+
244
+ await new Promise((resolve) => {
245
+ const onSig = async () => {
246
+ process.stderr.write(`\n[matrix] shutting down…\n`);
247
+ try { await ch.stop(); } catch { /* best-effort */ }
248
+ resolve();
249
+ };
250
+ process.once('SIGINT', onSig);
251
+ process.once('SIGTERM', onSig);
252
+ });
253
+ }
254
+
255
+