daemora 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -76
- package/SOUL.md +100 -28
- package/config/mcp.json +9 -9
- package/package.json +15 -8
- package/skills/apple-notes.md +0 -52
- package/skills/apple-reminders.md +1 -87
- package/skills/camsnap.md +20 -144
- package/skills/coding.md +7 -7
- package/skills/documents.md +6 -6
- package/skills/email.md +6 -6
- package/skills/gif-search.md +28 -171
- package/skills/healthcheck.md +21 -203
- package/skills/image-gen.md +24 -123
- package/skills/model-usage.md +18 -165
- package/skills/obsidian.md +28 -174
- package/skills/pdf.md +30 -181
- package/skills/research.md +6 -6
- package/skills/skill-creator.md +35 -111
- package/skills/spotify.md +2 -17
- package/skills/summarize.md +36 -193
- package/skills/things.md +23 -175
- package/skills/tmux.md +1 -91
- package/skills/trello.md +32 -157
- package/skills/video-frames.md +26 -166
- package/skills/weather.md +6 -6
- package/src/a2a/A2AClient.js +2 -2
- package/src/a2a/A2AServer.js +6 -6
- package/src/a2a/AgentCard.js +2 -2
- package/src/agents/SubAgentManager.js +61 -19
- package/src/agents/Supervisor.js +4 -4
- package/src/channels/BaseChannel.js +6 -6
- package/src/channels/BlueBubblesChannel.js +112 -0
- package/src/channels/DiscordChannel.js +8 -8
- package/src/channels/EmailChannel.js +54 -26
- package/src/channels/FeishuChannel.js +140 -0
- package/src/channels/GoogleChatChannel.js +8 -8
- package/src/channels/HttpChannel.js +2 -2
- package/src/channels/IRCChannel.js +144 -0
- package/src/channels/LineChannel.js +13 -13
- package/src/channels/MatrixChannel.js +97 -0
- package/src/channels/MattermostChannel.js +119 -0
- package/src/channels/NextcloudChannel.js +133 -0
- package/src/channels/NostrChannel.js +175 -0
- package/src/channels/SignalChannel.js +9 -9
- package/src/channels/SlackChannel.js +10 -10
- package/src/channels/TeamsChannel.js +10 -10
- package/src/channels/TelegramChannel.js +8 -8
- package/src/channels/TwitchChannel.js +128 -0
- package/src/channels/WhatsAppChannel.js +10 -10
- package/src/channels/ZaloChannel.js +119 -0
- package/src/channels/iMessageChannel.js +150 -0
- package/src/channels/index.js +241 -11
- package/src/cli.js +835 -38
- package/src/config/agentProfiles.js +19 -19
- package/src/config/channels.js +1 -1
- package/src/config/default.js +12 -7
- package/src/config/models.js +3 -3
- package/src/config/permissions.js +2 -2
- package/src/core/AgentLoop.js +13 -13
- package/src/core/Compaction.js +3 -3
- package/src/core/CostTracker.js +2 -2
- package/src/core/EventBus.js +15 -15
- package/src/core/TaskQueue.js +24 -7
- package/src/core/TaskRunner.js +19 -6
- package/src/daemon/DaemonManager.js +4 -4
- package/src/hooks/HookRunner.js +4 -4
- package/src/index.js +6 -2
- package/src/mcp/MCPAgentRunner.js +3 -3
- package/src/mcp/MCPClient.js +9 -9
- package/src/mcp/MCPManager.js +14 -14
- package/src/models/ModelRouter.js +2 -2
- package/src/safety/AuditLog.js +3 -3
- package/src/safety/CircuitBreaker.js +2 -2
- package/src/safety/CommandGuard.js +132 -0
- package/src/safety/FilesystemGuard.js +23 -3
- package/src/safety/GitRollback.js +5 -5
- package/src/safety/HumanApproval.js +9 -9
- package/src/safety/InputSanitizer.js +81 -8
- package/src/safety/PermissionGuard.js +2 -2
- package/src/safety/Sandbox.js +1 -1
- package/src/safety/SecretScanner.js +90 -28
- package/src/safety/SecretVault.js +2 -2
- package/src/scheduler/Heartbeat.js +3 -3
- package/src/scheduler/Scheduler.js +6 -6
- package/src/setup/theme.js +171 -66
- package/src/setup/wizard.js +432 -57
- package/src/skills/SkillLoader.js +145 -8
- package/src/storage/TaskStore.js +39 -15
- package/src/systemPrompt.js +45 -43
- package/src/tenants/TenantManager.js +79 -22
- package/src/tools/ToolRegistry.js +3 -3
- package/src/tools/applyPatch.js +2 -2
- package/src/tools/browserAutomation.js +4 -4
- package/src/tools/calendar.js +155 -0
- package/src/tools/clipboard.js +71 -0
- package/src/tools/contacts.js +138 -0
- package/src/tools/createDocument.js +2 -2
- package/src/tools/cronTool.js +14 -14
- package/src/tools/database.js +165 -0
- package/src/tools/editFile.js +10 -10
- package/src/tools/executeCommand.js +11 -3
- package/src/tools/generateImage.js +79 -0
- package/src/tools/gitTool.js +141 -0
- package/src/tools/glob.js +1 -1
- package/src/tools/googlePlaces.js +136 -0
- package/src/tools/grep.js +2 -2
- package/src/tools/iMessageTool.js +86 -0
- package/src/tools/imageAnalysis.js +3 -3
- package/src/tools/index.js +56 -2
- package/src/tools/makeVoiceCall.js +283 -0
- package/src/tools/manageAgents.js +2 -2
- package/src/tools/manageMCP.js +38 -20
- package/src/tools/memory.js +25 -32
- package/src/tools/messageChannel.js +1 -1
- package/src/tools/notification.js +90 -0
- package/src/tools/philipsHue.js +147 -0
- package/src/tools/projectTracker.js +8 -8
- package/src/tools/readFile.js +1 -1
- package/src/tools/readPDF.js +73 -0
- package/src/tools/screenCapture.js +6 -6
- package/src/tools/searchContent.js +2 -2
- package/src/tools/searchFiles.js +1 -1
- package/src/tools/sendEmail.js +79 -24
- package/src/tools/sendFile.js +4 -4
- package/src/tools/sonos.js +137 -0
- package/src/tools/sshTool.js +130 -0
- package/src/tools/textToSpeech.js +5 -5
- package/src/tools/transcribeAudio.js +4 -4
- package/src/tools/useMCP.js +4 -4
- package/src/tools/webFetch.js +2 -2
- package/src/tools/webSearch.js +1 -1
- package/src/utils/Embeddings.js +79 -0
- package/src/voice/VoiceSessionManager.js +170 -0
- package/src/voice/VoiceWebhook.js +188 -0
|
@@ -7,9 +7,9 @@ import { join, extname, basename } from "node:path";
|
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Slack Channel
|
|
10
|
+
* Slack Channel - receives messages via Slack Bolt + Socket Mode.
|
|
11
11
|
*
|
|
12
|
-
* Socket Mode means NO public webhook URL needed
|
|
12
|
+
* Socket Mode means NO public webhook URL needed - works on any machine.
|
|
13
13
|
*
|
|
14
14
|
* Setup:
|
|
15
15
|
* 1. Go to https://api.slack.com/apps → Create New App → From Scratch
|
|
@@ -25,10 +25,10 @@ import { tmpdir } from "node:os";
|
|
|
25
25
|
* 5. Set env: SLACK_BOT_TOKEN, SLACK_APP_TOKEN
|
|
26
26
|
*
|
|
27
27
|
* Config:
|
|
28
|
-
* botToken
|
|
29
|
-
* appToken
|
|
30
|
-
* allowlist
|
|
31
|
-
* model
|
|
28
|
+
* botToken - xoxb-... token
|
|
29
|
+
* appToken - xapp-... token for Socket Mode
|
|
30
|
+
* allowlist - Optional array of Slack user IDs (Uxxxxxxxx) allowed to use the bot
|
|
31
|
+
* model - Optional model override
|
|
32
32
|
*
|
|
33
33
|
* The bot responds to:
|
|
34
34
|
* - Direct messages (message.im)
|
|
@@ -43,7 +43,7 @@ export class SlackChannel extends BaseChannel {
|
|
|
43
43
|
|
|
44
44
|
async start() {
|
|
45
45
|
if (!this.config.botToken || !this.config.appToken) {
|
|
46
|
-
console.log(`[Channel:Slack] Skipped
|
|
46
|
+
console.log(`[Channel:Slack] Skipped - need SLACK_BOT_TOKEN and SLACK_APP_TOKEN`);
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -119,7 +119,7 @@ export class SlackChannel extends BaseChannel {
|
|
|
119
119
|
this.running = true;
|
|
120
120
|
console.log(`[Channel:Slack] Started (Socket Mode)`);
|
|
121
121
|
if (this.config.allowlist?.length) {
|
|
122
|
-
console.log(`[Channel:Slack] Allowlist active
|
|
122
|
+
console.log(`[Channel:Slack] Allowlist active - ${this.config.allowlist.length} authorized user(s)`);
|
|
123
123
|
}
|
|
124
124
|
} catch (err) {
|
|
125
125
|
console.log(`[Channel:Slack] Failed to start: ${err.message}`);
|
|
@@ -147,7 +147,7 @@ export class SlackChannel extends BaseChannel {
|
|
|
147
147
|
|
|
148
148
|
const mimeType = file.mimetype || "";
|
|
149
149
|
if (mimeType.startsWith("audio/")) {
|
|
150
|
-
console.log(`[Channel:Slack] Audio file
|
|
150
|
+
console.log(`[Channel:Slack] Audio file - transcribing...`);
|
|
151
151
|
const transcript = await transcribeAudio(localPath);
|
|
152
152
|
inputParts.push(transcript.startsWith("Error:")
|
|
153
153
|
? `[Audio file: ${localPath}]\n${transcript}`
|
|
@@ -174,7 +174,7 @@ export class SlackChannel extends BaseChannel {
|
|
|
174
174
|
try {
|
|
175
175
|
const completedTask = await taskQueue.waitForCompletion(task.id);
|
|
176
176
|
|
|
177
|
-
// Absorbed into a concurrent session
|
|
177
|
+
// Absorbed into a concurrent session - response already sent via original task
|
|
178
178
|
if (this.isTaskMerged(completedTask)) {
|
|
179
179
|
await this._removeReaction(channelId, messageTs, "hourglass_flowing_sand");
|
|
180
180
|
await this._addReaction(channelId, messageTs, "white_check_mark");
|
|
@@ -6,7 +6,7 @@ import { join, extname } from "node:path";
|
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Microsoft Teams Channel
|
|
9
|
+
* Microsoft Teams Channel - receives messages via Bot Framework v4 (CloudAdapter).
|
|
10
10
|
*
|
|
11
11
|
* Setup:
|
|
12
12
|
* 1. Go to https://portal.azure.com → Create a resource → Azure Bot
|
|
@@ -16,10 +16,10 @@ import { tmpdir } from "node:os";
|
|
|
16
16
|
* 5. In the bot resource → Channels → Add Microsoft Teams
|
|
17
17
|
*
|
|
18
18
|
* Config:
|
|
19
|
-
* appId
|
|
20
|
-
* appPassword
|
|
21
|
-
* allowlist
|
|
22
|
-
* model
|
|
19
|
+
* appId - Microsoft App ID from Azure Bot registration
|
|
20
|
+
* appPassword - Client secret from Azure AD app registration
|
|
21
|
+
* allowlist - Optional array of Teams user IDs / AAD object IDs
|
|
22
|
+
* model - Optional model override
|
|
23
23
|
*
|
|
24
24
|
* Unlike OpenClaw's 461-line channel with Adaptive Cards and Graph API,
|
|
25
25
|
* this is minimal: text messages + file attachments + proactive reply.
|
|
@@ -36,7 +36,7 @@ export class TeamsChannel extends BaseChannel {
|
|
|
36
36
|
|
|
37
37
|
async start() {
|
|
38
38
|
if (!this.config.appId || !this.config.appPassword) {
|
|
39
|
-
console.log(`[Channel:Teams] Skipped
|
|
39
|
+
console.log(`[Channel:Teams] Skipped - set TEAMS_APP_ID and TEAMS_APP_PASSWORD`);
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -63,7 +63,7 @@ export class TeamsChannel extends BaseChannel {
|
|
|
63
63
|
this.running = true;
|
|
64
64
|
console.log(`[Channel:Teams] Ready (webhook: POST /webhooks/teams)`);
|
|
65
65
|
if (this.config.allowlist?.length) {
|
|
66
|
-
console.log(`[Channel:Teams] Allowlist active
|
|
66
|
+
console.log(`[Channel:Teams] Allowlist active - ${this.config.allowlist.length} authorized user(s)`);
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -148,10 +148,10 @@ export class TeamsChannel extends BaseChannel {
|
|
|
148
148
|
const ref = this._TurnContext.getConversationReference(context.activity);
|
|
149
149
|
this._conversationRefs.set(userId, ref);
|
|
150
150
|
|
|
151
|
-
// Ack with typing indicator
|
|
151
|
+
// Ack with typing indicator - keeps Teams from showing "delivery failed"
|
|
152
152
|
await context.sendActivity({ type: "typing" });
|
|
153
153
|
|
|
154
|
-
// Enqueue task and reply proactively (don't await
|
|
154
|
+
// Enqueue task and reply proactively (don't await - we must return within 5s)
|
|
155
155
|
const task = taskQueue.enqueue({
|
|
156
156
|
input,
|
|
157
157
|
channel: "teams",
|
|
@@ -209,7 +209,7 @@ export class TeamsChannel extends BaseChannel {
|
|
|
209
209
|
try {
|
|
210
210
|
// Teams file sending requires SharePoint upload for larger files.
|
|
211
211
|
// For simplicity: send the file path as text with caption.
|
|
212
|
-
// Full file upload requires Graph API + bot permissions
|
|
212
|
+
// Full file upload requires Graph API + bot permissions - out of scope here.
|
|
213
213
|
const msg = caption
|
|
214
214
|
? `${caption}\n(File: ${filePath})`
|
|
215
215
|
: `File: ${filePath}`;
|
|
@@ -6,7 +6,7 @@ import { join, extname } from "node:path";
|
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Telegram Channel
|
|
9
|
+
* Telegram Channel - receives messages via Telegram Bot API (grammy).
|
|
10
10
|
*
|
|
11
11
|
* Handles:
|
|
12
12
|
* ✅ Text messages
|
|
@@ -23,9 +23,9 @@ import { tmpdir } from "node:os";
|
|
|
23
23
|
* ✅ Documents (any other file)
|
|
24
24
|
*
|
|
25
25
|
* Config:
|
|
26
|
-
* token
|
|
27
|
-
* allowlist
|
|
28
|
-
* model
|
|
26
|
+
* token - Bot token from @BotFather
|
|
27
|
+
* allowlist - Optional array of chat IDs allowed to send tasks. Empty = open.
|
|
28
|
+
* model - Optional model override
|
|
29
29
|
*/
|
|
30
30
|
export class TelegramChannel extends BaseChannel {
|
|
31
31
|
constructor(config) {
|
|
@@ -38,7 +38,7 @@ export class TelegramChannel extends BaseChannel {
|
|
|
38
38
|
this._InputFile = InputFile;
|
|
39
39
|
|
|
40
40
|
if (!this.config.token) {
|
|
41
|
-
console.log(`[Channel:Telegram] Skipped
|
|
41
|
+
console.log(`[Channel:Telegram] Skipped - no TELEGRAM_BOT_TOKEN`);
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -78,7 +78,7 @@ export class TelegramChannel extends BaseChannel {
|
|
|
78
78
|
return;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
console.log(`[Channel:Telegram] Voice from ${userName} (${chatId})
|
|
81
|
+
console.log(`[Channel:Telegram] Voice from ${userName} (${chatId}) - transcribing...`);
|
|
82
82
|
await ctx.replyWithChatAction("typing");
|
|
83
83
|
await this.sendReaction({ chatId, messageId: ctx.message.message_id }, "⏳");
|
|
84
84
|
|
|
@@ -198,7 +198,7 @@ export class TelegramChannel extends BaseChannel {
|
|
|
198
198
|
this.running = true;
|
|
199
199
|
console.log(`[Channel:Telegram] Started as @${me.username}`);
|
|
200
200
|
if (this.config.allowlist?.length) {
|
|
201
|
-
console.log(`[Channel:Telegram] Allowlist active
|
|
201
|
+
console.log(`[Channel:Telegram] Allowlist active - ${this.config.allowlist.length} authorized chat(s)`);
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
|
|
@@ -218,7 +218,7 @@ export class TelegramChannel extends BaseChannel {
|
|
|
218
218
|
try {
|
|
219
219
|
const completedTask = await taskQueue.waitForCompletion(task.id);
|
|
220
220
|
|
|
221
|
-
// Task was absorbed into a concurrent agent session
|
|
221
|
+
// Task was absorbed into a concurrent agent session - response already sent
|
|
222
222
|
if (this.isTaskMerged(completedTask)) {
|
|
223
223
|
await this.sendReaction({ chatId, messageId }, "✅");
|
|
224
224
|
return;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { BaseChannel } from "./BaseChannel.js";
|
|
2
|
+
import taskQueue from "../core/TaskQueue.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Twitch Channel - receives chat commands via Twitch IRC (TMI).
|
|
6
|
+
*
|
|
7
|
+
* Setup:
|
|
8
|
+
* 1. Create a Twitch app at https://dev.twitch.tv/console
|
|
9
|
+
* 2. Generate an OAuth token for the bot account (chat:read, chat:edit scope)
|
|
10
|
+
* Use: https://twitchapps.com/tmi/
|
|
11
|
+
* 3. Set env: TWITCH_BOT_USERNAME, TWITCH_OAUTH_TOKEN, TWITCH_CHANNEL
|
|
12
|
+
*
|
|
13
|
+
* The bot responds to "!ask <question>" or "@botname <question>" in chat.
|
|
14
|
+
*
|
|
15
|
+
* Config:
|
|
16
|
+
* username - Bot Twitch username
|
|
17
|
+
* token - OAuth token (without "oauth:" prefix)
|
|
18
|
+
* channel - Twitch channel to join (without #)
|
|
19
|
+
* prefix - Command prefix, default "!ask"
|
|
20
|
+
* allowlist - Optional array of Twitch usernames (mods, subscribers, etc.)
|
|
21
|
+
* model - Optional model override
|
|
22
|
+
*/
|
|
23
|
+
export class TwitchChannel extends BaseChannel {
|
|
24
|
+
constructor(config) {
|
|
25
|
+
super("twitch", config);
|
|
26
|
+
this.ws = null;
|
|
27
|
+
this.channel = (config.channel || "").toLowerCase();
|
|
28
|
+
this.prefix = config.prefix || "!ask";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async start() {
|
|
32
|
+
if (!this.config.username || !this.config.token || !this.channel) {
|
|
33
|
+
console.log("[Channel:Twitch] Skipped - missing TWITCH_BOT_USERNAME, TWITCH_OAUTH_TOKEN, or TWITCH_CHANNEL");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const { WebSocket } = await import("ws");
|
|
39
|
+
this.ws = new WebSocket("wss://irc-ws.chat.twitch.tv:443");
|
|
40
|
+
|
|
41
|
+
this.ws.on("open", () => {
|
|
42
|
+
this.ws.send(`PASS oauth:${this.config.token}`);
|
|
43
|
+
this.ws.send(`NICK ${this.config.username}`);
|
|
44
|
+
this.ws.send(`JOIN #${this.channel}`);
|
|
45
|
+
this.running = true;
|
|
46
|
+
console.log(`[Channel:Twitch] Connected to #${this.channel}`);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
this.ws.on("message", async (raw) => {
|
|
50
|
+
const line = raw.toString().trim();
|
|
51
|
+
|
|
52
|
+
// Keep alive
|
|
53
|
+
if (line.startsWith("PING")) {
|
|
54
|
+
this.ws.send("PONG :tmi.twitch.tv");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Parse: :user!user@user.tmi.twitch.tv PRIVMSG #channel :message
|
|
59
|
+
const match = line.match(/^:(\w+)!\w+@\w+\.tmi\.twitch\.tv PRIVMSG #(\w+) :(.+)$/);
|
|
60
|
+
if (!match) return;
|
|
61
|
+
|
|
62
|
+
const [, username, , message] = match;
|
|
63
|
+
if (username.toLowerCase() === this.config.username.toLowerCase()) return;
|
|
64
|
+
|
|
65
|
+
// Check for command prefix or @mention
|
|
66
|
+
const mentioned = message.toLowerCase().includes(`@${this.config.username.toLowerCase()}`);
|
|
67
|
+
const hasPrefix = message.toLowerCase().startsWith(this.prefix.toLowerCase());
|
|
68
|
+
if (!hasPrefix && !mentioned) return;
|
|
69
|
+
|
|
70
|
+
if (!this.isAllowed(username)) {
|
|
71
|
+
this._sendChat(`@${username} Sorry, you're not on the allowlist.`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const input = message
|
|
76
|
+
.replace(new RegExp(`^${this.prefix}\\s*`, "i"), "")
|
|
77
|
+
.replace(new RegExp(`@${this.config.username}\\s*`, "ig"), "")
|
|
78
|
+
.trim();
|
|
79
|
+
|
|
80
|
+
if (!input) return;
|
|
81
|
+
|
|
82
|
+
const channelMeta = { channel: this.channel, username };
|
|
83
|
+
|
|
84
|
+
const task = await taskQueue.enqueue({
|
|
85
|
+
input,
|
|
86
|
+
channel: "twitch",
|
|
87
|
+
sessionId: this.getSessionId(username),
|
|
88
|
+
channelMeta,
|
|
89
|
+
model: this.getModel(),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const result = await taskQueue.waitForResult(task.id);
|
|
93
|
+
if (!this.isTaskMerged(result)) {
|
|
94
|
+
// Twitch chat max 500 chars per message
|
|
95
|
+
const reply = (result.result || "(no response)").slice(0, 490);
|
|
96
|
+
this._sendChat(`@${username} ${reply}`);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
this.ws.on("error", (err) => console.log(`[Channel:Twitch] WS error: ${err.message}`));
|
|
101
|
+
this.ws.on("close", () => {
|
|
102
|
+
this.running = false;
|
|
103
|
+
console.log("[Channel:Twitch] Disconnected");
|
|
104
|
+
});
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.log(`[Channel:Twitch] Failed to start: ${err.message}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_sendChat(text) {
|
|
111
|
+
if (this.ws?.readyState === 1) {
|
|
112
|
+
this.ws.send(`PRIVMSG #${this.channel} :${text.slice(0, 490)}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async stop() {
|
|
117
|
+
if (this.ws) {
|
|
118
|
+
this.ws.close();
|
|
119
|
+
this.running = false;
|
|
120
|
+
}
|
|
121
|
+
console.log("[Channel:Twitch] Stopped");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async sendReply(channelMeta, text) {
|
|
125
|
+
const prefix = channelMeta?.username ? `@${channelMeta.username} ` : "";
|
|
126
|
+
this._sendChat(`${prefix}${text.slice(0, 490 - prefix.length)}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -6,7 +6,7 @@ import { join } from "node:path";
|
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* WhatsApp Channel
|
|
9
|
+
* WhatsApp Channel - receives messages via Twilio webhook.
|
|
10
10
|
*
|
|
11
11
|
* Setup:
|
|
12
12
|
* 1. Create Twilio account + WhatsApp sandbox
|
|
@@ -14,11 +14,11 @@ import { tmpdir } from "node:os";
|
|
|
14
14
|
* 3. Set env: TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_WHATSAPP_FROM
|
|
15
15
|
*
|
|
16
16
|
* Config:
|
|
17
|
-
* accountSid
|
|
18
|
-
* authToken
|
|
19
|
-
* from
|
|
20
|
-
* allowlist
|
|
21
|
-
* model
|
|
17
|
+
* accountSid - Twilio account SID
|
|
18
|
+
* authToken - Twilio auth token
|
|
19
|
+
* from - Your WhatsApp number (whatsapp:+14155238886)
|
|
20
|
+
* allowlist - Optional array of phone numbers (+1234567890) allowed to send tasks
|
|
21
|
+
* model - Optional model override
|
|
22
22
|
*/
|
|
23
23
|
export class WhatsAppChannel extends BaseChannel {
|
|
24
24
|
constructor(config) {
|
|
@@ -28,7 +28,7 @@ export class WhatsAppChannel extends BaseChannel {
|
|
|
28
28
|
|
|
29
29
|
async start() {
|
|
30
30
|
if (!this.config.accountSid || !this.config.authToken) {
|
|
31
|
-
console.log(`[Channel:WhatsApp] Skipped
|
|
31
|
+
console.log(`[Channel:WhatsApp] Skipped - no Twilio credentials`);
|
|
32
32
|
return;
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -37,7 +37,7 @@ export class WhatsAppChannel extends BaseChannel {
|
|
|
37
37
|
this.running = true;
|
|
38
38
|
console.log(`[Channel:WhatsApp] Ready (webhook: POST /webhooks/whatsapp)`);
|
|
39
39
|
if (this.config.allowlist?.length) {
|
|
40
|
-
console.log(`[Channel:WhatsApp] Allowlist active
|
|
40
|
+
console.log(`[Channel:WhatsApp] Allowlist active - ${this.config.allowlist.length} authorized number(s)`);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -78,7 +78,7 @@ export class WhatsAppChannel extends BaseChannel {
|
|
|
78
78
|
if (!localPath) continue;
|
|
79
79
|
|
|
80
80
|
if (mediaType.startsWith("audio/")) {
|
|
81
|
-
console.log(`[Channel:WhatsApp] Audio media
|
|
81
|
+
console.log(`[Channel:WhatsApp] Audio media - transcribing...`);
|
|
82
82
|
const transcript = await transcribeAudio(localPath);
|
|
83
83
|
inputParts.push(transcript.startsWith("Error:")
|
|
84
84
|
? `[Audio file: ${localPath}]\n${transcript}`
|
|
@@ -106,7 +106,7 @@ export class WhatsAppChannel extends BaseChannel {
|
|
|
106
106
|
// Wait for completion
|
|
107
107
|
const completedTask = await taskQueue.waitForCompletion(task.id);
|
|
108
108
|
|
|
109
|
-
// Absorbed into a concurrent session
|
|
109
|
+
// Absorbed into a concurrent session - response already sent via original task
|
|
110
110
|
if (this.isTaskMerged(completedTask)) return;
|
|
111
111
|
|
|
112
112
|
// Send reply via Twilio
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { BaseChannel } from "./BaseChannel.js";
|
|
2
|
+
import taskQueue from "../core/TaskQueue.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Zalo Channel - receives messages via Zalo Official Account API.
|
|
6
|
+
* Popular messaging platform in Vietnam (~75M users).
|
|
7
|
+
*
|
|
8
|
+
* Setup:
|
|
9
|
+
* 1. Create a Zalo Official Account at https://oa.zalo.me
|
|
10
|
+
* 2. Register an app and get App ID + App Secret
|
|
11
|
+
* 3. Get an access token via OAuth or ZCA token
|
|
12
|
+
* 4. Set env: ZALO_APP_ID, ZALO_APP_SECRET, ZALO_ACCESS_TOKEN
|
|
13
|
+
* 5. Configure webhook URL: https://your-domain.com/channels/zalo
|
|
14
|
+
*
|
|
15
|
+
* Config:
|
|
16
|
+
* appId - Zalo App ID
|
|
17
|
+
* appSecret - Zalo App Secret
|
|
18
|
+
* accessToken - Zalo OA access token
|
|
19
|
+
* port - Webhook port (default 3005)
|
|
20
|
+
* allowlist - Optional array of Zalo user IDs
|
|
21
|
+
* model - Optional model override
|
|
22
|
+
*/
|
|
23
|
+
export class ZaloChannel extends BaseChannel {
|
|
24
|
+
constructor(config) {
|
|
25
|
+
super("zalo", config);
|
|
26
|
+
this.server = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async start() {
|
|
30
|
+
if (!this.config.appId || !this.config.accessToken) {
|
|
31
|
+
console.log("[Channel:Zalo] Skipped - missing ZALO_APP_ID or ZALO_ACCESS_TOKEN");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { createServer } = await import("node:http");
|
|
36
|
+
|
|
37
|
+
this.server = createServer(async (req, res) => {
|
|
38
|
+
if (req.url !== "/channels/zalo") {
|
|
39
|
+
res.writeHead(404).end();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Zalo sends GET for webhook verification
|
|
44
|
+
if (req.method === "GET") {
|
|
45
|
+
const url = new URL(req.url, `http://localhost`);
|
|
46
|
+
const challenge = url.searchParams.get("challenge");
|
|
47
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
48
|
+
res.end(challenge || "ok");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (req.method !== "POST") {
|
|
53
|
+
res.writeHead(405).end();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let body = "";
|
|
58
|
+
req.on("data", d => body += d);
|
|
59
|
+
req.on("end", async () => {
|
|
60
|
+
res.writeHead(200).end("ok");
|
|
61
|
+
try {
|
|
62
|
+
const payload = JSON.parse(body);
|
|
63
|
+
if (payload.event_name !== "user_send_text") return;
|
|
64
|
+
|
|
65
|
+
const senderId = payload.sender?.id;
|
|
66
|
+
if (!senderId || !this.isAllowed(senderId)) return;
|
|
67
|
+
|
|
68
|
+
const input = payload.message?.text?.trim();
|
|
69
|
+
if (!input) return;
|
|
70
|
+
|
|
71
|
+
const channelMeta = { senderId };
|
|
72
|
+
const task = await taskQueue.enqueue({
|
|
73
|
+
input,
|
|
74
|
+
channel: "zalo",
|
|
75
|
+
sessionId: this.getSessionId(senderId),
|
|
76
|
+
channelMeta,
|
|
77
|
+
model: this.getModel(),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const result = await taskQueue.waitForResult(task.id);
|
|
81
|
+
if (!this.isTaskMerged(result)) {
|
|
82
|
+
await this.sendReply(channelMeta, result.result || "(no response)");
|
|
83
|
+
}
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.log(`[Channel:Zalo] Error: ${err.message}`);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const port = this.config.port || 3005;
|
|
91
|
+
await new Promise(resolve => this.server.listen(port, resolve));
|
|
92
|
+
this.running = true;
|
|
93
|
+
console.log(`[Channel:Zalo] Webhook listening on port ${port}/channels/zalo`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async stop() {
|
|
97
|
+
if (this.server) {
|
|
98
|
+
await new Promise(resolve => this.server.close(resolve));
|
|
99
|
+
this.running = false;
|
|
100
|
+
}
|
|
101
|
+
console.log("[Channel:Zalo] Stopped");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async sendReply(channelMeta, text) {
|
|
105
|
+
if (!channelMeta?.senderId) return;
|
|
106
|
+
const fetchFn = globalThis.fetch || (await import("node-fetch")).default;
|
|
107
|
+
await fetchFn(`https://openapi.zalo.me/v3.0/oa/message/cs`, {
|
|
108
|
+
method: "POST",
|
|
109
|
+
headers: {
|
|
110
|
+
"access_token": this.config.accessToken,
|
|
111
|
+
"Content-Type": "application/json",
|
|
112
|
+
},
|
|
113
|
+
body: JSON.stringify({
|
|
114
|
+
recipient: { user_id: channelMeta.senderId },
|
|
115
|
+
message: { text: text.slice(0, 2000) },
|
|
116
|
+
}),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { BaseChannel } from "./BaseChannel.js";
|
|
2
|
+
import taskQueue from "../core/TaskQueue.js";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* iMessage Channel - send/receive iMessages on macOS via AppleScript.
|
|
7
|
+
*
|
|
8
|
+
* Setup:
|
|
9
|
+
* 1. Must run on macOS with Messages app configured
|
|
10
|
+
* 2. Grant Accessibility and Automation permissions to Terminal/Node
|
|
11
|
+
* 3. Set env: IMESSAGE_POLL_INTERVAL_MS (default 5000)
|
|
12
|
+
* Optional: IMESSAGE_ALLOWLIST - comma-separated phone numbers/emails
|
|
13
|
+
*
|
|
14
|
+
* Note: Polling-based (reads latest unread messages periodically).
|
|
15
|
+
* This is a best-effort implementation — macOS AppleScript access to iMessages
|
|
16
|
+
* has limitations; consider BlueBubbles channel for more reliable access.
|
|
17
|
+
*
|
|
18
|
+
* Config:
|
|
19
|
+
* pollIntervalMs - How often to check for new messages (default 5000)
|
|
20
|
+
* allowlist - Optional array of phone numbers/emails
|
|
21
|
+
* model - Optional model override
|
|
22
|
+
*/
|
|
23
|
+
export class iMessageChannel extends BaseChannel {
|
|
24
|
+
constructor(config) {
|
|
25
|
+
super("imessage", config);
|
|
26
|
+
this.pollInterval = null;
|
|
27
|
+
this.pollMs = config.pollIntervalMs || 5000;
|
|
28
|
+
this.lastChecked = new Date();
|
|
29
|
+
this.processedIds = new Set();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async start() {
|
|
33
|
+
if (process.platform !== "darwin") {
|
|
34
|
+
console.log("[Channel:iMessage] Skipped - macOS only");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Test that osascript can access Messages
|
|
40
|
+
execSync("osascript -e 'tell application \"Messages\" to count chats'", { timeout: 5000 });
|
|
41
|
+
} catch {
|
|
42
|
+
console.log("[Channel:iMessage] Skipped - cannot access Messages app (check Accessibility permissions)");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.running = true;
|
|
47
|
+
console.log(`[Channel:iMessage] Started polling every ${this.pollMs}ms`);
|
|
48
|
+
|
|
49
|
+
this.pollInterval = setInterval(() => this._poll(), this.pollMs);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async _poll() {
|
|
53
|
+
try {
|
|
54
|
+
const script = `
|
|
55
|
+
tell application "Messages"
|
|
56
|
+
set output to {}
|
|
57
|
+
repeat with aChat in chats
|
|
58
|
+
if (count of messages of aChat) > 0 then
|
|
59
|
+
set lastMsg to last message of aChat
|
|
60
|
+
set msgDate to date sent of lastMsg
|
|
61
|
+
set msgId to id of lastMsg
|
|
62
|
+
if msgDate > date "${this.lastChecked.toLocaleString()}" then
|
|
63
|
+
if incoming of lastMsg then
|
|
64
|
+
set msgContent to content of lastMsg
|
|
65
|
+
set senderId to handle id of sender of lastMsg
|
|
66
|
+
set end of output to msgId & "|" & senderId & "|" & msgContent
|
|
67
|
+
end if
|
|
68
|
+
end if
|
|
69
|
+
end if
|
|
70
|
+
end repeat
|
|
71
|
+
return output
|
|
72
|
+
end tell
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
let raw;
|
|
76
|
+
try {
|
|
77
|
+
raw = execSync(`osascript -e '${script.replace(/'/g, "\\'")}'`, {
|
|
78
|
+
encoding: "utf-8", timeout: 10000
|
|
79
|
+
});
|
|
80
|
+
} catch { return; }
|
|
81
|
+
|
|
82
|
+
this.lastChecked = new Date();
|
|
83
|
+
if (!raw.trim()) return;
|
|
84
|
+
|
|
85
|
+
const messages = raw.trim().split(", ");
|
|
86
|
+
for (const msg of messages) {
|
|
87
|
+
const [id, sender, ...contentParts] = msg.split("|");
|
|
88
|
+
const content = contentParts.join("|").trim();
|
|
89
|
+
if (!id || !sender || !content) continue;
|
|
90
|
+
if (this.processedIds.has(id)) continue;
|
|
91
|
+
this.processedIds.add(id);
|
|
92
|
+
|
|
93
|
+
if (!this.isAllowed(sender)) continue;
|
|
94
|
+
|
|
95
|
+
const channelMeta = { sender, chatId: sender };
|
|
96
|
+
const task = await taskQueue.enqueue({
|
|
97
|
+
input: content,
|
|
98
|
+
channel: "imessage",
|
|
99
|
+
sessionId: this.getSessionId(sender),
|
|
100
|
+
channelMeta,
|
|
101
|
+
model: this.getModel(),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
taskQueue.waitForResult(task.id).then(result => {
|
|
105
|
+
if (!this.isTaskMerged(result)) {
|
|
106
|
+
this.sendReply(channelMeta, result.result || "(no response)");
|
|
107
|
+
}
|
|
108
|
+
}).catch(() => {});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Keep processedIds from growing unbounded
|
|
112
|
+
if (this.processedIds.size > 1000) {
|
|
113
|
+
const arr = [...this.processedIds];
|
|
114
|
+
this.processedIds = new Set(arr.slice(-500));
|
|
115
|
+
}
|
|
116
|
+
} catch (err) {
|
|
117
|
+
// Silent — polling failures are expected occasionally
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async stop() {
|
|
122
|
+
if (this.pollInterval) {
|
|
123
|
+
clearInterval(this.pollInterval);
|
|
124
|
+
this.pollInterval = null;
|
|
125
|
+
}
|
|
126
|
+
this.running = false;
|
|
127
|
+
console.log("[Channel:iMessage] Stopped");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async sendReply(channelMeta, text) {
|
|
131
|
+
if (!channelMeta?.sender) return;
|
|
132
|
+
const sender = channelMeta.sender;
|
|
133
|
+
// Split into chunks (AppleScript may time out on very long strings)
|
|
134
|
+
const chunks = text.match(/[\s\S]{1,1000}/g) || [text];
|
|
135
|
+
for (const chunk of chunks) {
|
|
136
|
+
const script = `
|
|
137
|
+
tell application "Messages"
|
|
138
|
+
set targetService to 1st service whose service type = iMessage
|
|
139
|
+
set targetBuddy to buddy "${sender.replace(/"/g, '\\"')}" of targetService
|
|
140
|
+
send "${chunk.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}" to targetBuddy
|
|
141
|
+
end tell
|
|
142
|
+
`;
|
|
143
|
+
try {
|
|
144
|
+
execSync(`osascript -e '${script.replace(/'/g, "\\'")}'`, { timeout: 10000 });
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.log(`[Channel:iMessage] Send error: ${err.message}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|