nextclaw 0.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.
@@ -0,0 +1,3125 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ AgentLoop,
4
+ ConfigSchema,
5
+ LiteLLMProvider,
6
+ PROVIDERS,
7
+ SessionManager,
8
+ getApiBase,
9
+ getConfigPath,
10
+ getDataDir,
11
+ getDataPath,
12
+ getProvider,
13
+ getProviderName,
14
+ getWorkspacePath,
15
+ loadConfig,
16
+ saveConfig
17
+ } from "../chunk-RTVGGPPW.js";
18
+
19
+ // src/cli/index.ts
20
+ import { Command } from "commander";
21
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4, cpSync, rmSync } from "fs";
22
+ import { join as join5, resolve } from "path";
23
+ import { spawnSync } from "child_process";
24
+ import { createInterface } from "readline";
25
+ import { fileURLToPath } from "url";
26
+
27
+ // src/bus/queue.ts
28
+ var AsyncQueue = class {
29
+ items = [];
30
+ waiters = [];
31
+ enqueue(item) {
32
+ const waiter = this.waiters.shift();
33
+ if (waiter) {
34
+ waiter(item);
35
+ } else {
36
+ this.items.push(item);
37
+ }
38
+ }
39
+ async dequeue() {
40
+ if (this.items.length > 0) {
41
+ return this.items.shift();
42
+ }
43
+ return new Promise((resolve2) => {
44
+ this.waiters.push(resolve2);
45
+ });
46
+ }
47
+ size() {
48
+ return this.items.length;
49
+ }
50
+ };
51
+ var MessageBus = class {
52
+ inboundQueue = new AsyncQueue();
53
+ outboundQueue = new AsyncQueue();
54
+ outboundSubscribers = {};
55
+ running = false;
56
+ async publishInbound(msg) {
57
+ this.inboundQueue.enqueue(msg);
58
+ }
59
+ async consumeInbound() {
60
+ return this.inboundQueue.dequeue();
61
+ }
62
+ async publishOutbound(msg) {
63
+ this.outboundQueue.enqueue(msg);
64
+ }
65
+ async consumeOutbound() {
66
+ return this.outboundQueue.dequeue();
67
+ }
68
+ subscribeOutbound(channel, callback) {
69
+ if (!this.outboundSubscribers[channel]) {
70
+ this.outboundSubscribers[channel] = [];
71
+ }
72
+ this.outboundSubscribers[channel].push(callback);
73
+ }
74
+ async dispatchOutbound() {
75
+ this.running = true;
76
+ while (this.running) {
77
+ const msg = await this.consumeOutbound();
78
+ const subscribers = this.outboundSubscribers[msg.channel] ?? [];
79
+ for (const callback of subscribers) {
80
+ try {
81
+ await callback(msg);
82
+ } catch (err) {
83
+ console.error(`Error dispatching to ${msg.channel}: ${String(err)}`);
84
+ }
85
+ }
86
+ }
87
+ }
88
+ stop() {
89
+ this.running = false;
90
+ }
91
+ get inboundSize() {
92
+ return this.inboundQueue.size();
93
+ }
94
+ get outboundSize() {
95
+ return this.outboundQueue.size();
96
+ }
97
+ };
98
+
99
+ // src/channels/telegram.ts
100
+ import TelegramBot from "node-telegram-bot-api";
101
+
102
+ // src/channels/base.ts
103
+ var BaseChannel = class {
104
+ constructor(config, bus) {
105
+ this.config = config;
106
+ this.bus = bus;
107
+ }
108
+ running = false;
109
+ isAllowed(senderId) {
110
+ const allowList = this.config.allowFrom ?? [];
111
+ if (!allowList.length) {
112
+ return true;
113
+ }
114
+ if (allowList.includes(senderId)) {
115
+ return true;
116
+ }
117
+ if (senderId.includes("|")) {
118
+ return senderId.split("|").some((part) => allowList.includes(part));
119
+ }
120
+ return false;
121
+ }
122
+ async handleMessage(params) {
123
+ if (!this.isAllowed(params.senderId)) {
124
+ return;
125
+ }
126
+ const msg = {
127
+ channel: this.name,
128
+ senderId: params.senderId,
129
+ chatId: params.chatId,
130
+ content: params.content,
131
+ timestamp: /* @__PURE__ */ new Date(),
132
+ media: params.media ?? [],
133
+ metadata: params.metadata ?? {}
134
+ };
135
+ await this.bus.publishInbound(msg);
136
+ }
137
+ get isRunning() {
138
+ return this.running;
139
+ }
140
+ };
141
+
142
+ // src/providers/transcription.ts
143
+ import { createReadStream, existsSync } from "fs";
144
+ import { basename } from "path";
145
+ import { FormData, fetch } from "undici";
146
+ var GroqTranscriptionProvider = class {
147
+ apiKey;
148
+ apiUrl = "https://api.groq.com/openai/v1/audio/transcriptions";
149
+ constructor(apiKey) {
150
+ this.apiKey = apiKey ?? process.env.GROQ_API_KEY ?? null;
151
+ }
152
+ async transcribe(filePath) {
153
+ if (!this.apiKey) {
154
+ return "";
155
+ }
156
+ if (!existsSync(filePath)) {
157
+ return "";
158
+ }
159
+ const form = new FormData();
160
+ form.append("file", createReadStream(filePath), basename(filePath));
161
+ form.append("model", "whisper-large-v3");
162
+ const response = await fetch(this.apiUrl, {
163
+ method: "POST",
164
+ headers: {
165
+ Authorization: `Bearer ${this.apiKey}`
166
+ },
167
+ body: form
168
+ });
169
+ if (!response.ok) {
170
+ return "";
171
+ }
172
+ const data = await response.json();
173
+ return data.text ?? "";
174
+ }
175
+ };
176
+
177
+ // src/channels/telegram.ts
178
+ import { join } from "path";
179
+ import { mkdirSync } from "fs";
180
+ var BOT_COMMANDS = [
181
+ { command: "start", description: "Start the bot" },
182
+ { command: "reset", description: "Reset conversation history" },
183
+ { command: "help", description: "Show available commands" }
184
+ ];
185
+ var TelegramChannel = class extends BaseChannel {
186
+ constructor(config, bus, groqApiKey, sessionManager) {
187
+ super(config, bus);
188
+ this.sessionManager = sessionManager;
189
+ this.transcriber = new GroqTranscriptionProvider(groqApiKey ?? null);
190
+ }
191
+ name = "telegram";
192
+ bot = null;
193
+ typingTasks = /* @__PURE__ */ new Map();
194
+ transcriber;
195
+ async start() {
196
+ if (!this.config.token) {
197
+ throw new Error("Telegram bot token not configured");
198
+ }
199
+ this.running = true;
200
+ const options = { polling: true };
201
+ if (this.config.proxy) {
202
+ options.request = { proxy: this.config.proxy };
203
+ }
204
+ this.bot = new TelegramBot(this.config.token, options);
205
+ this.bot.onText(/^\/start$/, async (msg) => {
206
+ await this.bot?.sendMessage(
207
+ msg.chat.id,
208
+ `\u{1F44B} Hi ${msg.from?.first_name ?? ""}! I'm nextclaw.
209
+
210
+ Send me a message and I'll respond!
211
+ Type /help to see available commands.`
212
+ );
213
+ });
214
+ this.bot.onText(/^\/help$/, async (msg) => {
215
+ const helpText = "\u{1F916} <b>nextclaw commands</b>\n\n/start \u2014 Start the bot\n/reset \u2014 Reset conversation history\n/help \u2014 Show this help message\n\nJust send me a text message to chat!";
216
+ await this.bot?.sendMessage(msg.chat.id, helpText, { parse_mode: "HTML" });
217
+ });
218
+ this.bot.onText(/^\/reset$/, async (msg) => {
219
+ const chatId = String(msg.chat.id);
220
+ if (!this.sessionManager) {
221
+ await this.bot?.sendMessage(msg.chat.id, "\u26A0\uFE0F Session management is not available.");
222
+ return;
223
+ }
224
+ const sessionKey = `${this.name}:${chatId}`;
225
+ const session = this.sessionManager.getOrCreate(sessionKey);
226
+ const count = session.messages.length;
227
+ this.sessionManager.clear(session);
228
+ this.sessionManager.save(session);
229
+ await this.bot?.sendMessage(msg.chat.id, `\u{1F504} Conversation history cleared (${count} messages).`);
230
+ });
231
+ this.bot.on("message", async (msg) => {
232
+ if (!msg.text && !msg.caption && !msg.photo && !msg.voice && !msg.audio && !msg.document) {
233
+ return;
234
+ }
235
+ if (msg.text?.startsWith("/")) {
236
+ return;
237
+ }
238
+ await this.handleIncoming(msg);
239
+ });
240
+ await this.bot.setMyCommands(BOT_COMMANDS);
241
+ }
242
+ async stop() {
243
+ this.running = false;
244
+ for (const task of this.typingTasks.values()) {
245
+ clearInterval(task);
246
+ }
247
+ this.typingTasks.clear();
248
+ if (this.bot) {
249
+ await this.bot.stopPolling();
250
+ this.bot = null;
251
+ }
252
+ }
253
+ async send(msg) {
254
+ if (!this.bot) {
255
+ return;
256
+ }
257
+ this.stopTyping(msg.chatId);
258
+ const htmlContent = markdownToTelegramHtml(msg.content ?? "");
259
+ try {
260
+ await this.bot.sendMessage(Number(msg.chatId), htmlContent, { parse_mode: "HTML" });
261
+ } catch {
262
+ await this.bot.sendMessage(Number(msg.chatId), msg.content ?? "");
263
+ }
264
+ }
265
+ async handleIncoming(message) {
266
+ if (!this.bot || !message.from) {
267
+ return;
268
+ }
269
+ const chatId = String(message.chat.id);
270
+ let senderId = String(message.from.id);
271
+ if (message.from.username) {
272
+ senderId = `${senderId}|${message.from.username}`;
273
+ }
274
+ const contentParts = [];
275
+ const mediaPaths = [];
276
+ if (message.text) {
277
+ contentParts.push(message.text);
278
+ }
279
+ if (message.caption) {
280
+ contentParts.push(message.caption);
281
+ }
282
+ const { fileId, mediaType, mimeType } = resolveMedia(message);
283
+ if (fileId && mediaType) {
284
+ const mediaDir = join(getDataPath(), "media");
285
+ mkdirSync(mediaDir, { recursive: true });
286
+ const extension = getExtension(mediaType, mimeType);
287
+ const downloaded = await this.bot.downloadFile(fileId, mediaDir);
288
+ const finalPath = extension && !downloaded.endsWith(extension) ? `${downloaded}${extension}` : downloaded;
289
+ mediaPaths.push(finalPath);
290
+ if (mediaType === "voice" || mediaType === "audio") {
291
+ const transcription = await this.transcriber.transcribe(finalPath);
292
+ if (transcription) {
293
+ contentParts.push(`[transcription: ${transcription}]`);
294
+ } else {
295
+ contentParts.push(`[${mediaType}: ${finalPath}]`);
296
+ }
297
+ } else {
298
+ contentParts.push(`[${mediaType}: ${finalPath}]`);
299
+ }
300
+ }
301
+ const content = contentParts.length ? contentParts.join("\n") : "[empty message]";
302
+ this.startTyping(chatId);
303
+ await this.dispatchToBus(senderId, chatId, content, mediaPaths, {
304
+ message_id: message.message_id,
305
+ user_id: message.from.id,
306
+ username: message.from.username,
307
+ first_name: message.from.first_name,
308
+ is_group: message.chat.type !== "private"
309
+ });
310
+ }
311
+ async dispatchToBus(senderId, chatId, content, media, metadata) {
312
+ await this.handleMessage({ senderId, chatId, content, media, metadata });
313
+ }
314
+ startTyping(chatId) {
315
+ this.stopTyping(chatId);
316
+ if (!this.bot) {
317
+ return;
318
+ }
319
+ const task = setInterval(() => {
320
+ void this.bot?.sendChatAction(Number(chatId), "typing");
321
+ }, 4e3);
322
+ this.typingTasks.set(chatId, task);
323
+ }
324
+ stopTyping(chatId) {
325
+ const task = this.typingTasks.get(chatId);
326
+ if (task) {
327
+ clearInterval(task);
328
+ this.typingTasks.delete(chatId);
329
+ }
330
+ }
331
+ };
332
+ function resolveMedia(message) {
333
+ if (message.photo?.length) {
334
+ const photo = message.photo[message.photo.length - 1];
335
+ return { fileId: photo.file_id, mediaType: "image", mimeType: "image/jpeg" };
336
+ }
337
+ if (message.voice) {
338
+ return { fileId: message.voice.file_id, mediaType: "voice", mimeType: message.voice.mime_type };
339
+ }
340
+ if (message.audio) {
341
+ return { fileId: message.audio.file_id, mediaType: "audio", mimeType: message.audio.mime_type };
342
+ }
343
+ if (message.document) {
344
+ return { fileId: message.document.file_id, mediaType: "file", mimeType: message.document.mime_type };
345
+ }
346
+ return {};
347
+ }
348
+ function getExtension(mediaType, mimeType) {
349
+ const map = {
350
+ "image/jpeg": ".jpg",
351
+ "image/png": ".png",
352
+ "image/gif": ".gif",
353
+ "audio/ogg": ".ogg",
354
+ "audio/mpeg": ".mp3",
355
+ "audio/mp4": ".m4a"
356
+ };
357
+ if (mimeType && map[mimeType]) {
358
+ return map[mimeType];
359
+ }
360
+ const fallback = {
361
+ image: ".jpg",
362
+ voice: ".ogg",
363
+ audio: ".mp3",
364
+ file: ""
365
+ };
366
+ return fallback[mediaType] ?? "";
367
+ }
368
+ function markdownToTelegramHtml(text) {
369
+ if (!text) {
370
+ return "";
371
+ }
372
+ const codeBlocks = [];
373
+ text = text.replace(/```[\w]*\n?([\s\S]*?)```/g, (_m, code) => {
374
+ codeBlocks.push(code);
375
+ return `\0CB${codeBlocks.length - 1}\0`;
376
+ });
377
+ const inlineCodes = [];
378
+ text = text.replace(/`([^`]+)`/g, (_m, code) => {
379
+ inlineCodes.push(code);
380
+ return `\0IC${inlineCodes.length - 1}\0`;
381
+ });
382
+ text = text.replace(/^#{1,6}\s+(.+)$/gm, "$1");
383
+ text = text.replace(/^>\s*(.*)$/gm, "$1");
384
+ text = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
385
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
386
+ text = text.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
387
+ text = text.replace(/__(.+?)__/g, "<b>$1</b>");
388
+ text = text.replace(/(?<![a-zA-Z0-9])_([^_]+)_(?![a-zA-Z0-9])/g, "<i>$1</i>");
389
+ text = text.replace(/~~(.+?)~~/g, "<s>$1</s>");
390
+ text = text.replace(/^[-*]\s+/gm, "\u2022 ");
391
+ inlineCodes.forEach((code, i) => {
392
+ const escaped = code.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
393
+ text = text.replace(`\0IC${i}\0`, `<code>${escaped}</code>`);
394
+ });
395
+ codeBlocks.forEach((code, i) => {
396
+ const escaped = code.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
397
+ text = text.replace(`\0CB${i}\0`, `<pre><code>${escaped}</code></pre>`);
398
+ });
399
+ return text;
400
+ }
401
+
402
+ // src/channels/whatsapp.ts
403
+ import WebSocket from "ws";
404
+ var WhatsAppChannel = class extends BaseChannel {
405
+ name = "whatsapp";
406
+ ws = null;
407
+ connected = false;
408
+ constructor(config, bus) {
409
+ super(config, bus);
410
+ }
411
+ async start() {
412
+ this.running = true;
413
+ const bridgeUrl = this.config.bridgeUrl;
414
+ while (this.running) {
415
+ try {
416
+ await new Promise((resolve2, reject) => {
417
+ const ws = new WebSocket(bridgeUrl);
418
+ this.ws = ws;
419
+ ws.on("open", () => {
420
+ this.connected = true;
421
+ });
422
+ ws.on("message", (data) => {
423
+ const payload = data.toString();
424
+ void this.handleBridgeMessage(payload);
425
+ });
426
+ ws.on("close", () => {
427
+ this.connected = false;
428
+ this.ws = null;
429
+ resolve2();
430
+ });
431
+ ws.on("error", (_err) => {
432
+ this.connected = false;
433
+ this.ws = null;
434
+ reject(_err);
435
+ });
436
+ });
437
+ } catch {
438
+ if (!this.running) {
439
+ break;
440
+ }
441
+ await sleep(5e3);
442
+ }
443
+ }
444
+ }
445
+ async stop() {
446
+ this.running = false;
447
+ this.connected = false;
448
+ if (this.ws) {
449
+ this.ws.close();
450
+ this.ws = null;
451
+ }
452
+ }
453
+ async send(msg) {
454
+ if (!this.ws || !this.connected) {
455
+ return;
456
+ }
457
+ const payload = {
458
+ type: "send",
459
+ to: msg.chatId,
460
+ text: msg.content
461
+ };
462
+ this.ws.send(JSON.stringify(payload));
463
+ }
464
+ async handleBridgeMessage(raw) {
465
+ let data;
466
+ try {
467
+ data = JSON.parse(raw);
468
+ } catch {
469
+ return;
470
+ }
471
+ const msgType = data.type;
472
+ if (msgType === "message") {
473
+ const pn = data.pn ?? "";
474
+ const sender = data.sender ?? "";
475
+ let content = data.content ?? "";
476
+ const userId = pn || sender;
477
+ const senderId = userId.includes("@") ? userId.split("@")[0] : userId;
478
+ if (content === "[Voice Message]") {
479
+ content = "[Voice Message: Transcription not available for WhatsApp yet]";
480
+ }
481
+ await this.handleMessage({
482
+ senderId,
483
+ chatId: sender || userId,
484
+ content,
485
+ media: [],
486
+ metadata: {
487
+ message_id: data.id,
488
+ timestamp: data.timestamp,
489
+ is_group: Boolean(data.isGroup)
490
+ }
491
+ });
492
+ return;
493
+ }
494
+ if (msgType === "status") {
495
+ const status = data.status;
496
+ if (status === "connected") {
497
+ this.connected = true;
498
+ } else if (status === "disconnected") {
499
+ this.connected = false;
500
+ }
501
+ return;
502
+ }
503
+ if (msgType === "qr") {
504
+ return;
505
+ }
506
+ if (msgType === "error") {
507
+ return;
508
+ }
509
+ }
510
+ };
511
+ function sleep(ms) {
512
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
513
+ }
514
+
515
+ // src/channels/discord.ts
516
+ import {
517
+ Client,
518
+ GatewayIntentBits,
519
+ Partials
520
+ } from "discord.js";
521
+ import { fetch as fetch2 } from "undici";
522
+ import { join as join2 } from "path";
523
+ import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
524
+ var MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
525
+ var DiscordChannel = class extends BaseChannel {
526
+ name = "discord";
527
+ client = null;
528
+ typingTasks = /* @__PURE__ */ new Map();
529
+ constructor(config, bus) {
530
+ super(config, bus);
531
+ }
532
+ async start() {
533
+ if (!this.config.token) {
534
+ throw new Error("Discord token not configured");
535
+ }
536
+ this.running = true;
537
+ this.client = new Client({
538
+ intents: this.config.intents ?? GatewayIntentBits.Guilds | GatewayIntentBits.GuildMessages | GatewayIntentBits.DirectMessages,
539
+ partials: [Partials.Channel]
540
+ });
541
+ this.client.on("ready", () => {
542
+ console.log("Discord bot connected");
543
+ });
544
+ this.client.on("messageCreate", async (message) => {
545
+ await this.handleIncoming(message);
546
+ });
547
+ await this.client.login(this.config.token);
548
+ }
549
+ async stop() {
550
+ this.running = false;
551
+ for (const task of this.typingTasks.values()) {
552
+ clearInterval(task);
553
+ }
554
+ this.typingTasks.clear();
555
+ if (this.client) {
556
+ await this.client.destroy();
557
+ this.client = null;
558
+ }
559
+ }
560
+ async send(msg) {
561
+ if (!this.client) {
562
+ return;
563
+ }
564
+ const channel = await this.client.channels.fetch(msg.chatId);
565
+ if (!channel || !channel.isTextBased()) {
566
+ return;
567
+ }
568
+ this.stopTyping(msg.chatId);
569
+ const textChannel = channel;
570
+ const payload = {
571
+ content: msg.content ?? ""
572
+ };
573
+ if (msg.replyTo) {
574
+ payload.reply = { messageReference: msg.replyTo };
575
+ }
576
+ await textChannel.send(payload);
577
+ }
578
+ async handleIncoming(message) {
579
+ if (message.author.bot) {
580
+ return;
581
+ }
582
+ const senderId = message.author.id;
583
+ const channelId = message.channelId;
584
+ if (!this.isAllowed(senderId)) {
585
+ return;
586
+ }
587
+ const contentParts = [];
588
+ const mediaPaths = [];
589
+ if (message.content) {
590
+ contentParts.push(message.content);
591
+ }
592
+ if (message.attachments.size) {
593
+ const mediaDir = join2(getDataPath(), "media");
594
+ mkdirSync2(mediaDir, { recursive: true });
595
+ for (const attachment of message.attachments.values()) {
596
+ if (attachment.size && attachment.size > MAX_ATTACHMENT_BYTES) {
597
+ contentParts.push(`[attachment: ${attachment.name ?? "file"} - too large]`);
598
+ continue;
599
+ }
600
+ try {
601
+ const res = await fetch2(attachment.url);
602
+ if (!res.ok) {
603
+ contentParts.push(`[attachment: ${attachment.name ?? "file"} - download failed]`);
604
+ continue;
605
+ }
606
+ const buffer = Buffer.from(await res.arrayBuffer());
607
+ const filename = `${attachment.id}_${(attachment.name ?? "file").replace(/\//g, "_")}`;
608
+ const filePath = join2(mediaDir, filename);
609
+ writeFileSync(filePath, buffer);
610
+ mediaPaths.push(filePath);
611
+ contentParts.push(`[attachment: ${filePath}]`);
612
+ } catch {
613
+ contentParts.push(`[attachment: ${attachment.name ?? "file"} - download failed]`);
614
+ }
615
+ }
616
+ }
617
+ const replyTo = message.reference?.messageId ?? null;
618
+ this.startTyping(channelId);
619
+ await this.handleMessage({
620
+ senderId,
621
+ chatId: channelId,
622
+ content: contentParts.length ? contentParts.join("\n") : "[empty message]",
623
+ media: mediaPaths,
624
+ metadata: {
625
+ message_id: message.id,
626
+ guild_id: message.guildId,
627
+ reply_to: replyTo
628
+ }
629
+ });
630
+ }
631
+ startTyping(channelId) {
632
+ this.stopTyping(channelId);
633
+ if (!this.client) {
634
+ return;
635
+ }
636
+ const channel = this.client.channels.cache.get(channelId);
637
+ if (!channel || !channel.isTextBased()) {
638
+ return;
639
+ }
640
+ const textChannel = channel;
641
+ const task = setInterval(() => {
642
+ void textChannel.sendTyping();
643
+ }, 8e3);
644
+ this.typingTasks.set(channelId, task);
645
+ }
646
+ stopTyping(channelId) {
647
+ const task = this.typingTasks.get(channelId);
648
+ if (task) {
649
+ clearInterval(task);
650
+ this.typingTasks.delete(channelId);
651
+ }
652
+ }
653
+ };
654
+
655
+ // src/channels/feishu.ts
656
+ import * as Lark from "@larksuiteoapi/node-sdk";
657
+ var MSG_TYPE_MAP = {
658
+ image: "[image]",
659
+ audio: "[audio]",
660
+ file: "[file]",
661
+ sticker: "[sticker]"
662
+ };
663
+ var TABLE_RE = /((?:^[ \t]*\|.+\|[ \t]*\n)(?:^[ \t]*\|[-:\s|]+\|[ \t]*\n)(?:^[ \t]*\|.+\|[ \t]*\n?)+)/gm;
664
+ var FeishuChannel = class extends BaseChannel {
665
+ name = "feishu";
666
+ client = null;
667
+ wsClient = null;
668
+ processedMessageIds = [];
669
+ processedSet = /* @__PURE__ */ new Set();
670
+ constructor(config, bus) {
671
+ super(config, bus);
672
+ }
673
+ async start() {
674
+ if (!this.config.appId || !this.config.appSecret) {
675
+ throw new Error("Feishu appId/appSecret not configured");
676
+ }
677
+ this.running = true;
678
+ this.client = new Lark.Client({ appId: this.config.appId, appSecret: this.config.appSecret });
679
+ const dispatcher = new Lark.EventDispatcher({
680
+ encryptKey: this.config.encryptKey || void 0,
681
+ verificationToken: this.config.verificationToken || void 0
682
+ }).register({
683
+ "im.message.receive_v1": async (data) => {
684
+ await this.handleIncoming(data);
685
+ }
686
+ });
687
+ this.wsClient = new Lark.WSClient({
688
+ appId: this.config.appId,
689
+ appSecret: this.config.appSecret,
690
+ loggerLevel: Lark.LoggerLevel.info
691
+ });
692
+ this.wsClient.start({ eventDispatcher: dispatcher });
693
+ }
694
+ async stop() {
695
+ this.running = false;
696
+ if (this.wsClient) {
697
+ this.wsClient.close();
698
+ this.wsClient = null;
699
+ }
700
+ }
701
+ async send(msg) {
702
+ if (!this.client) {
703
+ return;
704
+ }
705
+ const receiveIdType = msg.chatId.startsWith("oc_") ? "chat_id" : "open_id";
706
+ const elements = buildCardElements(msg.content ?? "");
707
+ const card = {
708
+ config: { wide_screen_mode: true },
709
+ elements
710
+ };
711
+ const content = JSON.stringify(card);
712
+ await this.client.im.message.create({
713
+ params: { receive_id_type: receiveIdType },
714
+ data: {
715
+ receive_id: msg.chatId,
716
+ msg_type: "interactive",
717
+ content
718
+ }
719
+ });
720
+ }
721
+ async handleIncoming(data) {
722
+ const message = data.message ?? {};
723
+ const sender = message.sender ?? data.sender ?? {};
724
+ const senderIdObj = sender.sender_id ?? {};
725
+ const senderId = senderIdObj.open_id || senderIdObj.user_id || senderIdObj.union_id || sender.open_id || sender.user_id || "";
726
+ const senderType = sender.sender_type ?? sender.senderType;
727
+ if (senderType === "bot") {
728
+ return;
729
+ }
730
+ const chatId = message.chat_id ?? "";
731
+ const chatType = message.chat_type ?? "";
732
+ const msgType = message.msg_type ?? message.message_type ?? "";
733
+ const messageId = message.message_id ?? "";
734
+ if (!senderId || !chatId) {
735
+ return;
736
+ }
737
+ if (!this.isAllowed(String(senderId))) {
738
+ return;
739
+ }
740
+ if (messageId && this.isDuplicate(messageId)) {
741
+ return;
742
+ }
743
+ if (messageId) {
744
+ await this.addReaction(messageId, "THUMBSUP");
745
+ }
746
+ let content = "";
747
+ if (message.content) {
748
+ try {
749
+ const parsed = JSON.parse(String(message.content));
750
+ content = String(parsed.text ?? parsed.content ?? "");
751
+ } catch {
752
+ content = String(message.content);
753
+ }
754
+ }
755
+ if (!content && MSG_TYPE_MAP[msgType]) {
756
+ content = MSG_TYPE_MAP[msgType];
757
+ }
758
+ if (!content) {
759
+ return;
760
+ }
761
+ const replyTo = chatType === "group" ? chatId : String(senderId);
762
+ await this.handleMessage({
763
+ senderId: String(senderId),
764
+ chatId: replyTo,
765
+ content,
766
+ media: [],
767
+ metadata: {
768
+ message_id: messageId,
769
+ chat_type: chatType,
770
+ msg_type: msgType
771
+ }
772
+ });
773
+ }
774
+ isDuplicate(messageId) {
775
+ if (this.processedSet.has(messageId)) {
776
+ return true;
777
+ }
778
+ this.processedSet.add(messageId);
779
+ this.processedMessageIds.push(messageId);
780
+ if (this.processedMessageIds.length > 1e3) {
781
+ const removed = this.processedMessageIds.splice(0, 500);
782
+ for (const id of removed) {
783
+ this.processedSet.delete(id);
784
+ }
785
+ }
786
+ return false;
787
+ }
788
+ async addReaction(messageId, emojiType) {
789
+ if (!this.client) {
790
+ return;
791
+ }
792
+ try {
793
+ await this.client.im.messageReaction.create({
794
+ path: { message_id: messageId },
795
+ data: { reaction_type: { emoji_type: emojiType } }
796
+ });
797
+ } catch {
798
+ }
799
+ }
800
+ };
801
+ function buildCardElements(content) {
802
+ const elements = [];
803
+ let lastEnd = 0;
804
+ for (const match of content.matchAll(TABLE_RE)) {
805
+ const start = match.index ?? 0;
806
+ const tableText = match[1] ?? "";
807
+ const before = content.slice(lastEnd, start).trim();
808
+ if (before) {
809
+ elements.push({ tag: "markdown", content: before });
810
+ }
811
+ elements.push(parseMdTable(tableText) ?? { tag: "markdown", content: tableText });
812
+ lastEnd = start + tableText.length;
813
+ }
814
+ const remaining = content.slice(lastEnd).trim();
815
+ if (remaining) {
816
+ elements.push({ tag: "markdown", content: remaining });
817
+ }
818
+ if (!elements.length) {
819
+ elements.push({ tag: "markdown", content });
820
+ }
821
+ return elements;
822
+ }
823
+ function parseMdTable(tableText) {
824
+ const lines = tableText.trim().split("\n").map((line) => line.trim()).filter(Boolean);
825
+ if (lines.length < 3) {
826
+ return null;
827
+ }
828
+ const split = (line) => line.replace(/^\|+|\|+$/g, "").split("|").map((item) => item.trim());
829
+ const headers = split(lines[0]);
830
+ const rows = lines.slice(2).map(split);
831
+ const columns = headers.map((header, index) => ({
832
+ tag: "column",
833
+ name: `c${index}`,
834
+ display_name: header,
835
+ width: "auto"
836
+ }));
837
+ const tableRows = rows.map((row) => {
838
+ const values = {};
839
+ headers.forEach((_, index) => {
840
+ values[`c${index}`] = row[index] ?? "";
841
+ });
842
+ return values;
843
+ });
844
+ return {
845
+ tag: "table",
846
+ page_size: rows.length + 1,
847
+ columns,
848
+ rows: tableRows
849
+ };
850
+ }
851
+
852
+ // src/channels/mochat.ts
853
+ import { io } from "socket.io-client";
854
+ import { fetch as fetch3 } from "undici";
855
+ import { join as join3 } from "path";
856
+ import { mkdirSync as mkdirSync3, existsSync as existsSync2, readFileSync, writeFileSync as writeFileSync2 } from "fs";
857
+ var MAX_SEEN_MESSAGE_IDS = 2e3;
858
+ var CURSOR_SAVE_DEBOUNCE_MS = 500;
859
+ var AsyncLock = class {
860
+ queue = Promise.resolve();
861
+ async run(task) {
862
+ const run = this.queue.then(task, task);
863
+ this.queue = run.then(
864
+ () => void 0,
865
+ () => void 0
866
+ );
867
+ return run;
868
+ }
869
+ };
870
+ var MochatChannel = class extends BaseChannel {
871
+ name = "mochat";
872
+ socket = null;
873
+ wsConnected = false;
874
+ wsReady = false;
875
+ stateDir = join3(getDataPath(), "mochat");
876
+ cursorPath = join3(this.stateDir, "session_cursors.json");
877
+ sessionCursor = {};
878
+ cursorSaveTimer = null;
879
+ sessionSet = /* @__PURE__ */ new Set();
880
+ panelSet = /* @__PURE__ */ new Set();
881
+ autoDiscoverSessions = false;
882
+ autoDiscoverPanels = false;
883
+ coldSessions = /* @__PURE__ */ new Set();
884
+ sessionByConverse = /* @__PURE__ */ new Map();
885
+ seenSet = /* @__PURE__ */ new Map();
886
+ seenQueue = /* @__PURE__ */ new Map();
887
+ delayStates = /* @__PURE__ */ new Map();
888
+ fallbackMode = false;
889
+ sessionFallbackTasks = /* @__PURE__ */ new Map();
890
+ panelFallbackTasks = /* @__PURE__ */ new Map();
891
+ refreshTimer = null;
892
+ targetLocks = /* @__PURE__ */ new Map();
893
+ refreshInFlight = false;
894
+ constructor(config, bus) {
895
+ super(config, bus);
896
+ }
897
+ async start() {
898
+ this.running = true;
899
+ if (!this.config.clawToken) {
900
+ throw new Error("Mochat clawToken not configured");
901
+ }
902
+ mkdirSync3(this.stateDir, { recursive: true });
903
+ await this.loadSessionCursors();
904
+ this.seedTargetsFromConfig();
905
+ await this.refreshTargets(false);
906
+ const socketReady = await this.startSocketClient();
907
+ if (!socketReady) {
908
+ await this.ensureFallbackWorkers();
909
+ }
910
+ const intervalMs = Math.max(1e3, this.config.refreshIntervalMs);
911
+ this.refreshTimer = setInterval(() => {
912
+ void this.refreshLoopTick();
913
+ }, intervalMs);
914
+ }
915
+ async stop() {
916
+ this.running = false;
917
+ if (this.refreshTimer) {
918
+ clearInterval(this.refreshTimer);
919
+ this.refreshTimer = null;
920
+ }
921
+ await this.stopFallbackWorkers();
922
+ await this.cancelDelayTimers();
923
+ if (this.socket) {
924
+ this.socket.disconnect();
925
+ this.socket = null;
926
+ }
927
+ if (this.cursorSaveTimer) {
928
+ clearTimeout(this.cursorSaveTimer);
929
+ this.cursorSaveTimer = null;
930
+ }
931
+ await this.saveSessionCursors();
932
+ this.wsConnected = false;
933
+ this.wsReady = false;
934
+ }
935
+ async send(msg) {
936
+ if (!this.config.clawToken) {
937
+ return;
938
+ }
939
+ const parts = [];
940
+ if (msg.content && msg.content.trim()) {
941
+ parts.push(msg.content.trim());
942
+ }
943
+ if (msg.media?.length) {
944
+ for (const item of msg.media) {
945
+ if (typeof item === "string" && item.trim()) {
946
+ parts.push(item.trim());
947
+ }
948
+ }
949
+ }
950
+ const content = parts.join("\n").trim();
951
+ if (!content) {
952
+ return;
953
+ }
954
+ const target = resolveMochatTarget(msg.chatId);
955
+ if (!target.id) {
956
+ return;
957
+ }
958
+ const isPanel = (target.isPanel || this.panelSet.has(target.id)) && !target.id.startsWith("session_");
959
+ if (isPanel) {
960
+ await this.apiSend(
961
+ "/api/claw/groups/panels/send",
962
+ "panelId",
963
+ target.id,
964
+ content,
965
+ msg.replyTo,
966
+ readGroupId(msg.metadata ?? {})
967
+ );
968
+ return;
969
+ }
970
+ await this.apiSend("/api/claw/sessions/send", "sessionId", target.id, content, msg.replyTo);
971
+ }
972
+ seedTargetsFromConfig() {
973
+ const [sessions, autoSessions] = normalizeIdList(this.config.sessions);
974
+ const [panels, autoPanels] = normalizeIdList(this.config.panels);
975
+ this.autoDiscoverSessions = autoSessions;
976
+ this.autoDiscoverPanels = autoPanels;
977
+ sessions.forEach((sid) => {
978
+ this.sessionSet.add(sid);
979
+ if (!(sid in this.sessionCursor)) {
980
+ this.coldSessions.add(sid);
981
+ }
982
+ });
983
+ panels.forEach((pid) => {
984
+ this.panelSet.add(pid);
985
+ });
986
+ }
987
+ async startSocketClient() {
988
+ let parser = void 0;
989
+ if (!this.config.socketDisableMsgpack) {
990
+ try {
991
+ const mod = await import("socket.io-msgpack-parser");
992
+ parser = mod.default ?? mod;
993
+ } catch {
994
+ parser = void 0;
995
+ }
996
+ }
997
+ const socketUrl = (this.config.socketUrl || this.config.baseUrl).trim().replace(/\/$/, "");
998
+ const socketPath = (this.config.socketPath || "/socket.io").trim();
999
+ const reconnectionDelay = Math.max(100, this.config.socketReconnectDelayMs);
1000
+ const reconnectionDelayMax = Math.max(100, this.config.socketMaxReconnectDelayMs);
1001
+ const timeout = Math.max(1e3, this.config.socketConnectTimeoutMs);
1002
+ const reconnectionAttempts = this.config.maxRetryAttempts > 0 ? this.config.maxRetryAttempts : Number.MAX_SAFE_INTEGER;
1003
+ const socket = io(socketUrl, {
1004
+ path: socketPath.startsWith("/") ? socketPath : `/${socketPath}`,
1005
+ transports: ["websocket"],
1006
+ auth: { token: this.config.clawToken },
1007
+ reconnection: true,
1008
+ reconnectionAttempts,
1009
+ reconnectionDelay,
1010
+ reconnectionDelayMax,
1011
+ timeout,
1012
+ parser
1013
+ });
1014
+ socket.on("connect", async () => {
1015
+ this.wsConnected = true;
1016
+ this.wsReady = false;
1017
+ const subscribed = await this.subscribeAll();
1018
+ this.wsReady = subscribed;
1019
+ if (subscribed) {
1020
+ await this.stopFallbackWorkers();
1021
+ } else {
1022
+ await this.ensureFallbackWorkers();
1023
+ }
1024
+ });
1025
+ socket.on("disconnect", async () => {
1026
+ if (!this.running) {
1027
+ return;
1028
+ }
1029
+ this.wsConnected = false;
1030
+ this.wsReady = false;
1031
+ await this.ensureFallbackWorkers();
1032
+ });
1033
+ socket.on("connect_error", () => {
1034
+ this.wsConnected = false;
1035
+ this.wsReady = false;
1036
+ });
1037
+ socket.on("claw.session.events", async (payload) => {
1038
+ await this.handleWatchPayload(payload, "session");
1039
+ });
1040
+ socket.on("claw.panel.events", async (payload) => {
1041
+ await this.handleWatchPayload(payload, "panel");
1042
+ });
1043
+ const notifyHandler = (eventName) => async (payload) => {
1044
+ if (eventName === "notify:chat.inbox.append") {
1045
+ await this.handleNotifyInboxAppend(payload);
1046
+ return;
1047
+ }
1048
+ if (eventName.startsWith("notify:chat.message.")) {
1049
+ await this.handleNotifyChatMessage(payload);
1050
+ }
1051
+ };
1052
+ [
1053
+ "notify:chat.inbox.append",
1054
+ "notify:chat.message.add",
1055
+ "notify:chat.message.update",
1056
+ "notify:chat.message.recall",
1057
+ "notify:chat.message.delete"
1058
+ ].forEach((eventName) => {
1059
+ socket.on(eventName, notifyHandler(eventName));
1060
+ });
1061
+ this.socket = socket;
1062
+ return new Promise((resolve2) => {
1063
+ const timer = setTimeout(() => resolve2(false), timeout);
1064
+ socket.once("connect", () => {
1065
+ clearTimeout(timer);
1066
+ resolve2(true);
1067
+ });
1068
+ socket.once("connect_error", () => {
1069
+ clearTimeout(timer);
1070
+ resolve2(false);
1071
+ });
1072
+ });
1073
+ }
1074
+ async subscribeAll() {
1075
+ const sessions = Array.from(this.sessionSet).sort();
1076
+ const panels = Array.from(this.panelSet).sort();
1077
+ let ok = await this.subscribeSessions(sessions);
1078
+ ok = await this.subscribePanels(panels) && ok;
1079
+ if (this.autoDiscoverSessions || this.autoDiscoverPanels) {
1080
+ await this.refreshTargets(true);
1081
+ }
1082
+ return ok;
1083
+ }
1084
+ async subscribeSessions(sessionIds) {
1085
+ if (!sessionIds.length) {
1086
+ return true;
1087
+ }
1088
+ for (const sid of sessionIds) {
1089
+ if (!(sid in this.sessionCursor)) {
1090
+ this.coldSessions.add(sid);
1091
+ }
1092
+ }
1093
+ const ack = await this.socketCall("com.claw.im.subscribeSessions", {
1094
+ sessionIds,
1095
+ cursors: this.sessionCursor,
1096
+ limit: this.config.watchLimit
1097
+ });
1098
+ if (!ack.result) {
1099
+ return false;
1100
+ }
1101
+ const data = ack.data;
1102
+ let items = [];
1103
+ if (Array.isArray(data)) {
1104
+ items = data.filter((item) => typeof item === "object" && item !== null);
1105
+ } else if (data && typeof data === "object") {
1106
+ const sessions = data.sessions;
1107
+ if (Array.isArray(sessions)) {
1108
+ items = sessions.filter((item) => typeof item === "object" && item !== null);
1109
+ } else if (data.sessionId) {
1110
+ items = [data];
1111
+ }
1112
+ }
1113
+ for (const payload of items) {
1114
+ await this.handleWatchPayload(payload, "session");
1115
+ }
1116
+ return true;
1117
+ }
1118
+ async subscribePanels(panelIds) {
1119
+ if (!this.autoDiscoverPanels && !panelIds.length) {
1120
+ return true;
1121
+ }
1122
+ const ack = await this.socketCall("com.claw.im.subscribePanels", { panelIds });
1123
+ if (!ack.result) {
1124
+ return false;
1125
+ }
1126
+ return true;
1127
+ }
1128
+ async socketCall(eventName, payload) {
1129
+ if (!this.socket) {
1130
+ return { result: false, message: "socket not connected" };
1131
+ }
1132
+ return new Promise((resolve2) => {
1133
+ this.socket?.timeout(1e4).emit(eventName, payload, (err, response) => {
1134
+ if (err) {
1135
+ resolve2({ result: false, message: String(err) });
1136
+ return;
1137
+ }
1138
+ if (response && typeof response === "object") {
1139
+ resolve2(response);
1140
+ return;
1141
+ }
1142
+ resolve2({ result: true, data: response });
1143
+ });
1144
+ });
1145
+ }
1146
+ async refreshLoopTick() {
1147
+ if (!this.running || this.refreshInFlight) {
1148
+ return;
1149
+ }
1150
+ this.refreshInFlight = true;
1151
+ try {
1152
+ await this.refreshTargets(this.wsReady);
1153
+ if (this.fallbackMode) {
1154
+ await this.ensureFallbackWorkers();
1155
+ }
1156
+ } finally {
1157
+ this.refreshInFlight = false;
1158
+ }
1159
+ }
1160
+ async refreshTargets(subscribeNew) {
1161
+ if (this.autoDiscoverSessions) {
1162
+ await this.refreshSessionsDirectory(subscribeNew);
1163
+ }
1164
+ if (this.autoDiscoverPanels) {
1165
+ await this.refreshPanels(subscribeNew);
1166
+ }
1167
+ }
1168
+ async refreshSessionsDirectory(subscribeNew) {
1169
+ let response;
1170
+ try {
1171
+ response = await this.postJson("/api/claw/sessions/list", {});
1172
+ } catch {
1173
+ return;
1174
+ }
1175
+ const sessions = response.sessions;
1176
+ if (!Array.isArray(sessions)) {
1177
+ return;
1178
+ }
1179
+ const newIds = [];
1180
+ for (const session of sessions) {
1181
+ const sid = strField(session, "sessionId");
1182
+ if (!sid) {
1183
+ continue;
1184
+ }
1185
+ if (!this.sessionSet.has(sid)) {
1186
+ this.sessionSet.add(sid);
1187
+ newIds.push(sid);
1188
+ if (!(sid in this.sessionCursor)) {
1189
+ this.coldSessions.add(sid);
1190
+ }
1191
+ }
1192
+ const converseId = strField(session, "converseId");
1193
+ if (converseId) {
1194
+ this.sessionByConverse.set(converseId, sid);
1195
+ }
1196
+ }
1197
+ if (!newIds.length) {
1198
+ return;
1199
+ }
1200
+ if (this.wsReady && subscribeNew) {
1201
+ await this.subscribeSessions(newIds);
1202
+ }
1203
+ if (this.fallbackMode) {
1204
+ await this.ensureFallbackWorkers();
1205
+ }
1206
+ }
1207
+ async refreshPanels(subscribeNew) {
1208
+ let response;
1209
+ try {
1210
+ response = await this.postJson("/api/claw/groups/get", {});
1211
+ } catch {
1212
+ return;
1213
+ }
1214
+ const panels = response.panels;
1215
+ if (!Array.isArray(panels)) {
1216
+ return;
1217
+ }
1218
+ const newIds = [];
1219
+ for (const panel of panels) {
1220
+ const panelType = panel.type;
1221
+ if (typeof panelType === "number" && panelType !== 0) {
1222
+ continue;
1223
+ }
1224
+ const pid = strField(panel, "id", "_id");
1225
+ if (pid && !this.panelSet.has(pid)) {
1226
+ this.panelSet.add(pid);
1227
+ newIds.push(pid);
1228
+ }
1229
+ }
1230
+ if (!newIds.length) {
1231
+ return;
1232
+ }
1233
+ if (this.wsReady && subscribeNew) {
1234
+ await this.subscribePanels(newIds);
1235
+ }
1236
+ if (this.fallbackMode) {
1237
+ await this.ensureFallbackWorkers();
1238
+ }
1239
+ }
1240
+ async ensureFallbackWorkers() {
1241
+ if (!this.running) {
1242
+ return;
1243
+ }
1244
+ this.fallbackMode = true;
1245
+ for (const sid of this.sessionSet) {
1246
+ if (this.sessionFallbackTasks.has(sid)) {
1247
+ continue;
1248
+ }
1249
+ const task = this.sessionWatchWorker(sid).finally(() => {
1250
+ if (this.sessionFallbackTasks.get(sid) === task) {
1251
+ this.sessionFallbackTasks.delete(sid);
1252
+ }
1253
+ });
1254
+ this.sessionFallbackTasks.set(sid, task);
1255
+ }
1256
+ for (const pid of this.panelSet) {
1257
+ if (this.panelFallbackTasks.has(pid)) {
1258
+ continue;
1259
+ }
1260
+ const task = this.panelPollWorker(pid).finally(() => {
1261
+ if (this.panelFallbackTasks.get(pid) === task) {
1262
+ this.panelFallbackTasks.delete(pid);
1263
+ }
1264
+ });
1265
+ this.panelFallbackTasks.set(pid, task);
1266
+ }
1267
+ }
1268
+ async stopFallbackWorkers() {
1269
+ this.fallbackMode = false;
1270
+ const tasks = [...this.sessionFallbackTasks.values(), ...this.panelFallbackTasks.values()];
1271
+ this.sessionFallbackTasks.clear();
1272
+ this.panelFallbackTasks.clear();
1273
+ await Promise.allSettled(tasks);
1274
+ }
1275
+ async sessionWatchWorker(sessionId) {
1276
+ while (this.running && this.fallbackMode) {
1277
+ try {
1278
+ const payload = await this.postJson("/api/claw/sessions/watch", {
1279
+ sessionId,
1280
+ cursor: this.sessionCursor[sessionId] ?? 0,
1281
+ timeoutMs: this.config.watchTimeoutMs,
1282
+ limit: this.config.watchLimit
1283
+ });
1284
+ await this.handleWatchPayload(payload, "session");
1285
+ } catch {
1286
+ await sleep2(Math.max(100, this.config.retryDelayMs));
1287
+ }
1288
+ }
1289
+ }
1290
+ async panelPollWorker(panelId) {
1291
+ const sleepMs = Math.max(1e3, this.config.refreshIntervalMs);
1292
+ while (this.running && this.fallbackMode) {
1293
+ try {
1294
+ const payload = await this.postJson("/api/claw/groups/panels/messages", {
1295
+ panelId,
1296
+ limit: Math.min(100, Math.max(1, this.config.watchLimit))
1297
+ });
1298
+ const messages = payload.messages;
1299
+ if (Array.isArray(messages)) {
1300
+ for (const msg of [...messages].reverse()) {
1301
+ const event = makeSyntheticEvent({
1302
+ messageId: String(msg.messageId ?? ""),
1303
+ author: String(msg.author ?? ""),
1304
+ content: msg.content,
1305
+ meta: msg.meta,
1306
+ groupId: String(payload.groupId ?? ""),
1307
+ converseId: panelId,
1308
+ timestamp: msg.createdAt,
1309
+ authorInfo: msg.authorInfo
1310
+ });
1311
+ await this.processInboundEvent(panelId, event, "panel");
1312
+ }
1313
+ }
1314
+ } catch {
1315
+ await sleep2(sleepMs);
1316
+ }
1317
+ await sleep2(sleepMs);
1318
+ }
1319
+ }
1320
+ async handleWatchPayload(payload, targetKind) {
1321
+ if (!payload || typeof payload !== "object") {
1322
+ return;
1323
+ }
1324
+ const targetId = strField(payload, "sessionId");
1325
+ if (!targetId) {
1326
+ return;
1327
+ }
1328
+ const lockKey = `${targetKind}:${targetId}`;
1329
+ const lock = this.targetLocks.get(lockKey) ?? new AsyncLock();
1330
+ this.targetLocks.set(lockKey, lock);
1331
+ await lock.run(async () => {
1332
+ const previousCursor = this.sessionCursor[targetId] ?? 0;
1333
+ const cursor = payload.cursor;
1334
+ if (targetKind === "session" && typeof cursor === "number" && cursor >= 0) {
1335
+ this.markSessionCursor(targetId, cursor);
1336
+ }
1337
+ const rawEvents = payload.events;
1338
+ if (!Array.isArray(rawEvents)) {
1339
+ return;
1340
+ }
1341
+ if (targetKind === "session" && this.coldSessions.has(targetId)) {
1342
+ this.coldSessions.delete(targetId);
1343
+ return;
1344
+ }
1345
+ for (const event of rawEvents) {
1346
+ const seq = event.seq;
1347
+ if (targetKind === "session" && typeof seq === "number" && seq > (this.sessionCursor[targetId] ?? previousCursor)) {
1348
+ this.markSessionCursor(targetId, seq);
1349
+ }
1350
+ if (event.type === "message.add") {
1351
+ await this.processInboundEvent(targetId, event, targetKind);
1352
+ }
1353
+ }
1354
+ });
1355
+ }
1356
+ async processInboundEvent(targetId, event, targetKind) {
1357
+ const payload = event.payload;
1358
+ if (!payload) {
1359
+ return;
1360
+ }
1361
+ const author = strField(payload, "author");
1362
+ if (!author || this.config.agentUserId && author === this.config.agentUserId) {
1363
+ return;
1364
+ }
1365
+ if (!this.isAllowed(author)) {
1366
+ return;
1367
+ }
1368
+ const messageId = strField(payload, "messageId");
1369
+ const seenKey = `${targetKind}:${targetId}`;
1370
+ if (messageId && this.rememberMessageId(seenKey, messageId)) {
1371
+ return;
1372
+ }
1373
+ const rawBody = normalizeMochatContent(payload.content) || "[empty message]";
1374
+ const authorInfo = safeDict(payload.authorInfo);
1375
+ const senderName = strField(authorInfo, "nickname", "email");
1376
+ const senderUsername = strField(authorInfo, "agentId");
1377
+ const groupId = strField(payload, "groupId");
1378
+ const isGroup = Boolean(groupId);
1379
+ const wasMentioned = resolveWasMentioned(payload, this.config.agentUserId);
1380
+ const requireMention = targetKind === "panel" && isGroup && resolveRequireMention(this.config, targetId, groupId);
1381
+ const useDelay = targetKind === "panel" && this.config.replyDelayMode === "non-mention";
1382
+ if (requireMention && !wasMentioned && !useDelay) {
1383
+ return;
1384
+ }
1385
+ const entry = {
1386
+ rawBody,
1387
+ author,
1388
+ senderName,
1389
+ senderUsername,
1390
+ timestamp: parseTimestamp(event.timestamp),
1391
+ messageId,
1392
+ groupId
1393
+ };
1394
+ if (useDelay) {
1395
+ const delayKey = seenKey;
1396
+ if (wasMentioned) {
1397
+ await this.flushDelayedEntries(delayKey, targetId, targetKind, true, entry);
1398
+ } else {
1399
+ await this.enqueueDelayedEntry(delayKey, targetId, targetKind, entry);
1400
+ }
1401
+ return;
1402
+ }
1403
+ await this.dispatchEntries(targetId, targetKind, [entry], wasMentioned);
1404
+ }
1405
+ rememberMessageId(key, messageId) {
1406
+ const seenSet = this.seenSet.get(key) ?? /* @__PURE__ */ new Set();
1407
+ const seenQueue = this.seenQueue.get(key) ?? [];
1408
+ if (seenSet.has(messageId)) {
1409
+ return true;
1410
+ }
1411
+ seenSet.add(messageId);
1412
+ seenQueue.push(messageId);
1413
+ while (seenQueue.length > MAX_SEEN_MESSAGE_IDS) {
1414
+ const removed = seenQueue.shift();
1415
+ if (removed) {
1416
+ seenSet.delete(removed);
1417
+ }
1418
+ }
1419
+ this.seenSet.set(key, seenSet);
1420
+ this.seenQueue.set(key, seenQueue);
1421
+ return false;
1422
+ }
1423
+ async enqueueDelayedEntry(key, targetId, targetKind, entry) {
1424
+ const state = this.delayStates.get(key) ?? { entries: [], timer: null, lock: new AsyncLock() };
1425
+ this.delayStates.set(key, state);
1426
+ await state.lock.run(async () => {
1427
+ state.entries.push(entry);
1428
+ if (state.timer) {
1429
+ clearTimeout(state.timer);
1430
+ }
1431
+ state.timer = setTimeout(() => {
1432
+ void this.flushDelayedEntries(key, targetId, targetKind, false, null);
1433
+ }, Math.max(0, this.config.replyDelayMs));
1434
+ });
1435
+ }
1436
+ async flushDelayedEntries(key, targetId, targetKind, mentioned, entry) {
1437
+ const state = this.delayStates.get(key) ?? { entries: [], timer: null, lock: new AsyncLock() };
1438
+ this.delayStates.set(key, state);
1439
+ let entries = [];
1440
+ await state.lock.run(async () => {
1441
+ if (entry) {
1442
+ state.entries.push(entry);
1443
+ }
1444
+ if (state.timer) {
1445
+ clearTimeout(state.timer);
1446
+ state.timer = null;
1447
+ }
1448
+ entries = [...state.entries];
1449
+ state.entries = [];
1450
+ });
1451
+ if (entries.length) {
1452
+ await this.dispatchEntries(targetId, targetKind, entries, mentioned);
1453
+ }
1454
+ }
1455
+ async dispatchEntries(targetId, targetKind, entries, wasMentioned) {
1456
+ const last = entries[entries.length - 1];
1457
+ const isGroup = Boolean(last.groupId);
1458
+ const body = buildBufferedBody(entries, isGroup) || "[empty message]";
1459
+ await this.handleMessage({
1460
+ senderId: last.author,
1461
+ chatId: targetId,
1462
+ content: body,
1463
+ media: [],
1464
+ metadata: {
1465
+ message_id: last.messageId,
1466
+ timestamp: last.timestamp,
1467
+ is_group: isGroup,
1468
+ group_id: last.groupId,
1469
+ sender_name: last.senderName,
1470
+ sender_username: last.senderUsername,
1471
+ target_kind: targetKind,
1472
+ was_mentioned: wasMentioned,
1473
+ buffered_count: entries.length
1474
+ }
1475
+ });
1476
+ }
1477
+ async cancelDelayTimers() {
1478
+ for (const state of this.delayStates.values()) {
1479
+ if (state.timer) {
1480
+ clearTimeout(state.timer);
1481
+ }
1482
+ }
1483
+ this.delayStates.clear();
1484
+ }
1485
+ async handleNotifyChatMessage(payload) {
1486
+ if (!payload || typeof payload !== "object") {
1487
+ return;
1488
+ }
1489
+ const data = payload;
1490
+ const groupId = strField(data, "groupId");
1491
+ const panelId = strField(data, "converseId", "panelId");
1492
+ if (!groupId || !panelId) {
1493
+ return;
1494
+ }
1495
+ if (this.panelSet.size && !this.panelSet.has(panelId)) {
1496
+ return;
1497
+ }
1498
+ const event = makeSyntheticEvent({
1499
+ messageId: String(data._id ?? data.messageId ?? ""),
1500
+ author: String(data.author ?? ""),
1501
+ content: data.content,
1502
+ meta: data.meta,
1503
+ groupId,
1504
+ converseId: panelId,
1505
+ timestamp: data.createdAt,
1506
+ authorInfo: data.authorInfo
1507
+ });
1508
+ await this.processInboundEvent(panelId, event, "panel");
1509
+ }
1510
+ async handleNotifyInboxAppend(payload) {
1511
+ if (!payload || typeof payload !== "object") {
1512
+ return;
1513
+ }
1514
+ const data = payload;
1515
+ if (data.type !== "message") {
1516
+ return;
1517
+ }
1518
+ const detail = data.payload;
1519
+ if (!detail || typeof detail !== "object") {
1520
+ return;
1521
+ }
1522
+ if (strField(detail, "groupId")) {
1523
+ return;
1524
+ }
1525
+ const converseId = strField(detail, "converseId");
1526
+ if (!converseId) {
1527
+ return;
1528
+ }
1529
+ let sessionId = this.sessionByConverse.get(converseId);
1530
+ if (!sessionId) {
1531
+ await this.refreshSessionsDirectory(this.wsReady);
1532
+ sessionId = this.sessionByConverse.get(converseId);
1533
+ }
1534
+ if (!sessionId) {
1535
+ return;
1536
+ }
1537
+ const event = makeSyntheticEvent({
1538
+ messageId: String(detail.messageId ?? data._id ?? ""),
1539
+ author: String(detail.messageAuthor ?? ""),
1540
+ content: String(detail.messagePlainContent ?? detail.messageSnippet ?? ""),
1541
+ meta: { source: "notify:chat.inbox.append", converseId },
1542
+ groupId: "",
1543
+ converseId,
1544
+ timestamp: data.createdAt
1545
+ });
1546
+ await this.processInboundEvent(sessionId, event, "session");
1547
+ }
1548
+ markSessionCursor(sessionId, cursor) {
1549
+ if (cursor < 0) {
1550
+ return;
1551
+ }
1552
+ const current = this.sessionCursor[sessionId] ?? 0;
1553
+ if (cursor < current) {
1554
+ return;
1555
+ }
1556
+ this.sessionCursor[sessionId] = cursor;
1557
+ if (!this.cursorSaveTimer) {
1558
+ this.cursorSaveTimer = setTimeout(() => {
1559
+ this.cursorSaveTimer = null;
1560
+ void this.saveSessionCursors();
1561
+ }, CURSOR_SAVE_DEBOUNCE_MS);
1562
+ }
1563
+ }
1564
+ async loadSessionCursors() {
1565
+ if (!existsSync2(this.cursorPath)) {
1566
+ return;
1567
+ }
1568
+ try {
1569
+ const raw = readFileSync(this.cursorPath, "utf-8");
1570
+ const data = JSON.parse(raw);
1571
+ const cursors = data.cursors;
1572
+ if (cursors && typeof cursors === "object") {
1573
+ for (const [sid, value] of Object.entries(cursors)) {
1574
+ if (typeof value === "number" && value >= 0) {
1575
+ this.sessionCursor[sid] = value;
1576
+ }
1577
+ }
1578
+ }
1579
+ } catch {
1580
+ return;
1581
+ }
1582
+ }
1583
+ async saveSessionCursors() {
1584
+ try {
1585
+ mkdirSync3(this.stateDir, { recursive: true });
1586
+ const payload = {
1587
+ schemaVersion: 1,
1588
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1589
+ cursors: this.sessionCursor
1590
+ };
1591
+ writeFileSync2(this.cursorPath, JSON.stringify(payload, null, 2) + "\n");
1592
+ } catch {
1593
+ return;
1594
+ }
1595
+ }
1596
+ async postJson(path, payload) {
1597
+ const url = `${this.config.baseUrl.trim().replace(/\/$/, "")}${path}`;
1598
+ const response = await fetch3(url, {
1599
+ method: "POST",
1600
+ headers: {
1601
+ "content-type": "application/json",
1602
+ "X-Claw-Token": this.config.clawToken
1603
+ },
1604
+ body: JSON.stringify(payload)
1605
+ });
1606
+ if (!response.ok) {
1607
+ throw new Error(`Mochat HTTP ${response.status}`);
1608
+ }
1609
+ let parsed;
1610
+ try {
1611
+ parsed = await response.json();
1612
+ } catch {
1613
+ parsed = await response.text();
1614
+ }
1615
+ if (parsed && typeof parsed === "object" && parsed.code !== void 0) {
1616
+ const data = parsed;
1617
+ if (typeof data.code === "number" && data.code !== 200) {
1618
+ throw new Error(String(data.message ?? data.name ?? "request failed"));
1619
+ }
1620
+ if (data.data && typeof data.data === "object") {
1621
+ return data.data;
1622
+ }
1623
+ return {};
1624
+ }
1625
+ if (parsed && typeof parsed === "object") {
1626
+ return parsed;
1627
+ }
1628
+ return {};
1629
+ }
1630
+ async apiSend(path, idKey, idValue, content, replyTo, groupId) {
1631
+ const body = { [idKey]: idValue, content };
1632
+ if (replyTo) {
1633
+ body.replyTo = replyTo;
1634
+ }
1635
+ if (groupId) {
1636
+ body.groupId = groupId;
1637
+ }
1638
+ await this.postJson(path, body);
1639
+ }
1640
+ };
1641
+ function normalizeIdList(values) {
1642
+ const cleaned = values.map((value) => String(value).trim()).filter(Boolean);
1643
+ const unique = Array.from(new Set(cleaned.filter((value) => value !== "*"))).sort();
1644
+ return [unique, cleaned.includes("*")];
1645
+ }
1646
+ function safeDict(value) {
1647
+ return value && typeof value === "object" ? value : {};
1648
+ }
1649
+ function strField(src, ...keys) {
1650
+ for (const key of keys) {
1651
+ const value = src[key];
1652
+ if (typeof value === "string" && value.trim()) {
1653
+ return value.trim();
1654
+ }
1655
+ }
1656
+ return "";
1657
+ }
1658
+ function makeSyntheticEvent(params) {
1659
+ const payload = {
1660
+ messageId: params.messageId,
1661
+ author: params.author,
1662
+ content: params.content,
1663
+ meta: safeDict(params.meta),
1664
+ groupId: params.groupId,
1665
+ converseId: params.converseId
1666
+ };
1667
+ if (params.authorInfo) {
1668
+ payload.authorInfo = safeDict(params.authorInfo);
1669
+ }
1670
+ return {
1671
+ type: "message.add",
1672
+ timestamp: params.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
1673
+ payload
1674
+ };
1675
+ }
1676
+ function normalizeMochatContent(content) {
1677
+ if (typeof content === "string") {
1678
+ return content.trim();
1679
+ }
1680
+ if (content === null || content === void 0) {
1681
+ return "";
1682
+ }
1683
+ try {
1684
+ return JSON.stringify(content);
1685
+ } catch {
1686
+ return String(content);
1687
+ }
1688
+ }
1689
+ function resolveMochatTarget(raw) {
1690
+ const trimmed = (raw || "").trim();
1691
+ if (!trimmed) {
1692
+ return { id: "", isPanel: false };
1693
+ }
1694
+ const lowered = trimmed.toLowerCase();
1695
+ let cleaned = trimmed;
1696
+ let forcedPanel = false;
1697
+ for (const prefix of ["mochat:", "group:", "channel:", "panel:"]) {
1698
+ if (lowered.startsWith(prefix)) {
1699
+ cleaned = trimmed.slice(prefix.length).trim();
1700
+ forcedPanel = prefix !== "mochat:";
1701
+ break;
1702
+ }
1703
+ }
1704
+ if (!cleaned) {
1705
+ return { id: "", isPanel: false };
1706
+ }
1707
+ return { id: cleaned, isPanel: forcedPanel || !cleaned.startsWith("session_") };
1708
+ }
1709
+ function extractMentionIds(value) {
1710
+ if (!Array.isArray(value)) {
1711
+ return [];
1712
+ }
1713
+ const ids = [];
1714
+ for (const item of value) {
1715
+ if (typeof item === "string" && item.trim()) {
1716
+ ids.push(item.trim());
1717
+ } else if (item && typeof item === "object") {
1718
+ const obj = item;
1719
+ for (const key of ["id", "userId", "_id"]) {
1720
+ const candidate = obj[key];
1721
+ if (typeof candidate === "string" && candidate.trim()) {
1722
+ ids.push(candidate.trim());
1723
+ break;
1724
+ }
1725
+ }
1726
+ }
1727
+ }
1728
+ return ids;
1729
+ }
1730
+ function resolveWasMentioned(payload, agentUserId) {
1731
+ const meta = payload.meta;
1732
+ if (meta) {
1733
+ if (meta.mentioned === true || meta.wasMentioned === true) {
1734
+ return true;
1735
+ }
1736
+ for (const field of ["mentions", "mentionIds", "mentionedUserIds", "mentionedUsers"]) {
1737
+ if (agentUserId && extractMentionIds(meta[field]).includes(agentUserId)) {
1738
+ return true;
1739
+ }
1740
+ }
1741
+ }
1742
+ if (!agentUserId) {
1743
+ return false;
1744
+ }
1745
+ const content = payload.content;
1746
+ if (typeof content !== "string" || !content) {
1747
+ return false;
1748
+ }
1749
+ return content.includes(`<@${agentUserId}>`) || content.includes(`@${agentUserId}`);
1750
+ }
1751
+ function resolveRequireMention(config, sessionId, groupId) {
1752
+ const groups = config.groups ?? {};
1753
+ for (const key of [groupId, sessionId, "*"]) {
1754
+ if (key && groups[key]) {
1755
+ return Boolean(groups[key].requireMention);
1756
+ }
1757
+ }
1758
+ return Boolean(config.mention.requireInGroups);
1759
+ }
1760
+ function buildBufferedBody(entries, isGroup) {
1761
+ if (!entries.length) {
1762
+ return "";
1763
+ }
1764
+ if (entries.length === 1) {
1765
+ return entries[0].rawBody;
1766
+ }
1767
+ const lines = [];
1768
+ for (const entry of entries) {
1769
+ if (!entry.rawBody) {
1770
+ continue;
1771
+ }
1772
+ if (isGroup) {
1773
+ const label = entry.senderName.trim() || entry.senderUsername.trim() || entry.author;
1774
+ if (label) {
1775
+ lines.push(`${label}: ${entry.rawBody}`);
1776
+ continue;
1777
+ }
1778
+ }
1779
+ lines.push(entry.rawBody);
1780
+ }
1781
+ return lines.join("\n").trim();
1782
+ }
1783
+ function parseTimestamp(value) {
1784
+ if (typeof value !== "string" || !value.trim()) {
1785
+ return null;
1786
+ }
1787
+ const parsed = Date.parse(value);
1788
+ return Number.isNaN(parsed) ? null : parsed;
1789
+ }
1790
+ function readGroupId(metadata) {
1791
+ const value = metadata.group_id ?? metadata.groupId;
1792
+ if (typeof value === "string" && value.trim()) {
1793
+ return value.trim();
1794
+ }
1795
+ return null;
1796
+ }
1797
+ function sleep2(ms) {
1798
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1799
+ }
1800
+
1801
+ // src/channels/dingtalk.ts
1802
+ import { DWClient, EventAck, TOPIC_ROBOT } from "dingtalk-stream";
1803
+ import { fetch as fetch4 } from "undici";
1804
+ var DingTalkChannel = class extends BaseChannel {
1805
+ name = "dingtalk";
1806
+ client = null;
1807
+ accessToken = null;
1808
+ tokenExpiry = 0;
1809
+ constructor(config, bus) {
1810
+ super(config, bus);
1811
+ }
1812
+ async start() {
1813
+ this.running = true;
1814
+ if (!this.config.clientId || !this.config.clientSecret) {
1815
+ throw new Error("DingTalk clientId/clientSecret not configured");
1816
+ }
1817
+ this.client = new DWClient({
1818
+ clientId: this.config.clientId,
1819
+ clientSecret: this.config.clientSecret,
1820
+ debug: false
1821
+ });
1822
+ this.client.registerCallbackListener(TOPIC_ROBOT, async (res) => {
1823
+ await this.handleRobotMessage(res);
1824
+ });
1825
+ this.client.registerAllEventListener(() => ({ status: EventAck.SUCCESS }));
1826
+ await this.client.connect();
1827
+ }
1828
+ async stop() {
1829
+ this.running = false;
1830
+ if (this.client) {
1831
+ this.client.disconnect();
1832
+ this.client = null;
1833
+ }
1834
+ }
1835
+ async send(msg) {
1836
+ const token = await this.getAccessToken();
1837
+ if (!token) {
1838
+ return;
1839
+ }
1840
+ const url = "https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend";
1841
+ const payload = {
1842
+ robotCode: this.config.clientId,
1843
+ userIds: [msg.chatId],
1844
+ msgKey: "sampleMarkdown",
1845
+ msgParam: JSON.stringify({
1846
+ text: msg.content,
1847
+ title: "Nextclaw Reply"
1848
+ })
1849
+ };
1850
+ const response = await fetch4(url, {
1851
+ method: "POST",
1852
+ headers: {
1853
+ "content-type": "application/json",
1854
+ "x-acs-dingtalk-access-token": token
1855
+ },
1856
+ body: JSON.stringify(payload)
1857
+ });
1858
+ if (!response.ok) {
1859
+ throw new Error(`DingTalk send failed: ${response.status}`);
1860
+ }
1861
+ }
1862
+ async handleRobotMessage(res) {
1863
+ if (!res?.data) {
1864
+ return;
1865
+ }
1866
+ let parsed;
1867
+ try {
1868
+ parsed = JSON.parse(res.data);
1869
+ } catch {
1870
+ return;
1871
+ }
1872
+ const text = parsed.text?.content?.trim() ?? "";
1873
+ if (!text) {
1874
+ this.client?.socketCallBackResponse(res.headers.messageId, { ok: true });
1875
+ return;
1876
+ }
1877
+ const senderId = parsed.senderStaffId || parsed.senderId || "";
1878
+ const senderName = parsed.senderNick || "";
1879
+ if (!senderId) {
1880
+ this.client?.socketCallBackResponse(res.headers.messageId, { ok: true });
1881
+ return;
1882
+ }
1883
+ await this.handleMessage({
1884
+ senderId,
1885
+ chatId: senderId,
1886
+ content: text,
1887
+ media: [],
1888
+ metadata: {
1889
+ sender_name: senderName,
1890
+ platform: "dingtalk"
1891
+ }
1892
+ });
1893
+ this.client?.socketCallBackResponse(res.headers.messageId, { ok: true });
1894
+ }
1895
+ async getAccessToken() {
1896
+ if (this.accessToken && Date.now() < this.tokenExpiry) {
1897
+ return this.accessToken;
1898
+ }
1899
+ const url = "https://api.dingtalk.com/v1.0/oauth2/accessToken";
1900
+ const payload = {
1901
+ appKey: this.config.clientId,
1902
+ appSecret: this.config.clientSecret
1903
+ };
1904
+ const response = await fetch4(url, {
1905
+ method: "POST",
1906
+ headers: { "content-type": "application/json" },
1907
+ body: JSON.stringify(payload)
1908
+ });
1909
+ if (!response.ok) {
1910
+ return null;
1911
+ }
1912
+ const data = await response.json();
1913
+ const token = data.accessToken;
1914
+ const expiresIn = Number(data.expireIn ?? 7200);
1915
+ if (!token) {
1916
+ return null;
1917
+ }
1918
+ this.accessToken = token;
1919
+ this.tokenExpiry = Date.now() + (expiresIn - 60) * 1e3;
1920
+ return token;
1921
+ }
1922
+ };
1923
+
1924
+ // src/channels/email.ts
1925
+ import { ImapFlow } from "imapflow";
1926
+ import { simpleParser } from "mailparser";
1927
+ import nodemailer from "nodemailer";
1928
+ var sleep3 = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
1929
+ var EmailChannel = class extends BaseChannel {
1930
+ name = "email";
1931
+ lastSubjectByChat = /* @__PURE__ */ new Map();
1932
+ lastMessageIdByChat = /* @__PURE__ */ new Map();
1933
+ processedUids = /* @__PURE__ */ new Set();
1934
+ maxProcessedUids = 1e5;
1935
+ constructor(config, bus) {
1936
+ super(config, bus);
1937
+ }
1938
+ async start() {
1939
+ if (!this.config.consentGranted) {
1940
+ return;
1941
+ }
1942
+ if (!this.validateConfig()) {
1943
+ return;
1944
+ }
1945
+ this.running = true;
1946
+ const pollSeconds = Math.max(5, Number(this.config.pollIntervalSeconds ?? 30));
1947
+ while (this.running) {
1948
+ try {
1949
+ const items = await this.fetchNewMessages();
1950
+ for (const item of items) {
1951
+ if (item.subject) {
1952
+ this.lastSubjectByChat.set(item.sender, item.subject);
1953
+ }
1954
+ if (item.messageId) {
1955
+ this.lastMessageIdByChat.set(item.sender, item.messageId);
1956
+ }
1957
+ await this.handleMessage({
1958
+ senderId: item.sender,
1959
+ chatId: item.sender,
1960
+ content: item.content,
1961
+ media: [],
1962
+ metadata: item.metadata ?? {}
1963
+ });
1964
+ }
1965
+ } catch {
1966
+ }
1967
+ await sleep3(pollSeconds * 1e3);
1968
+ }
1969
+ }
1970
+ async stop() {
1971
+ this.running = false;
1972
+ }
1973
+ async send(msg) {
1974
+ if (!this.config.consentGranted) {
1975
+ return;
1976
+ }
1977
+ const forceSend = Boolean((msg.metadata ?? {}).force_send);
1978
+ if (!this.config.autoReplyEnabled && !forceSend) {
1979
+ return;
1980
+ }
1981
+ if (!this.config.smtpHost) {
1982
+ return;
1983
+ }
1984
+ const toAddr = msg.chatId.trim();
1985
+ if (!toAddr) {
1986
+ return;
1987
+ }
1988
+ const baseSubject = this.lastSubjectByChat.get(toAddr) ?? "nextclaw reply";
1989
+ const subject = msg.metadata?.subject?.trim() || this.replySubject(baseSubject);
1990
+ const transporter = nodemailer.createTransport({
1991
+ host: this.config.smtpHost,
1992
+ port: this.config.smtpPort,
1993
+ secure: this.config.smtpUseSsl,
1994
+ auth: {
1995
+ user: this.config.smtpUsername,
1996
+ pass: this.config.smtpPassword
1997
+ },
1998
+ tls: this.config.smtpUseTls ? { rejectUnauthorized: false } : void 0
1999
+ });
2000
+ await transporter.sendMail({
2001
+ from: this.config.fromAddress || this.config.smtpUsername || this.config.imapUsername,
2002
+ to: toAddr,
2003
+ subject,
2004
+ text: msg.content ?? "",
2005
+ inReplyTo: this.lastMessageIdByChat.get(toAddr) ?? void 0,
2006
+ references: this.lastMessageIdByChat.get(toAddr) ?? void 0
2007
+ });
2008
+ }
2009
+ validateConfig() {
2010
+ const missing = [];
2011
+ if (!this.config.imapHost) missing.push("imapHost");
2012
+ if (!this.config.imapUsername) missing.push("imapUsername");
2013
+ if (!this.config.imapPassword) missing.push("imapPassword");
2014
+ if (!this.config.smtpHost) missing.push("smtpHost");
2015
+ if (!this.config.smtpUsername) missing.push("smtpUsername");
2016
+ if (!this.config.smtpPassword) missing.push("smtpPassword");
2017
+ return missing.length === 0;
2018
+ }
2019
+ replySubject(subject) {
2020
+ const prefix = this.config.subjectPrefix || "Re: ";
2021
+ return subject.startsWith(prefix) ? subject : `${prefix}${subject}`;
2022
+ }
2023
+ async fetchNewMessages() {
2024
+ const client = new ImapFlow({
2025
+ host: this.config.imapHost,
2026
+ port: this.config.imapPort,
2027
+ secure: this.config.imapUseSsl,
2028
+ auth: {
2029
+ user: this.config.imapUsername,
2030
+ pass: this.config.imapPassword
2031
+ }
2032
+ });
2033
+ await client.connect();
2034
+ const lock = await client.getMailboxLock(this.config.imapMailbox || "INBOX");
2035
+ const items = [];
2036
+ try {
2037
+ const uids = await client.search({ seen: false });
2038
+ if (!Array.isArray(uids)) {
2039
+ return items;
2040
+ }
2041
+ for (const uid of uids) {
2042
+ const key = String(uid);
2043
+ if (this.processedUids.has(key)) {
2044
+ continue;
2045
+ }
2046
+ const message = await client.fetchOne(uid, { uid: true, source: true, envelope: true });
2047
+ if (!message || !message.source) {
2048
+ continue;
2049
+ }
2050
+ const parsed = await simpleParser(message.source);
2051
+ const sender = parsed.from?.value?.[0]?.address ?? "";
2052
+ if (!sender) {
2053
+ continue;
2054
+ }
2055
+ if (!this.isAllowed(sender)) {
2056
+ continue;
2057
+ }
2058
+ const rawContent = parsed.text ?? parsed.html ?? "";
2059
+ const content = typeof rawContent === "string" ? rawContent : "";
2060
+ const subject = parsed.subject ?? "";
2061
+ const messageId = parsed.messageId ?? "";
2062
+ items.push({
2063
+ sender,
2064
+ subject,
2065
+ content: content.slice(0, this.config.maxBodyChars),
2066
+ messageId,
2067
+ metadata: { subject }
2068
+ });
2069
+ if (this.config.markSeen) {
2070
+ await client.messageFlagsAdd(uid, ["\\Seen"]);
2071
+ }
2072
+ this.processedUids.add(key);
2073
+ if (this.processedUids.size > this.maxProcessedUids) {
2074
+ const iterator = this.processedUids.values();
2075
+ const oldest = iterator.next().value;
2076
+ if (oldest) {
2077
+ this.processedUids.delete(oldest);
2078
+ }
2079
+ }
2080
+ }
2081
+ } finally {
2082
+ lock.release();
2083
+ await client.logout();
2084
+ }
2085
+ return items;
2086
+ }
2087
+ };
2088
+
2089
+ // src/channels/slack.ts
2090
+ import { WebClient } from "@slack/web-api";
2091
+ import { SocketModeClient } from "@slack/socket-mode";
2092
+ var SlackChannel = class extends BaseChannel {
2093
+ name = "slack";
2094
+ webClient = null;
2095
+ socketClient = null;
2096
+ botUserId = null;
2097
+ constructor(config, bus) {
2098
+ super(config, bus);
2099
+ }
2100
+ async start() {
2101
+ if (!this.config.botToken || !this.config.appToken) {
2102
+ throw new Error("Slack bot/app token not configured");
2103
+ }
2104
+ if (this.config.mode !== "socket") {
2105
+ throw new Error(`Unsupported Slack mode: ${this.config.mode}`);
2106
+ }
2107
+ this.running = true;
2108
+ this.webClient = new WebClient(this.config.botToken);
2109
+ this.socketClient = new SocketModeClient({
2110
+ appToken: this.config.appToken
2111
+ });
2112
+ this.socketClient.on("events_api", async ({ body, ack }) => {
2113
+ await ack();
2114
+ await this.handleEvent(body?.event);
2115
+ });
2116
+ try {
2117
+ const auth = await this.webClient.auth.test();
2118
+ this.botUserId = auth.user_id ?? null;
2119
+ } catch {
2120
+ this.botUserId = null;
2121
+ }
2122
+ await this.socketClient.start();
2123
+ }
2124
+ async stop() {
2125
+ this.running = false;
2126
+ if (this.socketClient) {
2127
+ await this.socketClient.disconnect();
2128
+ this.socketClient = null;
2129
+ }
2130
+ }
2131
+ async send(msg) {
2132
+ if (!this.webClient) {
2133
+ return;
2134
+ }
2135
+ const slackMeta = msg.metadata?.slack ?? {};
2136
+ const threadTs = slackMeta.thread_ts;
2137
+ const channelType = slackMeta.channel_type;
2138
+ const useThread = Boolean(threadTs && channelType !== "im");
2139
+ await this.webClient.chat.postMessage({
2140
+ channel: msg.chatId,
2141
+ text: msg.content ?? "",
2142
+ thread_ts: useThread ? threadTs : void 0
2143
+ });
2144
+ }
2145
+ async handleEvent(event) {
2146
+ if (!event) {
2147
+ return;
2148
+ }
2149
+ const eventType = event.type;
2150
+ if (eventType !== "message" && eventType !== "app_mention") {
2151
+ return;
2152
+ }
2153
+ if (event.subtype) {
2154
+ return;
2155
+ }
2156
+ const senderId = event.user;
2157
+ const chatId = event.channel;
2158
+ const channelType = event.channel_type ?? "";
2159
+ const text = event.text ?? "";
2160
+ if (!senderId || !chatId) {
2161
+ return;
2162
+ }
2163
+ if (this.botUserId && senderId === this.botUserId) {
2164
+ return;
2165
+ }
2166
+ if (eventType === "message" && this.botUserId && text.includes(`<@${this.botUserId}>`)) {
2167
+ return;
2168
+ }
2169
+ if (!this.isAllowedInSlack(senderId, chatId, channelType)) {
2170
+ return;
2171
+ }
2172
+ if (channelType !== "im" && !this.shouldRespondInChannel(eventType, text, chatId)) {
2173
+ return;
2174
+ }
2175
+ const cleanText = this.stripBotMention(text);
2176
+ const threadTs = event.thread_ts ?? event.ts;
2177
+ try {
2178
+ if (this.webClient && event.ts) {
2179
+ await this.webClient.reactions.add({
2180
+ channel: chatId,
2181
+ name: "eyes",
2182
+ timestamp: event.ts
2183
+ });
2184
+ }
2185
+ } catch {
2186
+ }
2187
+ await this.handleMessage({
2188
+ senderId,
2189
+ chatId,
2190
+ content: cleanText,
2191
+ media: [],
2192
+ metadata: {
2193
+ slack: {
2194
+ event,
2195
+ thread_ts: threadTs,
2196
+ channel_type: channelType
2197
+ }
2198
+ }
2199
+ });
2200
+ }
2201
+ isAllowedInSlack(senderId, chatId, channelType) {
2202
+ if (channelType === "im") {
2203
+ if (!this.config.dm.enabled) {
2204
+ return false;
2205
+ }
2206
+ if (this.config.dm.policy === "allowlist") {
2207
+ return this.config.dm.allowFrom.includes(senderId);
2208
+ }
2209
+ return true;
2210
+ }
2211
+ if (this.config.groupPolicy === "allowlist") {
2212
+ return this.config.groupAllowFrom.includes(chatId);
2213
+ }
2214
+ return true;
2215
+ }
2216
+ shouldRespondInChannel(eventType, text, chatId) {
2217
+ if (this.config.groupPolicy === "open") {
2218
+ return true;
2219
+ }
2220
+ if (this.config.groupPolicy === "mention") {
2221
+ if (eventType === "app_mention") {
2222
+ return true;
2223
+ }
2224
+ return this.botUserId ? text.includes(`<@${this.botUserId}>`) : false;
2225
+ }
2226
+ if (this.config.groupPolicy === "allowlist") {
2227
+ return this.config.groupAllowFrom.includes(chatId);
2228
+ }
2229
+ return false;
2230
+ }
2231
+ stripBotMention(text) {
2232
+ if (!text || !this.botUserId) {
2233
+ return text;
2234
+ }
2235
+ const pattern = new RegExp(`<@${this.botUserId}>\\s*`, "g");
2236
+ return text.replace(pattern, "").trim();
2237
+ }
2238
+ };
2239
+
2240
+ // src/channels/qq.ts
2241
+ import { createOpenAPI, createWebsocket, AvailableIntentsEventsEnum } from "qq-bot-sdk";
2242
+ var QQChannel = class extends BaseChannel {
2243
+ name = "qq";
2244
+ client = null;
2245
+ ws = null;
2246
+ processedIds = [];
2247
+ processedSet = /* @__PURE__ */ new Set();
2248
+ constructor(config, bus) {
2249
+ super(config, bus);
2250
+ }
2251
+ async start() {
2252
+ this.running = true;
2253
+ if (!this.config.appId || !this.config.secret) {
2254
+ throw new Error("QQ appId/secret not configured");
2255
+ }
2256
+ const wsConfig = {
2257
+ appID: this.config.appId,
2258
+ token: this.config.secret,
2259
+ intents: [AvailableIntentsEventsEnum.GROUP_AND_C2C_EVENT]
2260
+ };
2261
+ this.client = createOpenAPI(wsConfig);
2262
+ this.ws = createWebsocket(wsConfig);
2263
+ this.ws.on(AvailableIntentsEventsEnum.GROUP_AND_C2C_EVENT, async (data) => {
2264
+ await this.handleIncoming(data);
2265
+ });
2266
+ }
2267
+ async stop() {
2268
+ this.running = false;
2269
+ if (this.ws) {
2270
+ this.ws.disconnect();
2271
+ this.ws = null;
2272
+ }
2273
+ }
2274
+ async send(msg) {
2275
+ if (!this.client) {
2276
+ return;
2277
+ }
2278
+ await this.client.c2cApi.postMessage(msg.chatId, {
2279
+ content: msg.content ?? "",
2280
+ msg_type: 0
2281
+ });
2282
+ }
2283
+ async handleIncoming(data) {
2284
+ const payload = data.msg ?? {};
2285
+ const messageId = payload.id ?? "";
2286
+ if (messageId && this.isDuplicate(messageId)) {
2287
+ return;
2288
+ }
2289
+ const author = payload.author ?? {};
2290
+ const senderId = author.user_openid || author.id || "";
2291
+ const content = payload.content?.trim() ?? "";
2292
+ if (!senderId || !content) {
2293
+ return;
2294
+ }
2295
+ if (!this.isAllowed(senderId)) {
2296
+ return;
2297
+ }
2298
+ await this.handleMessage({
2299
+ senderId,
2300
+ chatId: senderId,
2301
+ content,
2302
+ media: [],
2303
+ metadata: {
2304
+ message_id: messageId
2305
+ }
2306
+ });
2307
+ }
2308
+ isDuplicate(messageId) {
2309
+ if (this.processedSet.has(messageId)) {
2310
+ return true;
2311
+ }
2312
+ this.processedSet.add(messageId);
2313
+ this.processedIds.push(messageId);
2314
+ if (this.processedIds.length > 1e3) {
2315
+ const removed = this.processedIds.splice(0, 500);
2316
+ for (const id of removed) {
2317
+ this.processedSet.delete(id);
2318
+ }
2319
+ }
2320
+ return false;
2321
+ }
2322
+ };
2323
+
2324
+ // src/channels/manager.ts
2325
+ var ChannelManager = class {
2326
+ constructor(config, bus, sessionManager) {
2327
+ this.config = config;
2328
+ this.bus = bus;
2329
+ this.sessionManager = sessionManager;
2330
+ this.initChannels();
2331
+ }
2332
+ channels = {};
2333
+ dispatchTask = null;
2334
+ dispatching = false;
2335
+ initChannels() {
2336
+ if (this.config.channels.telegram.enabled) {
2337
+ const channel = new TelegramChannel(
2338
+ this.config.channels.telegram,
2339
+ this.bus,
2340
+ this.config.providers.groq.apiKey,
2341
+ this.sessionManager
2342
+ );
2343
+ this.channels.telegram = channel;
2344
+ }
2345
+ if (this.config.channels.whatsapp.enabled) {
2346
+ const channel = new WhatsAppChannel(this.config.channels.whatsapp, this.bus);
2347
+ this.channels.whatsapp = channel;
2348
+ }
2349
+ if (this.config.channels.discord.enabled) {
2350
+ const channel = new DiscordChannel(this.config.channels.discord, this.bus);
2351
+ this.channels.discord = channel;
2352
+ }
2353
+ if (this.config.channels.feishu.enabled) {
2354
+ const channel = new FeishuChannel(this.config.channels.feishu, this.bus);
2355
+ this.channels.feishu = channel;
2356
+ }
2357
+ if (this.config.channels.mochat.enabled) {
2358
+ const channel = new MochatChannel(this.config.channels.mochat, this.bus);
2359
+ this.channels.mochat = channel;
2360
+ }
2361
+ if (this.config.channels.dingtalk.enabled) {
2362
+ const channel = new DingTalkChannel(this.config.channels.dingtalk, this.bus);
2363
+ this.channels.dingtalk = channel;
2364
+ }
2365
+ if (this.config.channels.email.enabled) {
2366
+ const channel = new EmailChannel(this.config.channels.email, this.bus);
2367
+ this.channels.email = channel;
2368
+ }
2369
+ if (this.config.channels.slack.enabled) {
2370
+ const channel = new SlackChannel(this.config.channels.slack, this.bus);
2371
+ this.channels.slack = channel;
2372
+ }
2373
+ if (this.config.channels.qq.enabled) {
2374
+ const channel = new QQChannel(this.config.channels.qq, this.bus);
2375
+ this.channels.qq = channel;
2376
+ }
2377
+ }
2378
+ async startChannel(name, channel) {
2379
+ try {
2380
+ await channel.start();
2381
+ } catch (err) {
2382
+ console.error(`Failed to start channel ${name}: ${String(err)}`);
2383
+ }
2384
+ }
2385
+ async startAll() {
2386
+ if (!Object.keys(this.channels).length) {
2387
+ return;
2388
+ }
2389
+ this.dispatching = true;
2390
+ this.dispatchTask = this.dispatchOutbound();
2391
+ const tasks = Object.entries(this.channels).map(([name, channel]) => this.startChannel(name, channel));
2392
+ await Promise.allSettled(tasks);
2393
+ }
2394
+ async stopAll() {
2395
+ this.dispatching = false;
2396
+ if (this.dispatchTask) {
2397
+ await this.dispatchTask;
2398
+ }
2399
+ const tasks = Object.entries(this.channels).map(async ([name, channel]) => {
2400
+ try {
2401
+ await channel.stop();
2402
+ } catch (err) {
2403
+ console.error(`Error stopping ${name}: ${String(err)}`);
2404
+ }
2405
+ });
2406
+ await Promise.allSettled(tasks);
2407
+ }
2408
+ async dispatchOutbound() {
2409
+ while (this.dispatching) {
2410
+ const msg = await this.bus.consumeOutbound();
2411
+ const channel = this.channels[msg.channel];
2412
+ if (!channel) {
2413
+ continue;
2414
+ }
2415
+ try {
2416
+ await channel.send(msg);
2417
+ } catch (err) {
2418
+ console.error(`Error sending to ${msg.channel}: ${String(err)}`);
2419
+ }
2420
+ }
2421
+ }
2422
+ getChannel(name) {
2423
+ return this.channels[name];
2424
+ }
2425
+ getStatus() {
2426
+ return Object.fromEntries(
2427
+ Object.entries(this.channels).map(([name, channel]) => [name, { enabled: true, running: channel.isRunning }])
2428
+ );
2429
+ }
2430
+ get enabledChannels() {
2431
+ return Object.keys(this.channels);
2432
+ }
2433
+ };
2434
+
2435
+ // src/cron/service.ts
2436
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync4 } from "fs";
2437
+ import { dirname } from "path";
2438
+ import { randomUUID } from "crypto";
2439
+ import cronParser from "cron-parser";
2440
+ var nowMs = () => Date.now();
2441
+ function computeNextRun(schedule, now) {
2442
+ if (schedule.kind === "at") {
2443
+ return schedule.atMs && schedule.atMs > now ? schedule.atMs : null;
2444
+ }
2445
+ if (schedule.kind === "every") {
2446
+ if (!schedule.everyMs || schedule.everyMs <= 0) {
2447
+ return null;
2448
+ }
2449
+ return now + schedule.everyMs;
2450
+ }
2451
+ if (schedule.kind === "cron" && schedule.expr) {
2452
+ try {
2453
+ const interval = cronParser.parseExpression(schedule.expr, { currentDate: new Date(now) });
2454
+ return interval.next().getTime();
2455
+ } catch {
2456
+ return null;
2457
+ }
2458
+ }
2459
+ return null;
2460
+ }
2461
+ var CronService = class {
2462
+ constructor(storePath, onJob) {
2463
+ this.storePath = storePath;
2464
+ this.onJob = onJob;
2465
+ }
2466
+ store = null;
2467
+ timer = null;
2468
+ running = false;
2469
+ onJob;
2470
+ loadStore() {
2471
+ if (this.store) {
2472
+ return this.store;
2473
+ }
2474
+ if (existsSync3(this.storePath)) {
2475
+ try {
2476
+ const data = JSON.parse(readFileSync2(this.storePath, "utf-8"));
2477
+ const jobs = (data.jobs ?? []).map((job) => ({
2478
+ id: String(job.id),
2479
+ name: String(job.name),
2480
+ enabled: Boolean(job.enabled ?? true),
2481
+ schedule: job.schedule ?? {},
2482
+ payload: job.payload ?? {},
2483
+ state: job.state ?? {},
2484
+ createdAtMs: Number(job.createdAtMs ?? 0),
2485
+ updatedAtMs: Number(job.updatedAtMs ?? 0),
2486
+ deleteAfterRun: Boolean(job.deleteAfterRun ?? false)
2487
+ }));
2488
+ this.store = { version: data.version ?? 1, jobs };
2489
+ } catch {
2490
+ this.store = { version: 1, jobs: [] };
2491
+ }
2492
+ } else {
2493
+ this.store = { version: 1, jobs: [] };
2494
+ }
2495
+ return this.store;
2496
+ }
2497
+ saveStore() {
2498
+ if (!this.store) {
2499
+ return;
2500
+ }
2501
+ mkdirSync4(dirname(this.storePath), { recursive: true });
2502
+ writeFileSync3(this.storePath, JSON.stringify(this.store, null, 2));
2503
+ }
2504
+ async start() {
2505
+ this.running = true;
2506
+ this.loadStore();
2507
+ this.recomputeNextRuns();
2508
+ this.saveStore();
2509
+ this.armTimer();
2510
+ }
2511
+ stop() {
2512
+ this.running = false;
2513
+ if (this.timer) {
2514
+ clearTimeout(this.timer);
2515
+ this.timer = null;
2516
+ }
2517
+ }
2518
+ recomputeNextRuns() {
2519
+ if (!this.store) {
2520
+ return;
2521
+ }
2522
+ const now = nowMs();
2523
+ for (const job of this.store.jobs) {
2524
+ if (job.enabled) {
2525
+ job.state.nextRunAtMs = computeNextRun(job.schedule, now);
2526
+ }
2527
+ }
2528
+ }
2529
+ getNextWakeMs() {
2530
+ if (!this.store) {
2531
+ return null;
2532
+ }
2533
+ const times = this.store.jobs.filter((job) => job.enabled && job.state.nextRunAtMs).map((job) => job.state.nextRunAtMs);
2534
+ if (!times.length) {
2535
+ return null;
2536
+ }
2537
+ return Math.min(...times);
2538
+ }
2539
+ armTimer() {
2540
+ if (this.timer) {
2541
+ clearTimeout(this.timer);
2542
+ }
2543
+ if (!this.running) {
2544
+ return;
2545
+ }
2546
+ const nextWake = this.getNextWakeMs();
2547
+ if (!nextWake) {
2548
+ return;
2549
+ }
2550
+ const delayMs = Math.max(0, nextWake - nowMs());
2551
+ this.timer = setTimeout(() => {
2552
+ void this.onTimer();
2553
+ }, delayMs);
2554
+ }
2555
+ async onTimer() {
2556
+ if (!this.store) {
2557
+ return;
2558
+ }
2559
+ const now = nowMs();
2560
+ const dueJobs = this.store.jobs.filter(
2561
+ (job) => job.enabled && job.state.nextRunAtMs && now >= (job.state.nextRunAtMs ?? 0)
2562
+ );
2563
+ for (const job of dueJobs) {
2564
+ await this.executeJob(job);
2565
+ }
2566
+ this.saveStore();
2567
+ this.armTimer();
2568
+ }
2569
+ async executeJob(job) {
2570
+ const start = nowMs();
2571
+ try {
2572
+ if (this.onJob) {
2573
+ await this.onJob(job);
2574
+ }
2575
+ job.state.lastStatus = "ok";
2576
+ job.state.lastError = null;
2577
+ } catch (err) {
2578
+ job.state.lastStatus = "error";
2579
+ job.state.lastError = String(err);
2580
+ }
2581
+ job.state.lastRunAtMs = start;
2582
+ job.updatedAtMs = nowMs();
2583
+ if (job.schedule.kind === "at") {
2584
+ if (job.deleteAfterRun) {
2585
+ if (this.store) {
2586
+ this.store.jobs = this.store.jobs.filter((existing) => existing.id !== job.id);
2587
+ }
2588
+ } else {
2589
+ job.enabled = false;
2590
+ job.state.nextRunAtMs = null;
2591
+ }
2592
+ } else {
2593
+ job.state.nextRunAtMs = computeNextRun(job.schedule, nowMs());
2594
+ }
2595
+ }
2596
+ listJobs(includeDisabled = false) {
2597
+ const store = this.loadStore();
2598
+ const jobs = includeDisabled ? store.jobs : store.jobs.filter((job) => job.enabled);
2599
+ return jobs.sort((a, b) => (a.state.nextRunAtMs ?? Infinity) - (b.state.nextRunAtMs ?? Infinity));
2600
+ }
2601
+ addJob(params) {
2602
+ const store = this.loadStore();
2603
+ const now = nowMs();
2604
+ const job = {
2605
+ id: randomUUID().slice(0, 8),
2606
+ name: params.name,
2607
+ enabled: true,
2608
+ schedule: params.schedule,
2609
+ payload: {
2610
+ kind: "agent_turn",
2611
+ message: params.message,
2612
+ deliver: params.deliver ?? false,
2613
+ channel: params.channel,
2614
+ to: params.to
2615
+ },
2616
+ state: {
2617
+ nextRunAtMs: computeNextRun(params.schedule, now)
2618
+ },
2619
+ createdAtMs: now,
2620
+ updatedAtMs: now,
2621
+ deleteAfterRun: params.deleteAfterRun ?? false
2622
+ };
2623
+ store.jobs.push(job);
2624
+ this.saveStore();
2625
+ this.armTimer();
2626
+ return job;
2627
+ }
2628
+ removeJob(jobId) {
2629
+ const store = this.loadStore();
2630
+ const before = store.jobs.length;
2631
+ store.jobs = store.jobs.filter((job) => job.id !== jobId);
2632
+ const removed = store.jobs.length < before;
2633
+ if (removed) {
2634
+ this.saveStore();
2635
+ this.armTimer();
2636
+ }
2637
+ return removed;
2638
+ }
2639
+ enableJob(jobId, enabled = true) {
2640
+ const store = this.loadStore();
2641
+ for (const job of store.jobs) {
2642
+ if (job.id === jobId) {
2643
+ job.enabled = enabled;
2644
+ job.updatedAtMs = nowMs();
2645
+ job.state.nextRunAtMs = enabled ? computeNextRun(job.schedule, nowMs()) : null;
2646
+ this.saveStore();
2647
+ this.armTimer();
2648
+ return job;
2649
+ }
2650
+ }
2651
+ return null;
2652
+ }
2653
+ async runJob(jobId, force = false) {
2654
+ const store = this.loadStore();
2655
+ for (const job of store.jobs) {
2656
+ if (job.id === jobId) {
2657
+ if (!force && !job.enabled) {
2658
+ return false;
2659
+ }
2660
+ await this.executeJob(job);
2661
+ this.saveStore();
2662
+ this.armTimer();
2663
+ return true;
2664
+ }
2665
+ }
2666
+ return false;
2667
+ }
2668
+ status() {
2669
+ const store = this.loadStore();
2670
+ return {
2671
+ enabled: this.running,
2672
+ jobs: store.jobs.length,
2673
+ nextWakeAtMs: this.getNextWakeMs()
2674
+ };
2675
+ }
2676
+ };
2677
+
2678
+ // src/heartbeat/service.ts
2679
+ import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
2680
+ import { join as join4 } from "path";
2681
+ var DEFAULT_HEARTBEAT_INTERVAL_S = 30 * 60;
2682
+ var HEARTBEAT_PROMPT = "Read HEARTBEAT.md in your workspace (if it exists).\nFollow any instructions or tasks listed there.\nIf nothing needs attention, reply with just: HEARTBEAT_OK";
2683
+ var HEARTBEAT_OK_TOKEN = "HEARTBEAT_OK";
2684
+ function isHeartbeatEmpty(content) {
2685
+ if (!content) {
2686
+ return true;
2687
+ }
2688
+ const skipPatterns = /* @__PURE__ */ new Set(["- [ ]", "* [ ]", "- [x]", "* [x]"]);
2689
+ for (const line of content.split("\n")) {
2690
+ const trimmed = line.trim();
2691
+ if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("<!--") || skipPatterns.has(trimmed)) {
2692
+ continue;
2693
+ }
2694
+ return false;
2695
+ }
2696
+ return true;
2697
+ }
2698
+ var HeartbeatService = class {
2699
+ constructor(workspace, onHeartbeat, intervalS = DEFAULT_HEARTBEAT_INTERVAL_S, enabled = true) {
2700
+ this.workspace = workspace;
2701
+ this.onHeartbeat = onHeartbeat;
2702
+ this.intervalS = intervalS;
2703
+ this.enabled = enabled;
2704
+ }
2705
+ running = false;
2706
+ timer = null;
2707
+ get heartbeatFile() {
2708
+ return join4(this.workspace, "HEARTBEAT.md");
2709
+ }
2710
+ readHeartbeatFile() {
2711
+ if (existsSync4(this.heartbeatFile)) {
2712
+ try {
2713
+ return readFileSync3(this.heartbeatFile, "utf-8");
2714
+ } catch {
2715
+ return null;
2716
+ }
2717
+ }
2718
+ return null;
2719
+ }
2720
+ async start() {
2721
+ if (!this.enabled) {
2722
+ return;
2723
+ }
2724
+ this.running = true;
2725
+ this.timer = setInterval(() => {
2726
+ void this.tick();
2727
+ }, this.intervalS * 1e3);
2728
+ }
2729
+ stop() {
2730
+ this.running = false;
2731
+ if (this.timer) {
2732
+ clearInterval(this.timer);
2733
+ this.timer = null;
2734
+ }
2735
+ }
2736
+ async tick() {
2737
+ if (!this.running) {
2738
+ return;
2739
+ }
2740
+ const content = this.readHeartbeatFile();
2741
+ if (isHeartbeatEmpty(content)) {
2742
+ return;
2743
+ }
2744
+ if (this.onHeartbeat) {
2745
+ const response = await this.onHeartbeat(HEARTBEAT_PROMPT);
2746
+ if (response.toUpperCase().replace(/_/g, "").includes(HEARTBEAT_OK_TOKEN.replace(/_/g, ""))) {
2747
+ return;
2748
+ }
2749
+ }
2750
+ }
2751
+ async triggerNow() {
2752
+ if (!this.onHeartbeat) {
2753
+ return null;
2754
+ }
2755
+ return this.onHeartbeat(HEARTBEAT_PROMPT);
2756
+ }
2757
+ };
2758
+
2759
+ // src/cli/index.ts
2760
+ var LOGO = "\u{1F916}";
2761
+ var EXIT_COMMANDS = /* @__PURE__ */ new Set(["exit", "quit", "/exit", "/quit", ":q"]);
2762
+ var VERSION = getPackageVersion();
2763
+ var program = new Command();
2764
+ program.name("nextclaw").description(`${LOGO} nextclaw - Personal AI Assistant`).version(VERSION, "-v, --version", "show version");
2765
+ program.command("onboard").description("Initialize nextclaw configuration and workspace").action(() => {
2766
+ const configPath = getConfigPath();
2767
+ if (existsSync5(configPath)) {
2768
+ console.log(`Config already exists at ${configPath}`);
2769
+ }
2770
+ const config = ConfigSchema.parse({});
2771
+ saveConfig(config);
2772
+ console.log(`\u2713 Created config at ${configPath}`);
2773
+ const workspace = getWorkspacePath();
2774
+ console.log(`\u2713 Created workspace at ${workspace}`);
2775
+ createWorkspaceTemplates(workspace);
2776
+ console.log(`
2777
+ ${LOGO} nextclaw is ready!`);
2778
+ console.log("\nNext steps:");
2779
+ console.log(` 1. Add your API key to ${configPath}`);
2780
+ console.log(' 2. Chat: nextclaw agent -m "Hello!"');
2781
+ });
2782
+ program.command("gateway").description("Start the nextclaw gateway").option("-p, --port <port>", "Gateway port", "18790").option("-v, --verbose", "Verbose output", false).action(async (_opts) => {
2783
+ const config = loadConfig();
2784
+ const bus = new MessageBus();
2785
+ const provider = makeProvider(config);
2786
+ const sessionManager = new SessionManager(getWorkspacePath(config.agents.defaults.workspace));
2787
+ const cronStorePath = join5(getDataDir(), "cron", "jobs.json");
2788
+ const cron2 = new CronService(cronStorePath);
2789
+ const agent = new AgentLoop({
2790
+ bus,
2791
+ provider,
2792
+ workspace: getWorkspacePath(config.agents.defaults.workspace),
2793
+ model: config.agents.defaults.model,
2794
+ maxIterations: config.agents.defaults.maxToolIterations,
2795
+ braveApiKey: config.tools.web.search.apiKey || void 0,
2796
+ execConfig: config.tools.exec,
2797
+ cronService: cron2,
2798
+ restrictToWorkspace: config.tools.restrictToWorkspace,
2799
+ sessionManager
2800
+ });
2801
+ cron2.onJob = async (job) => {
2802
+ const response = await agent.processDirect({
2803
+ content: job.payload.message,
2804
+ sessionKey: `cron:${job.id}`,
2805
+ channel: job.payload.channel ?? "cli",
2806
+ chatId: job.payload.to ?? "direct"
2807
+ });
2808
+ if (job.payload.deliver && job.payload.to) {
2809
+ await bus.publishOutbound({
2810
+ channel: job.payload.channel ?? "cli",
2811
+ chatId: job.payload.to,
2812
+ content: response,
2813
+ media: [],
2814
+ metadata: {}
2815
+ });
2816
+ }
2817
+ return response;
2818
+ };
2819
+ const heartbeat = new HeartbeatService(
2820
+ getWorkspacePath(config.agents.defaults.workspace),
2821
+ async (prompt2) => agent.processDirect({ content: prompt2, sessionKey: "heartbeat" }),
2822
+ 30 * 60,
2823
+ true
2824
+ );
2825
+ const channels2 = new ChannelManager(config, bus, sessionManager);
2826
+ if (channels2.enabledChannels.length) {
2827
+ console.log(`\u2713 Channels enabled: ${channels2.enabledChannels.join(", ")}`);
2828
+ } else {
2829
+ console.log("Warning: No channels enabled");
2830
+ }
2831
+ const cronStatus = cron2.status();
2832
+ if (cronStatus.jobs > 0) {
2833
+ console.log(`\u2713 Cron: ${cronStatus.jobs} scheduled jobs`);
2834
+ }
2835
+ console.log("\u2713 Heartbeat: every 30m");
2836
+ await cron2.start();
2837
+ await heartbeat.start();
2838
+ await Promise.allSettled([agent.run(), channels2.startAll()]);
2839
+ });
2840
+ program.command("agent").description("Interact with the agent directly").option("-m, --message <message>", "Message to send to the agent").option("-s, --session <session>", "Session ID", "cli:default").option("--no-markdown", "Disable Markdown rendering").action(async (opts) => {
2841
+ const config = loadConfig();
2842
+ const bus = new MessageBus();
2843
+ const provider = makeProvider(config);
2844
+ const agentLoop = new AgentLoop({
2845
+ bus,
2846
+ provider,
2847
+ workspace: getWorkspacePath(config.agents.defaults.workspace),
2848
+ braveApiKey: config.tools.web.search.apiKey || void 0,
2849
+ execConfig: config.tools.exec,
2850
+ restrictToWorkspace: config.tools.restrictToWorkspace
2851
+ });
2852
+ if (opts.message) {
2853
+ const response = await agentLoop.processDirect({
2854
+ content: opts.message,
2855
+ sessionKey: opts.session,
2856
+ channel: "cli",
2857
+ chatId: "direct"
2858
+ });
2859
+ printAgentResponse(response);
2860
+ return;
2861
+ }
2862
+ console.log(`${LOGO} Interactive mode (type exit or Ctrl+C to quit)
2863
+ `);
2864
+ const historyFile = join5(getDataDir(), "history", "cli_history");
2865
+ const historyDir = resolve(historyFile, "..");
2866
+ mkdirSync5(historyDir, { recursive: true });
2867
+ const history = existsSync5(historyFile) ? readFileSync4(historyFile, "utf-8").split("\n").filter(Boolean) : [];
2868
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2869
+ rl.on("close", () => {
2870
+ const merged = history.concat(rl.history ?? []);
2871
+ writeFileSync4(historyFile, merged.join("\n"));
2872
+ process.exit(0);
2873
+ });
2874
+ let running = true;
2875
+ while (running) {
2876
+ const line = await prompt(rl, "You: ");
2877
+ const trimmed = line.trim();
2878
+ if (!trimmed) {
2879
+ continue;
2880
+ }
2881
+ if (EXIT_COMMANDS.has(trimmed.toLowerCase())) {
2882
+ rl.close();
2883
+ running = false;
2884
+ break;
2885
+ }
2886
+ const response = await agentLoop.processDirect({ content: trimmed, sessionKey: opts.session });
2887
+ printAgentResponse(response);
2888
+ }
2889
+ });
2890
+ var channels = program.command("channels").description("Manage channels");
2891
+ channels.command("status").description("Show channel status").action(() => {
2892
+ const config = loadConfig();
2893
+ console.log("Channel Status");
2894
+ console.log(`WhatsApp: ${config.channels.whatsapp.enabled ? "\u2713" : "\u2717"}`);
2895
+ console.log(`Discord: ${config.channels.discord.enabled ? "\u2713" : "\u2717"}`);
2896
+ console.log(`Feishu: ${config.channels.feishu.enabled ? "\u2713" : "\u2717"}`);
2897
+ console.log(`Mochat: ${config.channels.mochat.enabled ? "\u2713" : "\u2717"}`);
2898
+ console.log(`Telegram: ${config.channels.telegram.enabled ? "\u2713" : "\u2717"}`);
2899
+ console.log(`Slack: ${config.channels.slack.enabled ? "\u2713" : "\u2717"}`);
2900
+ console.log(`QQ: ${config.channels.qq.enabled ? "\u2713" : "\u2717"}`);
2901
+ });
2902
+ channels.command("login").description("Link device via QR code").action(() => {
2903
+ const bridgeDir = getBridgeDir();
2904
+ console.log(`${LOGO} Starting bridge...`);
2905
+ console.log("Scan the QR code to connect.\n");
2906
+ const result = spawnSync("npm", ["start"], { cwd: bridgeDir, stdio: "inherit" });
2907
+ if (result.status !== 0) {
2908
+ console.error(`Bridge failed: ${result.status ?? 1}`);
2909
+ }
2910
+ });
2911
+ var cron = program.command("cron").description("Manage scheduled tasks");
2912
+ cron.command("list").option("-a, --all", "Include disabled jobs").action((opts) => {
2913
+ const storePath = join5(getDataDir(), "cron", "jobs.json");
2914
+ const service = new CronService(storePath);
2915
+ const jobs = service.listJobs(Boolean(opts.all));
2916
+ if (!jobs.length) {
2917
+ console.log("No scheduled jobs.");
2918
+ return;
2919
+ }
2920
+ for (const job of jobs) {
2921
+ let schedule = "";
2922
+ if (job.schedule.kind === "every") {
2923
+ schedule = `every ${Math.round((job.schedule.everyMs ?? 0) / 1e3)}s`;
2924
+ } else if (job.schedule.kind === "cron") {
2925
+ schedule = job.schedule.expr ?? "";
2926
+ } else {
2927
+ schedule = job.schedule.atMs ? new Date(job.schedule.atMs).toISOString() : "";
2928
+ }
2929
+ console.log(`${job.id} ${job.name} ${schedule}`);
2930
+ }
2931
+ });
2932
+ cron.command("add").requiredOption("-n, --name <name>", "Job name").requiredOption("-m, --message <message>", "Message for agent").option("-e, --every <seconds>", "Run every N seconds").option("-c, --cron <expr>", "Cron expression").option("--at <iso>", "Run once at time (ISO format)").option("-d, --deliver", "Deliver response to channel").option("--to <recipient>", "Recipient for delivery").option("--channel <channel>", "Channel for delivery").action((opts) => {
2933
+ const storePath = join5(getDataDir(), "cron", "jobs.json");
2934
+ const service = new CronService(storePath);
2935
+ let schedule = null;
2936
+ if (opts.every) {
2937
+ schedule = { kind: "every", everyMs: Number(opts.every) * 1e3 };
2938
+ } else if (opts.cron) {
2939
+ schedule = { kind: "cron", expr: String(opts.cron) };
2940
+ } else if (opts.at) {
2941
+ schedule = { kind: "at", atMs: Date.parse(String(opts.at)) };
2942
+ }
2943
+ if (!schedule) {
2944
+ console.error("Error: Must specify --every, --cron, or --at");
2945
+ return;
2946
+ }
2947
+ const job = service.addJob({
2948
+ name: opts.name,
2949
+ schedule,
2950
+ message: opts.message,
2951
+ deliver: Boolean(opts.deliver),
2952
+ channel: opts.channel,
2953
+ to: opts.to
2954
+ });
2955
+ console.log(`\u2713 Added job '${job.name}' (${job.id})`);
2956
+ });
2957
+ cron.command("remove <jobId>").action((jobId) => {
2958
+ const storePath = join5(getDataDir(), "cron", "jobs.json");
2959
+ const service = new CronService(storePath);
2960
+ if (service.removeJob(jobId)) {
2961
+ console.log(`\u2713 Removed job ${jobId}`);
2962
+ } else {
2963
+ console.log(`Job ${jobId} not found`);
2964
+ }
2965
+ });
2966
+ cron.command("enable <jobId>").option("--disable", "Disable instead of enable").action((jobId, opts) => {
2967
+ const storePath = join5(getDataDir(), "cron", "jobs.json");
2968
+ const service = new CronService(storePath);
2969
+ const job = service.enableJob(jobId, !opts.disable);
2970
+ if (job) {
2971
+ console.log(`\u2713 Job '${job.name}' ${opts.disable ? "disabled" : "enabled"}`);
2972
+ } else {
2973
+ console.log(`Job ${jobId} not found`);
2974
+ }
2975
+ });
2976
+ cron.command("run <jobId>").option("-f, --force", "Run even if disabled").action(async (jobId, opts) => {
2977
+ const storePath = join5(getDataDir(), "cron", "jobs.json");
2978
+ const service = new CronService(storePath);
2979
+ const ok = await service.runJob(jobId, Boolean(opts.force));
2980
+ console.log(ok ? "\u2713 Job executed" : `Failed to run job ${jobId}`);
2981
+ });
2982
+ program.command("status").description("Show nextclaw status").action(() => {
2983
+ const configPath = getConfigPath();
2984
+ const config = loadConfig();
2985
+ const workspace = getWorkspacePath(config.agents.defaults.workspace);
2986
+ console.log(`${LOGO} nextclaw Status
2987
+ `);
2988
+ console.log(`Config: ${configPath} ${existsSync5(configPath) ? "\u2713" : "\u2717"}`);
2989
+ console.log(`Workspace: ${workspace} ${existsSync5(workspace) ? "\u2713" : "\u2717"}`);
2990
+ console.log(`Model: ${config.agents.defaults.model}`);
2991
+ for (const spec of PROVIDERS) {
2992
+ const provider = config.providers[spec.name];
2993
+ if (!provider) {
2994
+ continue;
2995
+ }
2996
+ if (spec.isLocal) {
2997
+ console.log(`${spec.displayName ?? spec.name}: ${provider.apiBase ? `\u2713 ${provider.apiBase}` : "not set"}`);
2998
+ } else {
2999
+ console.log(`${spec.displayName ?? spec.name}: ${provider.apiKey ? "\u2713" : "not set"}`);
3000
+ }
3001
+ }
3002
+ });
3003
+ program.parseAsync(process.argv);
3004
+ function makeProvider(config) {
3005
+ const provider = getProvider(config);
3006
+ const model = config.agents.defaults.model;
3007
+ if (!provider?.apiKey && !model.startsWith("bedrock/")) {
3008
+ console.error("Error: No API key configured.");
3009
+ console.error("Set one in ~/.nextclaw/config.json under providers section");
3010
+ process.exit(1);
3011
+ }
3012
+ return new LiteLLMProvider({
3013
+ apiKey: provider?.apiKey ?? null,
3014
+ apiBase: getApiBase(config),
3015
+ defaultModel: model,
3016
+ extraHeaders: provider?.extraHeaders ?? null,
3017
+ providerName: getProviderName(config)
3018
+ });
3019
+ }
3020
+ function createWorkspaceTemplates(workspace) {
3021
+ const templates = {
3022
+ "AGENTS.md": "# Agent Instructions\n\nYou are a helpful AI assistant. Be concise, accurate, and friendly.\n\n## Guidelines\n\n- Always explain what you're doing before taking actions\n- Ask for clarification when the request is ambiguous\n- Use tools to help accomplish tasks\n- Remember important information in your memory files\n",
3023
+ "SOUL.md": "# Soul\n\nI am nextclaw, a lightweight AI assistant.\n\n## Personality\n\n- Helpful and friendly\n- Concise and to the point\n- Curious and eager to learn\n\n## Values\n\n- Accuracy over speed\n- User privacy and safety\n- Transparency in actions\n",
3024
+ "USER.md": "# User\n\nInformation about the user goes here.\n\n## Preferences\n\n- Communication style: (casual/formal)\n- Timezone: (your timezone)\n- Language: (your preferred language)\n"
3025
+ };
3026
+ for (const [filename, content] of Object.entries(templates)) {
3027
+ const filePath = join5(workspace, filename);
3028
+ if (!existsSync5(filePath)) {
3029
+ writeFileSync4(filePath, content);
3030
+ }
3031
+ }
3032
+ const memoryDir = join5(workspace, "memory");
3033
+ mkdirSync5(memoryDir, { recursive: true });
3034
+ const memoryFile = join5(memoryDir, "MEMORY.md");
3035
+ if (!existsSync5(memoryFile)) {
3036
+ writeFileSync4(
3037
+ memoryFile,
3038
+ "# Long-term Memory\n\nThis file stores important information that should persist across sessions.\n\n## User Information\n\n(Important facts about the user)\n\n## Preferences\n\n(User preferences learned over time)\n\n## Important Notes\n\n(Things to remember)\n"
3039
+ );
3040
+ }
3041
+ const skillsDir = join5(workspace, "skills");
3042
+ mkdirSync5(skillsDir, { recursive: true });
3043
+ }
3044
+ function printAgentResponse(response) {
3045
+ console.log("\n" + response + "\n");
3046
+ }
3047
+ async function prompt(rl, question) {
3048
+ rl.setPrompt(question);
3049
+ rl.prompt();
3050
+ return new Promise((resolve2) => {
3051
+ rl.once("line", (line) => resolve2(line));
3052
+ });
3053
+ }
3054
+ function getBridgeDir() {
3055
+ const userBridge = join5(getDataDir(), "bridge");
3056
+ if (existsSync5(join5(userBridge, "dist", "index.js"))) {
3057
+ return userBridge;
3058
+ }
3059
+ if (!which("npm")) {
3060
+ console.error("npm not found. Please install Node.js >= 18.");
3061
+ process.exit(1);
3062
+ }
3063
+ const cliDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
3064
+ const pkgRoot = resolve(cliDir, "..", "..");
3065
+ const pkgBridge = join5(pkgRoot, "bridge");
3066
+ const srcBridge = join5(pkgRoot, "..", "..", "bridge");
3067
+ let source = null;
3068
+ if (existsSync5(join5(pkgBridge, "package.json"))) {
3069
+ source = pkgBridge;
3070
+ } else if (existsSync5(join5(srcBridge, "package.json"))) {
3071
+ source = srcBridge;
3072
+ }
3073
+ if (!source) {
3074
+ console.error("Bridge source not found. Try reinstalling nextclaw.");
3075
+ process.exit(1);
3076
+ }
3077
+ console.log(`${LOGO} Setting up bridge...`);
3078
+ mkdirSync5(resolve(userBridge, ".."), { recursive: true });
3079
+ if (existsSync5(userBridge)) {
3080
+ rmSync(userBridge, { recursive: true, force: true });
3081
+ }
3082
+ cpSync(source, userBridge, {
3083
+ recursive: true,
3084
+ filter: (src) => !src.includes("node_modules") && !src.includes("dist")
3085
+ });
3086
+ const install = spawnSync("npm", ["install"], { cwd: userBridge, stdio: "pipe" });
3087
+ if (install.status !== 0) {
3088
+ console.error(`Bridge install failed: ${install.status ?? 1}`);
3089
+ if (install.stderr) {
3090
+ console.error(String(install.stderr).slice(0, 500));
3091
+ }
3092
+ process.exit(1);
3093
+ }
3094
+ const build = spawnSync("npm", ["run", "build"], { cwd: userBridge, stdio: "pipe" });
3095
+ if (build.status !== 0) {
3096
+ console.error(`Bridge build failed: ${build.status ?? 1}`);
3097
+ if (build.stderr) {
3098
+ console.error(String(build.stderr).slice(0, 500));
3099
+ }
3100
+ process.exit(1);
3101
+ }
3102
+ console.log("\u2713 Bridge ready\n");
3103
+ return userBridge;
3104
+ }
3105
+ function getPackageVersion() {
3106
+ try {
3107
+ const cliDir = resolve(fileURLToPath(new URL(".", import.meta.url)));
3108
+ const pkgPath = resolve(cliDir, "..", "..", "package.json");
3109
+ const raw = readFileSync4(pkgPath, "utf-8");
3110
+ const parsed = JSON.parse(raw);
3111
+ return typeof parsed.version === "string" ? parsed.version : "0.0.0";
3112
+ } catch {
3113
+ return "0.0.0";
3114
+ }
3115
+ }
3116
+ function which(binary) {
3117
+ const paths = (process.env.PATH ?? "").split(":");
3118
+ for (const dir of paths) {
3119
+ const full = join5(dir, binary);
3120
+ if (existsSync5(full)) {
3121
+ return true;
3122
+ }
3123
+ }
3124
+ return false;
3125
+ }