lazyclaw 6.0.0 → 6.1.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 +88 -25
- package/README.md +121 -190
- package/channels/handoff.mjs +18 -5
- package/channels/matrix.mjs +23 -5
- package/channels/slack.mjs +83 -50
- package/channels/slack_env.mjs +45 -0
- package/channels/telegram.mjs +49 -6
- package/channels-discord/index.mjs +76 -0
- package/channels-discord/package.json +14 -0
- package/channels-email/index.mjs +109 -0
- package/channels-email/package.json +14 -0
- package/channels-signal/index.mjs +69 -0
- package/channels-signal/package.json +9 -0
- package/channels-voice/index.mjs +81 -0
- package/channels-voice/package.json +9 -0
- package/channels-whatsapp/index.mjs +70 -0
- package/channels-whatsapp/package.json +13 -0
- package/cli.mjs +10 -21
- package/commands/agents.mjs +669 -0
- package/commands/auth_nodes.mjs +323 -0
- package/commands/automation.mjs +582 -0
- package/commands/channels.mjs +254 -0
- package/commands/chat.mjs +1253 -0
- package/commands/config.mjs +315 -0
- package/commands/daemon.mjs +275 -0
- package/commands/gateway.mjs +194 -0
- package/commands/misc.mjs +128 -0
- package/commands/providers.mjs +490 -0
- package/commands/service.mjs +113 -0
- package/commands/sessions.mjs +343 -0
- package/commands/setup.mjs +742 -0
- package/commands/setup_channels.mjs +207 -0
- package/commands/skills.mjs +218 -0
- package/commands/workflow.mjs +661 -0
- package/config_features.mjs +106 -0
- package/daemon/lib/auth.mjs +58 -0
- package/daemon/lib/cost.mjs +30 -0
- package/daemon/lib/inbound_dedup.mjs +108 -0
- package/daemon/lib/learn_queue.mjs +46 -0
- package/daemon/lib/provider.mjs +69 -0
- package/daemon/lib/respond.mjs +83 -0
- package/daemon/route_table.mjs +96 -0
- package/daemon/routes/_deps.mjs +42 -0
- package/daemon/routes/config.mjs +99 -0
- package/daemon/routes/conversation.mjs +435 -0
- package/daemon/routes/meta.mjs +239 -0
- package/daemon/routes/ops.mjs +165 -0
- package/daemon/routes/providers.mjs +211 -0
- package/daemon/routes/rates.mjs +90 -0
- package/daemon/routes/registry.mjs +223 -0
- package/daemon/routes/sessions.mjs +213 -0
- package/daemon/routes/skills.mjs +260 -0
- package/daemon/routes/workflows.mjs +224 -0
- package/dotenv_min.mjs +51 -0
- package/first_run.mjs +24 -0
- package/goals_cron.mjs +37 -0
- package/lib/args.mjs +216 -0
- package/lib/config.mjs +113 -0
- package/lib/gateway_guard.mjs +100 -0
- package/lib/inbound_client.mjs +108 -0
- package/lib/registry_boot.mjs +55 -0
- package/lib/service_install.mjs +207 -0
- package/package.json +15 -3
- package/providers/probe.mjs +28 -0
- package/secure_write.mjs +46 -0
- package/tui/editor.mjs +18 -2
- package/tui/repl.mjs +25 -4
- package/tui/slash_commands.mjs +4 -0
- package/tui/slash_dispatcher.mjs +118 -0
- package/tui/splash_props.mjs +52 -0
- package/web/dashboard.css +6 -12
- package/web/dashboard.html +2 -34
- package/web/dashboard.js +3 -3
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// Slack / Telegram / Matrix listener commands, extracted from cli.mjs (D3).
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { configPath, readConfig, writeConfig } from '../lib/config.mjs';
|
|
4
|
+
import { ensureRegistry } from '../lib/registry_boot.mjs';
|
|
5
|
+
import { loadDotenvIfAny as _loadDotenvShared } from '../dotenv_min.mjs';
|
|
6
|
+
import { channelStatusList, channelSetEnabled, KNOWN_CHANNELS } from '../config_features.mjs';
|
|
7
|
+
import { assertUnattendedSafe, installCrashHandlers } from '../lib/gateway_guard.mjs';
|
|
8
|
+
import { makeInboundHandler } from '../lib/inbound_client.mjs';
|
|
9
|
+
|
|
10
|
+
// Thin .env loader wrapper kept local so the module stays self-contained.
|
|
11
|
+
export function _loadDotenvIfAny(cfgDir) { return _loadDotenvShared(cfgDir); }
|
|
12
|
+
|
|
13
|
+
// Fail closed before opening a listener socket: refuse to expose a remote
|
|
14
|
+
// inbound surface while the global unattended-sensitive tool override is on.
|
|
15
|
+
function _bootGuard(cfg, surface) {
|
|
16
|
+
try { assertUnattendedSafe(cfg, { surface }); }
|
|
17
|
+
catch (e) { console.error(e.message); process.exit(2); }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Resolve the daemon the listener forwards to. Defaults to the dashboard/
|
|
21
|
+
// daemon loopback port; override with --daemon-url or LAZYCLAW_DAEMON_URL.
|
|
22
|
+
const DEFAULT_DAEMON_URL = 'http://127.0.0.1:19600';
|
|
23
|
+
function _daemonTarget(flags) {
|
|
24
|
+
return {
|
|
25
|
+
daemonUrl: flags['daemon-url'] || process.env.LAZYCLAW_DAEMON_URL || DEFAULT_DAEMON_URL,
|
|
26
|
+
daemonToken: flags['auth-token'] || process.env.LAZYCLAW_AUTH_TOKEN || null,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function cmdSlack(sub, positional, flags = {}) {
|
|
31
|
+
if (sub !== 'listen') {
|
|
32
|
+
console.error('Usage: lazyclaw slack listen [--provider X] [--model Y]');
|
|
33
|
+
process.exit(2);
|
|
34
|
+
}
|
|
35
|
+
await ensureRegistry();
|
|
36
|
+
const cfg = readConfig();
|
|
37
|
+
_bootGuard(cfg, 'slack');
|
|
38
|
+
const cfgDir = path.dirname(configPath());
|
|
39
|
+
|
|
40
|
+
const envInfo = _loadDotenvIfAny(cfgDir);
|
|
41
|
+
process.stderr.write(`[slack] .env: ${envInfo.loaded} keys loaded from ${envInfo.path}\n`);
|
|
42
|
+
|
|
43
|
+
const { daemonUrl, daemonToken } = _daemonTarget(flags);
|
|
44
|
+
// Bridge every inbound through the daemon's session-bearing /inbound so
|
|
45
|
+
// chat + dashboard + all channels share one session/memory (single agent).
|
|
46
|
+
// Slack captures the sender id (event.user) and forwards it, so a configured
|
|
47
|
+
// pairing allowlist gates Slack the same as telegram/matrix.
|
|
48
|
+
const handler = makeInboundHandler({ channel: 'slack', daemonUrl, daemonToken, provider: flags.provider, model: flags.model });
|
|
49
|
+
|
|
50
|
+
const { SlackChannel } = await import('../channels/slack.mjs');
|
|
51
|
+
const ch = new SlackChannel();
|
|
52
|
+
process.stderr.write(`[slack] bridging to daemon ${daemonUrl}${flags.provider ? ` (provider=${flags.provider})` : ''}\n`);
|
|
53
|
+
try {
|
|
54
|
+
await ch.start(handler);
|
|
55
|
+
await ch._connectSocketMode({ logger: (line) => process.stderr.write(line) });
|
|
56
|
+
} catch (err) {
|
|
57
|
+
if (err?.code === 'SLACK_MISSING_ENV') {
|
|
58
|
+
console.error(`slack: missing env vars: ${(err.missing || []).join(', ')}`);
|
|
59
|
+
console.error(`hint: set SLACK_APP_TOKEN (xapp-…, Socket Mode) in ${path.join(cfgDir, '.env')}`);
|
|
60
|
+
} else {
|
|
61
|
+
console.error(`slack: ${err?.message || err}`);
|
|
62
|
+
}
|
|
63
|
+
process.exit(2);
|
|
64
|
+
}
|
|
65
|
+
process.stderr.write(`[slack] listening. Ctrl-C to stop.\n`);
|
|
66
|
+
installCrashHandlers({ label: 'slack', stop: () => ch.stop() });
|
|
67
|
+
|
|
68
|
+
await new Promise((resolve) => {
|
|
69
|
+
const onSig = async () => {
|
|
70
|
+
process.stderr.write(`\n[slack] shutting down…\n`);
|
|
71
|
+
try { await ch.stop(); } catch { /* best-effort */ }
|
|
72
|
+
resolve();
|
|
73
|
+
};
|
|
74
|
+
process.once('SIGINT', onSig);
|
|
75
|
+
process.once('SIGTERM', onSig);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// `lazyclaw telegram listen` — zero-install mobile control surface.
|
|
80
|
+
// Long-polls the Telegram Bot API (no public URL / webhook needed) and
|
|
81
|
+
// pipes each inbound message through the active provider, replying in
|
|
82
|
+
// the same chat. Mirrors `slack listen`. Access is gated by the existing
|
|
83
|
+
// `pairing` allowlist (Telegram numeric user ids); an empty allowlist
|
|
84
|
+
// means "reply to anyone who can reach the bot".
|
|
85
|
+
export async function cmdTelegram(sub, positional, flags = {}) {
|
|
86
|
+
if (sub !== 'listen') {
|
|
87
|
+
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>`.');
|
|
88
|
+
process.exit(2);
|
|
89
|
+
}
|
|
90
|
+
await ensureRegistry();
|
|
91
|
+
const cfg = readConfig();
|
|
92
|
+
_bootGuard(cfg, 'telegram');
|
|
93
|
+
const cfgDir = path.dirname(configPath());
|
|
94
|
+
|
|
95
|
+
const envInfo = _loadDotenvIfAny(cfgDir);
|
|
96
|
+
process.stderr.write(`[telegram] .env: ${envInfo.loaded} keys loaded from ${envInfo.path}\n`);
|
|
97
|
+
|
|
98
|
+
const { daemonUrl, daemonToken } = _daemonTarget(flags);
|
|
99
|
+
const handler = makeInboundHandler({ channel: 'telegram', daemonUrl, daemonToken, provider: flags.provider, model: flags.model });
|
|
100
|
+
|
|
101
|
+
// The pairing allowlist doubles as the Telegram sender allowlist (a
|
|
102
|
+
// channel-level gate complementary to the daemon's /inbound pairing check).
|
|
103
|
+
const allowlist = (cfg.pairing || []).map((p) => String(p.id));
|
|
104
|
+
const { TelegramChannel } = await import('../channels/telegram.mjs');
|
|
105
|
+
let ch;
|
|
106
|
+
try {
|
|
107
|
+
ch = new TelegramChannel({ allowlist: allowlist.length ? allowlist : null });
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error(`telegram: ${err?.message || err}`);
|
|
110
|
+
process.exit(2);
|
|
111
|
+
}
|
|
112
|
+
process.stderr.write(`[telegram] bridging to daemon ${daemonUrl} allowlist=${allowlist.length || 'open'}${flags.provider ? ` (provider=${flags.provider})` : ''}\n`);
|
|
113
|
+
try {
|
|
114
|
+
await ch.start(handler, { poll: true, logger: (line) => process.stderr.write(line) });
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (err?.code === 'TELEGRAM_MISSING_TOKEN') {
|
|
117
|
+
console.error('telegram: TELEGRAM_BOT_TOKEN not set');
|
|
118
|
+
console.error(`hint: add TELEGRAM_BOT_TOKEN=... to ${path.join(cfgDir, '.env')}`);
|
|
119
|
+
} else {
|
|
120
|
+
console.error(`telegram: ${err?.message || err}`);
|
|
121
|
+
}
|
|
122
|
+
process.exit(2);
|
|
123
|
+
}
|
|
124
|
+
process.stderr.write(`[telegram] listening. Ctrl-C to stop.\n`);
|
|
125
|
+
installCrashHandlers({ label: 'telegram', stop: () => ch.stop() });
|
|
126
|
+
|
|
127
|
+
await new Promise((resolve) => {
|
|
128
|
+
const onSig = async () => {
|
|
129
|
+
process.stderr.write(`\n[telegram] shutting down…\n`);
|
|
130
|
+
try { await ch.stop(); } catch { /* best-effort */ }
|
|
131
|
+
resolve();
|
|
132
|
+
};
|
|
133
|
+
process.once('SIGINT', onSig);
|
|
134
|
+
process.once('SIGTERM', onSig);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// `lazyclaw matrix listen` — Matrix inbound over the client-server API's
|
|
139
|
+
// long-poll /sync (no SDK). Mirrors `telegram listen`. Set MATRIX_HOMESERVER
|
|
140
|
+
// + MATRIX_ACCESS_TOKEN (+ MATRIX_USER_ID for self-filtering) in ~/.lazyclaw/.env.
|
|
141
|
+
export async function cmdMatrix(sub, positional, flags = {}) {
|
|
142
|
+
if (sub !== 'listen') {
|
|
143
|
+
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>`.');
|
|
144
|
+
process.exit(2);
|
|
145
|
+
}
|
|
146
|
+
await ensureRegistry();
|
|
147
|
+
const cfg = readConfig();
|
|
148
|
+
_bootGuard(cfg, 'matrix');
|
|
149
|
+
const cfgDir = path.dirname(configPath());
|
|
150
|
+
|
|
151
|
+
const envInfo = _loadDotenvIfAny(cfgDir);
|
|
152
|
+
process.stderr.write(`[matrix] .env: ${envInfo.loaded} keys loaded from ${envInfo.path}\n`);
|
|
153
|
+
|
|
154
|
+
const { daemonUrl, daemonToken } = _daemonTarget(flags);
|
|
155
|
+
const handler = makeInboundHandler({ channel: 'matrix', daemonUrl, daemonToken, provider: flags.provider, model: flags.model });
|
|
156
|
+
|
|
157
|
+
const allowlist = (cfg.pairing || []).map((p) => String(p.id));
|
|
158
|
+
const { MatrixChannel } = await import('../channels/matrix.mjs');
|
|
159
|
+
let ch;
|
|
160
|
+
try {
|
|
161
|
+
ch = new MatrixChannel({ allowlist: allowlist.length ? allowlist : null });
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error(`matrix: ${err?.message || err}`);
|
|
164
|
+
process.exit(2);
|
|
165
|
+
}
|
|
166
|
+
process.stderr.write(`[matrix] bridging to daemon ${daemonUrl} allowlist=${allowlist.length || 'open'}${flags.provider ? ` (provider=${flags.provider})` : ''}\n`);
|
|
167
|
+
try {
|
|
168
|
+
await ch.start(handler, { poll: true, logger: (line) => process.stderr.write(line) });
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (err?.code === 'MATRIX_MISSING_TOKEN' || err?.code === 'MATRIX_MISSING_HOMESERVER') {
|
|
171
|
+
console.error(`matrix: ${err.message}`);
|
|
172
|
+
console.error(`hint: set MATRIX_HOMESERVER and MATRIX_ACCESS_TOKEN in ${path.join(cfgDir, '.env')}`);
|
|
173
|
+
} else {
|
|
174
|
+
console.error(`matrix: ${err?.message || err}`);
|
|
175
|
+
}
|
|
176
|
+
process.exit(2);
|
|
177
|
+
}
|
|
178
|
+
process.stderr.write(`[matrix] listening. Ctrl-C to stop.\n`);
|
|
179
|
+
installCrashHandlers({ label: 'matrix', stop: () => ch.stop() });
|
|
180
|
+
|
|
181
|
+
await new Promise((resolve) => {
|
|
182
|
+
const onSig = async () => {
|
|
183
|
+
process.stderr.write(`\n[matrix] shutting down…\n`);
|
|
184
|
+
try { await ch.stop(); } catch { /* best-effort */ }
|
|
185
|
+
resolve();
|
|
186
|
+
};
|
|
187
|
+
process.once('SIGINT', onSig);
|
|
188
|
+
process.once('SIGTERM', onSig);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// `lazyclaw channels [list|enable <name>|disable <name>|install <pkg>|remove <pkg>]`
|
|
193
|
+
// list — configured built-in channels (cfg.channels.<name>) + installed plugins.
|
|
194
|
+
// enable/disable — toggle cfg.channels.<name>.enabled (view/edit the setting).
|
|
195
|
+
// install/remove — manage @lazyclaw/channel-* plugin packages.
|
|
196
|
+
export async function cmdChannels(sub, positional = [], flags = {}) {
|
|
197
|
+
const cfgDir = path.dirname(configPath());
|
|
198
|
+
const { createLoader, listInstalled } = await import('../channels/loader.mjs');
|
|
199
|
+
const loader = createLoader({ configDir: cfgDir });
|
|
200
|
+
|
|
201
|
+
if (sub === 'install') {
|
|
202
|
+
const name = positional[0];
|
|
203
|
+
if (!name) { process.stderr.write('usage: lazyclaw channels install <@lazyclaw/channel-name>\n'); process.exit(2); }
|
|
204
|
+
const info = await loader.install(name);
|
|
205
|
+
process.stdout.write(`installed ${info.name}@${info.version}\n`);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (sub === 'remove' || sub === 'uninstall') {
|
|
209
|
+
const name = positional[0];
|
|
210
|
+
if (!name) { process.stderr.write('usage: lazyclaw channels remove <@lazyclaw/channel-name>\n'); process.exit(2); }
|
|
211
|
+
await loader.remove(name);
|
|
212
|
+
process.stdout.write(`removed ${name}\n`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (sub === 'enable' || sub === 'disable') {
|
|
216
|
+
const name = (positional[0] || '').toLowerCase();
|
|
217
|
+
if (!name) { process.stderr.write(`usage: lazyclaw channels ${sub} <name>\n`); process.exit(2); }
|
|
218
|
+
const cfg = readConfig();
|
|
219
|
+
// Reject unknown names so a typo can't silently create a bogus
|
|
220
|
+
// cfg.channels.<name> section that then leaks into the list view.
|
|
221
|
+
// Stay permissive for pre-existing custom sections.
|
|
222
|
+
const existing = (cfg.channels && typeof cfg.channels === 'object') ? cfg.channels : {};
|
|
223
|
+
if (!KNOWN_CHANNELS.includes(name) && !(name in existing)) {
|
|
224
|
+
process.stderr.write(`unknown channel: ${name} (known: ${KNOWN_CHANNELS.join(', ')})\n`);
|
|
225
|
+
process.exit(2);
|
|
226
|
+
}
|
|
227
|
+
channelSetEnabled(cfg, name, sub === 'enable');
|
|
228
|
+
writeConfig(cfg);
|
|
229
|
+
process.stdout.write(JSON.stringify({ ok: true, channel: name, enabled: sub === 'enable' }) + '\n');
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// list (default): configured built-in channels + installed plugins.
|
|
234
|
+
const cfg = readConfig();
|
|
235
|
+
const configured = channelStatusList(cfg);
|
|
236
|
+
const plugins = listInstalled(cfgDir);
|
|
237
|
+
if (flags.json) {
|
|
238
|
+
process.stdout.write(JSON.stringify({ configured, plugins }, null, 2) + '\n');
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
process.stdout.write('configured channels:\n');
|
|
242
|
+
if (configured.length === 0) {
|
|
243
|
+
process.stdout.write(' (none — run `lazyclaw setup` or `/config` in chat to add one)\n');
|
|
244
|
+
} else {
|
|
245
|
+
for (const ch of configured) {
|
|
246
|
+
const agent = ch.boundAgent ? ` · agent: ${ch.boundAgent}` : '';
|
|
247
|
+
process.stdout.write(` ${ch.name}\t${ch.enabled ? 'enabled' : 'disabled'}${agent}\n`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
process.stdout.write(`plugins: ${plugins.length ? plugins.map((p) => `${p.name}@${p.version}`).join(', ') : '(none installed)'}\n`);
|
|
251
|
+
process.stdout.write('\ntoggle: lazyclaw channels <enable|disable> <name> · add creds: lazyclaw setup\n');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
|