daemora 1.0.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.
Files changed (115) hide show
  1. package/README.md +666 -0
  2. package/SOUL.md +104 -0
  3. package/config/hooks.json +14 -0
  4. package/config/mcp.json +145 -0
  5. package/package.json +86 -0
  6. package/skills/.gitkeep +0 -0
  7. package/skills/apple-notes.md +193 -0
  8. package/skills/apple-reminders.md +189 -0
  9. package/skills/camsnap.md +162 -0
  10. package/skills/coding.md +14 -0
  11. package/skills/documents.md +13 -0
  12. package/skills/email.md +13 -0
  13. package/skills/gif-search.md +196 -0
  14. package/skills/healthcheck.md +225 -0
  15. package/skills/image-gen.md +147 -0
  16. package/skills/model-usage.md +182 -0
  17. package/skills/obsidian.md +207 -0
  18. package/skills/pdf.md +211 -0
  19. package/skills/research.md +13 -0
  20. package/skills/skill-creator.md +142 -0
  21. package/skills/spotify.md +149 -0
  22. package/skills/summarize.md +230 -0
  23. package/skills/things.md +199 -0
  24. package/skills/tmux.md +204 -0
  25. package/skills/trello.md +183 -0
  26. package/skills/video-frames.md +202 -0
  27. package/skills/weather.md +127 -0
  28. package/src/a2a/A2AClient.js +136 -0
  29. package/src/a2a/A2AServer.js +316 -0
  30. package/src/a2a/AgentCard.js +79 -0
  31. package/src/agents/SubAgentManager.js +369 -0
  32. package/src/agents/Supervisor.js +192 -0
  33. package/src/channels/BaseChannel.js +104 -0
  34. package/src/channels/DiscordChannel.js +288 -0
  35. package/src/channels/EmailChannel.js +172 -0
  36. package/src/channels/GoogleChatChannel.js +316 -0
  37. package/src/channels/HttpChannel.js +26 -0
  38. package/src/channels/LineChannel.js +168 -0
  39. package/src/channels/SignalChannel.js +186 -0
  40. package/src/channels/SlackChannel.js +329 -0
  41. package/src/channels/TeamsChannel.js +272 -0
  42. package/src/channels/TelegramChannel.js +347 -0
  43. package/src/channels/WhatsAppChannel.js +219 -0
  44. package/src/channels/index.js +198 -0
  45. package/src/cli.js +1267 -0
  46. package/src/config/agentProfiles.js +120 -0
  47. package/src/config/channels.js +32 -0
  48. package/src/config/default.js +206 -0
  49. package/src/config/models.js +123 -0
  50. package/src/config/permissions.js +167 -0
  51. package/src/core/AgentLoop.js +446 -0
  52. package/src/core/Compaction.js +143 -0
  53. package/src/core/CostTracker.js +116 -0
  54. package/src/core/EventBus.js +46 -0
  55. package/src/core/Task.js +67 -0
  56. package/src/core/TaskQueue.js +206 -0
  57. package/src/core/TaskRunner.js +226 -0
  58. package/src/daemon/DaemonManager.js +301 -0
  59. package/src/hooks/HookRunner.js +230 -0
  60. package/src/index.js +482 -0
  61. package/src/mcp/MCPAgentRunner.js +112 -0
  62. package/src/mcp/MCPClient.js +186 -0
  63. package/src/mcp/MCPManager.js +412 -0
  64. package/src/models/ModelRouter.js +180 -0
  65. package/src/safety/AuditLog.js +135 -0
  66. package/src/safety/CircuitBreaker.js +126 -0
  67. package/src/safety/FilesystemGuard.js +169 -0
  68. package/src/safety/GitRollback.js +139 -0
  69. package/src/safety/HumanApproval.js +156 -0
  70. package/src/safety/InputSanitizer.js +72 -0
  71. package/src/safety/PermissionGuard.js +83 -0
  72. package/src/safety/Sandbox.js +70 -0
  73. package/src/safety/SecretScanner.js +100 -0
  74. package/src/safety/SecretVault.js +250 -0
  75. package/src/scheduler/Heartbeat.js +115 -0
  76. package/src/scheduler/Scheduler.js +228 -0
  77. package/src/services/models/outputSchema.js +15 -0
  78. package/src/services/openai.js +25 -0
  79. package/src/services/sessions.js +65 -0
  80. package/src/setup/theme.js +110 -0
  81. package/src/setup/wizard.js +788 -0
  82. package/src/skills/SkillLoader.js +168 -0
  83. package/src/storage/TaskStore.js +69 -0
  84. package/src/systemPrompt.js +526 -0
  85. package/src/tenants/TenantContext.js +19 -0
  86. package/src/tenants/TenantManager.js +379 -0
  87. package/src/tools/ToolRegistry.js +141 -0
  88. package/src/tools/applyPatch.js +144 -0
  89. package/src/tools/browserAutomation.js +223 -0
  90. package/src/tools/createDocument.js +265 -0
  91. package/src/tools/cronTool.js +105 -0
  92. package/src/tools/editFile.js +139 -0
  93. package/src/tools/executeCommand.js +123 -0
  94. package/src/tools/glob.js +67 -0
  95. package/src/tools/grep.js +121 -0
  96. package/src/tools/imageAnalysis.js +120 -0
  97. package/src/tools/index.js +173 -0
  98. package/src/tools/listDirectory.js +47 -0
  99. package/src/tools/manageAgents.js +47 -0
  100. package/src/tools/manageMCP.js +159 -0
  101. package/src/tools/memory.js +478 -0
  102. package/src/tools/messageChannel.js +45 -0
  103. package/src/tools/projectTracker.js +259 -0
  104. package/src/tools/readFile.js +52 -0
  105. package/src/tools/screenCapture.js +112 -0
  106. package/src/tools/searchContent.js +76 -0
  107. package/src/tools/searchFiles.js +75 -0
  108. package/src/tools/sendEmail.js +118 -0
  109. package/src/tools/sendFile.js +63 -0
  110. package/src/tools/textToSpeech.js +161 -0
  111. package/src/tools/transcribeAudio.js +82 -0
  112. package/src/tools/useMCP.js +29 -0
  113. package/src/tools/webFetch.js +150 -0
  114. package/src/tools/webSearch.js +134 -0
  115. package/src/tools/writeFile.js +26 -0
@@ -0,0 +1,329 @@
1
+ import { BaseChannel } from "./BaseChannel.js";
2
+ import taskQueue from "../core/TaskQueue.js";
3
+ import eventBus from "../core/EventBus.js";
4
+ import { transcribeAudio } from "../tools/transcribeAudio.js";
5
+ import { createReadStream, writeFileSync, mkdirSync } from "node:fs";
6
+ import { join, extname, basename } from "node:path";
7
+ import { tmpdir } from "node:os";
8
+
9
+ /**
10
+ * Slack Channel — receives messages via Slack Bolt + Socket Mode.
11
+ *
12
+ * Socket Mode means NO public webhook URL needed — works on any machine.
13
+ *
14
+ * Setup:
15
+ * 1. Go to https://api.slack.com/apps → Create New App → From Scratch
16
+ * 2. Under "Socket Mode" → Enable Socket Mode → Generate App-Level Token (xapp-...)
17
+ * → Grant scope: connections:write → copy token as SLACK_APP_TOKEN
18
+ * 3. Under "OAuth & Permissions" → Bot Token Scopes: add:
19
+ * chat:write, channels:history, groups:history, im:history, mpim:history,
20
+ * channels:read, groups:read, im:read, mpim:read, app_mentions:read,
21
+ * reactions:write, reactions:read
22
+ * → Install app to workspace → copy Bot User OAuth Token (xoxb-...) as SLACK_BOT_TOKEN
23
+ * 4. Under "Event Subscriptions" → Enable Events → Subscribe to bot events:
24
+ * message.im, app_mention
25
+ * 5. Set env: SLACK_BOT_TOKEN, SLACK_APP_TOKEN
26
+ *
27
+ * Config:
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
+ *
33
+ * The bot responds to:
34
+ * - Direct messages (message.im)
35
+ * - @mentions in channels (app_mention)
36
+ */
37
+ export class SlackChannel extends BaseChannel {
38
+ constructor(config) {
39
+ super("slack", config);
40
+ this.app = null;
41
+ this.botUserId = null;
42
+ }
43
+
44
+ async start() {
45
+ if (!this.config.botToken || !this.config.appToken) {
46
+ console.log(`[Channel:Slack] Skipped — need SLACK_BOT_TOKEN and SLACK_APP_TOKEN`);
47
+ return;
48
+ }
49
+
50
+ const { App } = await import("@slack/bolt");
51
+
52
+ this.app = new App({
53
+ token: this.config.botToken,
54
+ appToken: this.config.appToken,
55
+ socketMode: true,
56
+ logLevel: "error",
57
+ });
58
+
59
+ // Resolve bot's own user ID
60
+ try {
61
+ const authResult = await this.app.client.auth.test({ token: this.config.botToken });
62
+ this.botUserId = authResult.user_id;
63
+ } catch (_) {}
64
+
65
+ // Handle @mentions in channels
66
+ this.app.event("app_mention", async ({ event, say }) => {
67
+ const text = event.text
68
+ .replace(/<@[A-Z0-9]+>/g, "")
69
+ .trim();
70
+
71
+ if (!text) {
72
+ await say({ text: "Yes? Send me a task.", thread_ts: event.ts });
73
+ return;
74
+ }
75
+
76
+ await this._handleMessage({
77
+ text,
78
+ userId: event.user,
79
+ channelId: event.channel,
80
+ threadTs: event.thread_ts || event.ts,
81
+ messageTs: event.ts,
82
+ say,
83
+ });
84
+ });
85
+
86
+ // Handle direct messages
87
+ this.app.message(async ({ message, say }) => {
88
+ if (message.bot_id) return; // Ignore bot messages
89
+
90
+ const hasFiles = message.files && message.files.length > 0;
91
+ if (!message.text && !hasFiles) return;
92
+
93
+ await this._handleMessage({
94
+ text: message.text?.trim() || "",
95
+ files: message.files || [],
96
+ userId: message.user,
97
+ channelId: message.channel,
98
+ threadTs: message.thread_ts || message.ts,
99
+ messageTs: message.ts,
100
+ say,
101
+ });
102
+ });
103
+
104
+ // Approval replies
105
+ eventBus.on("approval:request", async (data) => {
106
+ if (data.channelMeta?.channel !== "slack") return;
107
+ try {
108
+ await this.app.client.chat.postMessage({
109
+ token: this.config.botToken,
110
+ channel: data.channelMeta?.channelId,
111
+ text: data.message,
112
+ thread_ts: data.channelMeta?.threadTs,
113
+ });
114
+ } catch (_) {}
115
+ });
116
+
117
+ try {
118
+ await this.app.start();
119
+ this.running = true;
120
+ console.log(`[Channel:Slack] Started (Socket Mode)`);
121
+ if (this.config.allowlist?.length) {
122
+ console.log(`[Channel:Slack] Allowlist active — ${this.config.allowlist.length} authorized user(s)`);
123
+ }
124
+ } catch (err) {
125
+ console.log(`[Channel:Slack] Failed to start: ${err.message}`);
126
+ }
127
+ }
128
+
129
+ async _handleMessage({ text, files = [], userId, channelId, threadTs, messageTs, say }) {
130
+ // Allowlist check
131
+ if (!this.isAllowed(userId)) {
132
+ console.log(`[Channel:Slack] Blocked (not in allowlist): ${userId}`);
133
+ await say({ text: "You are not authorized to use this agent.", thread_ts: threadTs });
134
+ return;
135
+ }
136
+
137
+ console.log(`[Channel:Slack] Message from ${userId}: "${text.slice(0, 80)}"${files.length ? ` + ${files.length} file(s)` : ""}`);
138
+
139
+ // React ⏳ to show we're working
140
+ await this._addReaction(channelId, messageTs, "hourglass_flowing_sand");
141
+
142
+ // Build input from text + files
143
+ const inputParts = text ? [text] : [];
144
+ for (const file of files) {
145
+ const localPath = await this._downloadFile(file);
146
+ if (!localPath) continue;
147
+
148
+ const mimeType = file.mimetype || "";
149
+ if (mimeType.startsWith("audio/")) {
150
+ console.log(`[Channel:Slack] Audio file — transcribing...`);
151
+ const transcript = await transcribeAudio(localPath);
152
+ inputParts.push(transcript.startsWith("Error:")
153
+ ? `[Audio file: ${localPath}]\n${transcript}`
154
+ : `[Audio transcript]: ${transcript}`);
155
+ } else if (mimeType.startsWith("image/")) {
156
+ inputParts.push(`[Photo received: ${localPath}]\nUser caption: ${file.title || text || "Describe and respond to this image."}`);
157
+ } else if (mimeType.startsWith("video/")) {
158
+ inputParts.push(`[Video received: ${localPath}]`);
159
+ } else {
160
+ inputParts.push(`[File received: ${localPath} (${file.name || "document"}, ${_fmtSize(file.size)})]`);
161
+ }
162
+ }
163
+
164
+ const input = inputParts.join("\n");
165
+
166
+ const task = taskQueue.enqueue({
167
+ input,
168
+ channel: "slack",
169
+ channelMeta: { userId, channelId, threadTs, messageTs, channel: "slack" },
170
+ sessionId: this.getSessionId(userId),
171
+ model: this.getModel(),
172
+ });
173
+
174
+ try {
175
+ const completedTask = await taskQueue.waitForCompletion(task.id);
176
+
177
+ // Absorbed into a concurrent session — response already sent via original task
178
+ if (this.isTaskMerged(completedTask)) {
179
+ await this._removeReaction(channelId, messageTs, "hourglass_flowing_sand");
180
+ await this._addReaction(channelId, messageTs, "white_check_mark");
181
+ return;
182
+ }
183
+
184
+ const failed = completedTask.status === "failed";
185
+ const response = failed
186
+ ? `Sorry, I encountered an error: ${completedTask.error}`
187
+ : completedTask.result || "Done.";
188
+
189
+ // Swap ⏳ for ✅ or ❌
190
+ await this._removeReaction(channelId, messageTs, "hourglass_flowing_sand");
191
+ await this._addReaction(channelId, messageTs, failed ? "x" : "white_check_mark");
192
+
193
+ // Reply in thread to keep conversations clean
194
+ const chunks = splitMessage(response, 3800);
195
+ for (const chunk of chunks) {
196
+ await say({ text: chunk, thread_ts: threadTs });
197
+ }
198
+ } catch (error) {
199
+ console.error(`[Channel:Slack] Error:`, error.message);
200
+ await this._removeReaction(channelId, messageTs, "hourglass_flowing_sand");
201
+ await this._addReaction(channelId, messageTs, "x");
202
+ await say({ text: "Sorry, something went wrong. Please try again.", thread_ts: threadTs });
203
+ }
204
+ }
205
+
206
+ async stop() {
207
+ if (this.app) {
208
+ await this.app.stop().catch(() => {});
209
+ this.running = false;
210
+ console.log(`[Channel:Slack] Stopped`);
211
+ }
212
+ }
213
+
214
+ async sendReply(channelMeta, text) {
215
+ if (!this.app) return;
216
+ const chunks = splitMessage(text, 3800);
217
+ for (const chunk of chunks) {
218
+ await this.app.client.chat.postMessage({
219
+ token: this.config.botToken,
220
+ channel: channelMeta.channelId,
221
+ text: chunk,
222
+ thread_ts: channelMeta.threadTs,
223
+ }).catch((err) => console.log(`[Channel:Slack] sendReply error: ${err.message}`));
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Add an emoji reaction to a Slack message.
229
+ * @param {string} channelId
230
+ * @param {string} timestamp - message ts
231
+ * @param {string} name - reaction name without colons (e.g. "white_check_mark")
232
+ */
233
+ async sendReaction(channelMeta, emoji) {
234
+ // Map unicode emoji to Slack reaction names
235
+ const emojiMap = { "✅": "white_check_mark", "❌": "x", "⏳": "hourglass_flowing_sand" };
236
+ const name = emojiMap[emoji] || emoji;
237
+ await this._addReaction(channelMeta.channelId, channelMeta.messageTs, name);
238
+ }
239
+
240
+ async _addReaction(channelId, timestamp, name) {
241
+ try {
242
+ await this.app.client.reactions.add({
243
+ token: this.config.botToken,
244
+ channel: channelId,
245
+ timestamp,
246
+ name,
247
+ });
248
+ } catch (_) {}
249
+ }
250
+
251
+ async _removeReaction(channelId, timestamp, name) {
252
+ try {
253
+ await this.app.client.reactions.remove({
254
+ token: this.config.botToken,
255
+ channel: channelId,
256
+ timestamp,
257
+ name,
258
+ });
259
+ } catch (_) {}
260
+ }
261
+
262
+ /**
263
+ * Send a local file to a Slack channel.
264
+ */
265
+ async sendFile(channelMeta, filePath, caption) {
266
+ if (!this.app) return;
267
+ try {
268
+ await this.app.client.filesUploadV2({
269
+ token: this.config.botToken,
270
+ channel_id: channelMeta.channelId,
271
+ file: createReadStream(filePath),
272
+ filename: basename(filePath),
273
+ initial_comment: caption || undefined,
274
+ });
275
+ } catch (err) {
276
+ console.log(`[Channel:Slack] sendFile error: ${err.message}`);
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Download a Slack file to /tmp (requires bot token for auth).
282
+ */
283
+ async _downloadFile(file) {
284
+ try {
285
+ const url = file.url_private || file.url_private_download;
286
+ if (!url) return null;
287
+
288
+ const ext = extname(file.name || file.title || "").split("?")[0] || "";
289
+ const tmpDir = join(tmpdir(), "daemora-slack");
290
+ mkdirSync(tmpDir, { recursive: true });
291
+ const filePath = join(tmpDir, `${file.id}${ext}`);
292
+
293
+ const res = await fetch(url, {
294
+ headers: { Authorization: `Bearer ${this.config.botToken}` },
295
+ signal: AbortSignal.timeout(30000),
296
+ });
297
+ if (!res.ok) return null;
298
+
299
+ const buffer = await res.arrayBuffer();
300
+ writeFileSync(filePath, Buffer.from(buffer));
301
+ return filePath;
302
+ } catch (err) {
303
+ console.log(`[Channel:Slack] File download error: ${err.message}`);
304
+ return null;
305
+ }
306
+ }
307
+ }
308
+
309
+ function _fmtSize(bytes) {
310
+ if (!bytes) return "unknown size";
311
+ if (bytes < 1024) return `${bytes} B`;
312
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
313
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
314
+ }
315
+
316
+ function splitMessage(text, maxLength) {
317
+ if (text.length <= maxLength) return [text];
318
+ const chunks = [];
319
+ let remaining = text;
320
+ while (remaining.length > 0) {
321
+ if (remaining.length <= maxLength) { chunks.push(remaining); break; }
322
+ let idx = remaining.lastIndexOf("\n", maxLength);
323
+ if (idx < maxLength * 0.5) idx = remaining.lastIndexOf(" ", maxLength);
324
+ if (idx === -1) idx = maxLength;
325
+ chunks.push(remaining.slice(0, idx));
326
+ remaining = remaining.slice(idx).trimStart();
327
+ }
328
+ return chunks;
329
+ }
@@ -0,0 +1,272 @@
1
+ import { BaseChannel } from "./BaseChannel.js";
2
+ import taskQueue from "../core/TaskQueue.js";
3
+ import { transcribeAudio } from "../tools/transcribeAudio.js";
4
+ import { writeFileSync, mkdirSync } from "node:fs";
5
+ import { join, extname } from "node:path";
6
+ import { tmpdir } from "node:os";
7
+
8
+ /**
9
+ * Microsoft Teams Channel — receives messages via Bot Framework v4 (CloudAdapter).
10
+ *
11
+ * Setup:
12
+ * 1. Go to https://portal.azure.com → Create a resource → Azure Bot
13
+ * 2. Set messaging endpoint to: https://your-server/webhooks/teams
14
+ * 3. Under "Configuration" → copy App ID as TEAMS_APP_ID
15
+ * 4. Under "Configuration" → Manage Password → New client secret → copy as TEAMS_APP_PASSWORD
16
+ * 5. In the bot resource → Channels → Add Microsoft Teams
17
+ *
18
+ * Config:
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
+ *
24
+ * Unlike OpenClaw's 461-line channel with Adaptive Cards and Graph API,
25
+ * this is minimal: text messages + file attachments + proactive reply.
26
+ *
27
+ * Teams has a 5-second webhook timeout, so we ack immediately and
28
+ * deliver the agent reply via adapter.continueConversation().
29
+ */
30
+ export class TeamsChannel extends BaseChannel {
31
+ constructor(config) {
32
+ super("teams", config);
33
+ this.adapter = null;
34
+ this._conversationRefs = new Map(); // userId → conversationReference
35
+ }
36
+
37
+ async start() {
38
+ if (!this.config.appId || !this.config.appPassword) {
39
+ console.log(`[Channel:Teams] Skipped — set TEAMS_APP_ID and TEAMS_APP_PASSWORD`);
40
+ return;
41
+ }
42
+
43
+ const {
44
+ CloudAdapter,
45
+ ConfigurationBotFrameworkAuthentication,
46
+ TurnContext,
47
+ } = await import("botbuilder");
48
+
49
+ this._TurnContext = TurnContext;
50
+
51
+ const auth = new ConfigurationBotFrameworkAuthentication({
52
+ MicrosoftAppId: this.config.appId,
53
+ MicrosoftAppPassword: this.config.appPassword,
54
+ });
55
+
56
+ this.adapter = new CloudAdapter(auth);
57
+
58
+ this.adapter.onTurnError = async (context, error) => {
59
+ console.error(`[Channel:Teams] Turn error: ${error.message}`);
60
+ try { await context.sendActivity("Sorry, something went wrong. Please try again."); } catch (_) {}
61
+ };
62
+
63
+ this.running = true;
64
+ console.log(`[Channel:Teams] Ready (webhook: POST /webhooks/teams)`);
65
+ if (this.config.allowlist?.length) {
66
+ console.log(`[Channel:Teams] Allowlist active — ${this.config.allowlist.length} authorized user(s)`);
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Handle inbound webhook from Bot Framework.
72
+ * Called by Express route in index.js.
73
+ * Must respond with HTTP 200 within ~5 seconds, so we ack immediately
74
+ * and deliver the agent reply via proactive messaging.
75
+ */
76
+ async handleWebhook(req, res) {
77
+ if (!this.adapter) {
78
+ res.status(503).json({ error: "Teams channel not started" });
79
+ return;
80
+ }
81
+
82
+ await this.adapter.process(req, res, async (context) => {
83
+ const type = context.activity.type;
84
+
85
+ // Welcome message when bot is added to a conversation
86
+ if (type === "conversationUpdate") {
87
+ const added = context.activity.membersAdded || [];
88
+ for (const member of added) {
89
+ if (member.id !== context.activity.recipient.id) {
90
+ await context.sendActivity("Hello! I'm Daemora. Send me a message and I'll get to work.");
91
+ }
92
+ }
93
+ return;
94
+ }
95
+
96
+ if (type !== "message") return;
97
+
98
+ const userId = context.activity.from?.id || "unknown";
99
+ const userName = context.activity.from?.name || "User";
100
+ const channelId = context.activity.channelData?.teamsChannelId || context.activity.conversation?.id;
101
+ const text = (context.activity.text || "").replace(/<at[^>]*>.*?<\/at>/gi, "").trim();
102
+ const attachments = context.activity.attachments || [];
103
+
104
+ // Allowlist check
105
+ if (!this.isAllowed(userId)) {
106
+ await context.sendActivity("You are not authorized to use this agent.");
107
+ return;
108
+ }
109
+
110
+ // Build input from text + any file attachments
111
+ const inputParts = text ? [text] : [];
112
+ for (const att of attachments) {
113
+ if (att.contentType === "application/vnd.microsoft.teams.file.download.info") {
114
+ const localPath = await this._downloadAttachment(att);
115
+ if (localPath) {
116
+ const ct = att.contentType || "";
117
+ if (ct.includes("audio")) {
118
+ const transcript = await transcribeAudio(localPath);
119
+ inputParts.push(transcript.startsWith("Error:")
120
+ ? `[Audio file: ${localPath}]\n${transcript}`
121
+ : `[Audio transcript]: ${transcript}`);
122
+ } else {
123
+ inputParts.push(`[File received: ${localPath}]`);
124
+ }
125
+ }
126
+ } else if (att.contentUrl) {
127
+ const ext = _extFromContentType(att.contentType || "");
128
+ const localPath = await this._downloadUrl(att.contentUrl, ext, this.config);
129
+ if (localPath) {
130
+ if ((att.contentType || "").startsWith("image/")) {
131
+ inputParts.push(`[Photo received: ${localPath}]${text ? "" : "\nDescribe this image."}`);
132
+ } else {
133
+ inputParts.push(`[File received: ${localPath} (${att.name || "attachment"})]`);
134
+ }
135
+ }
136
+ }
137
+ }
138
+
139
+ if (inputParts.length === 0) {
140
+ await context.sendActivity("Send me a message and I'll get to work.");
141
+ return;
142
+ }
143
+
144
+ const input = inputParts.join("\n");
145
+ console.log(`[Channel:Teams] Message from ${userName} (${userId}): "${input.slice(0, 80)}"`);
146
+
147
+ // Save conversation reference for proactive reply later
148
+ const ref = this._TurnContext.getConversationReference(context.activity);
149
+ this._conversationRefs.set(userId, ref);
150
+
151
+ // Ack with typing indicator — keeps Teams from showing "delivery failed"
152
+ await context.sendActivity({ type: "typing" });
153
+
154
+ // Enqueue task and reply proactively (don't await — we must return within 5s)
155
+ const task = taskQueue.enqueue({
156
+ input,
157
+ channel: "teams",
158
+ channelMeta: { userId, channelId, userName, channel: "teams" },
159
+ sessionId: this.getSessionId(userId),
160
+ model: this.getModel(),
161
+ });
162
+
163
+ // Fire-and-forget: wait for completion then deliver via continueConversation
164
+ taskQueue.waitForCompletion(task.id)
165
+ .then(async (completedTask) => {
166
+ if (this.isTaskMerged(completedTask)) return; // absorbed into concurrent session
167
+ const failed = completedTask.status === "failed";
168
+ const response = failed
169
+ ? `Sorry, I encountered an error: ${completedTask.error}`
170
+ : completedTask.result || "Done.";
171
+
172
+ await this.adapter.continueConversation(ref, async (proactiveCtx) => {
173
+ const chunks = splitMessage(response, 4000);
174
+ for (const chunk of chunks) {
175
+ await proactiveCtx.sendActivity(chunk);
176
+ }
177
+ });
178
+ })
179
+ .catch((err) => {
180
+ console.error(`[Channel:Teams] Reply error: ${err.message}`);
181
+ });
182
+ });
183
+ }
184
+
185
+ async stop() {
186
+ this.running = false;
187
+ this._conversationRefs.clear();
188
+ console.log(`[Channel:Teams] Stopped`);
189
+ }
190
+
191
+ async sendReply(channelMeta, text) {
192
+ if (!this.adapter) return;
193
+ const ref = this._conversationRefs.get(channelMeta.userId);
194
+ if (!ref) return;
195
+ try {
196
+ await this.adapter.continueConversation(ref, async (ctx) => {
197
+ const chunks = splitMessage(text, 4000);
198
+ for (const chunk of chunks) { await ctx.sendActivity(chunk); }
199
+ });
200
+ } catch (err) {
201
+ console.log(`[Channel:Teams] sendReply error: ${err.message}`);
202
+ }
203
+ }
204
+
205
+ async sendFile(channelMeta, filePath, caption) {
206
+ if (!this.adapter) return;
207
+ const ref = this._conversationRefs.get(channelMeta.userId);
208
+ if (!ref) return;
209
+ try {
210
+ // Teams file sending requires SharePoint upload for larger files.
211
+ // For simplicity: send the file path as text with caption.
212
+ // Full file upload requires Graph API + bot permissions — out of scope here.
213
+ const msg = caption
214
+ ? `${caption}\n(File: ${filePath})`
215
+ : `File: ${filePath}`;
216
+ await this.adapter.continueConversation(ref, async (ctx) => {
217
+ await ctx.sendActivity(msg);
218
+ });
219
+ } catch (err) {
220
+ console.log(`[Channel:Teams] sendFile error: ${err.message}`);
221
+ }
222
+ }
223
+
224
+ async _downloadUrl(url, ext, cfg) {
225
+ try {
226
+ const headers = {};
227
+ // Teams content URLs may require the bot credentials for auth
228
+ if (cfg?.appId && cfg?.appPassword) {
229
+ const creds = Buffer.from(`${cfg.appId}:${cfg.appPassword}`).toString("base64");
230
+ headers["Authorization"] = `Basic ${creds}`;
231
+ }
232
+ const res = await fetch(url, { headers, signal: AbortSignal.timeout(30000) });
233
+ if (!res.ok) return null;
234
+ const tmpDir = join(tmpdir(), "daemora-teams");
235
+ mkdirSync(tmpDir, { recursive: true });
236
+ const filePath = join(tmpDir, `att-${Date.now()}${ext}`);
237
+ writeFileSync(filePath, Buffer.from(await res.arrayBuffer()));
238
+ return filePath;
239
+ } catch { return null; }
240
+ }
241
+
242
+ async _downloadAttachment(att) {
243
+ const info = att.content?.downloadUrl;
244
+ if (!info) return null;
245
+ const ext = att.name ? extname(att.name) : "";
246
+ return this._downloadUrl(info, ext, this.config);
247
+ }
248
+ }
249
+
250
+ function _extFromContentType(ct) {
251
+ const map = {
252
+ "image/jpeg": ".jpg", "image/png": ".png", "image/gif": ".gif",
253
+ "audio/ogg": ".ogg", "audio/mpeg": ".mp3", "video/mp4": ".mp4",
254
+ "application/pdf": ".pdf",
255
+ };
256
+ return map[ct] || "";
257
+ }
258
+
259
+ function splitMessage(text, maxLength) {
260
+ if (text.length <= maxLength) return [text];
261
+ const chunks = [];
262
+ let remaining = text;
263
+ while (remaining.length > 0) {
264
+ if (remaining.length <= maxLength) { chunks.push(remaining); break; }
265
+ let idx = remaining.lastIndexOf("\n", maxLength);
266
+ if (idx < maxLength * 0.5) idx = remaining.lastIndexOf(" ", maxLength);
267
+ if (idx === -1) idx = maxLength;
268
+ chunks.push(remaining.slice(0, idx));
269
+ remaining = remaining.slice(idx).trimStart();
270
+ }
271
+ return chunks;
272
+ }