myaiforone 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 (315) hide show
  1. package/README.md +113 -0
  2. package/agents/_template/CLAUDE.md +18 -0
  3. package/agents/_template/agent.json +7 -0
  4. package/agents/platform/agentcreator/CLAUDE.md +300 -0
  5. package/agents/platform/appcreator/CLAUDE.md +158 -0
  6. package/agents/platform/gym/CLAUDE.md +486 -0
  7. package/agents/platform/gym/agent.json +40 -0
  8. package/agents/platform/gym/programs/agent-building/program.json +160 -0
  9. package/agents/platform/gym/programs/automations-mastery/program.json +129 -0
  10. package/agents/platform/gym/programs/getting-started/program.json +124 -0
  11. package/agents/platform/gym/programs/mcp-integrations/program.json +116 -0
  12. package/agents/platform/gym/programs/multi-model-strategy/program.json +115 -0
  13. package/agents/platform/gym/programs/prompt-engineering/program.json +136 -0
  14. package/agents/platform/gym/souls/alex.md +12 -0
  15. package/agents/platform/gym/souls/jordan.md +12 -0
  16. package/agents/platform/gym/souls/morgan.md +12 -0
  17. package/agents/platform/gym/souls/riley.md +12 -0
  18. package/agents/platform/gym/souls/sam.md +12 -0
  19. package/agents/platform/hub/CLAUDE.md +372 -0
  20. package/agents/platform/promptcreator/CLAUDE.md +130 -0
  21. package/agents/platform/skillcreator/CLAUDE.md +163 -0
  22. package/bin/cli.js +566 -0
  23. package/config.example.json +310 -0
  24. package/dist/agent-registry.d.ts +32 -0
  25. package/dist/agent-registry.d.ts.map +1 -0
  26. package/dist/agent-registry.js +144 -0
  27. package/dist/agent-registry.js.map +1 -0
  28. package/dist/channels/discord.d.ts +17 -0
  29. package/dist/channels/discord.d.ts.map +1 -0
  30. package/dist/channels/discord.js +114 -0
  31. package/dist/channels/discord.js.map +1 -0
  32. package/dist/channels/imessage.d.ts +23 -0
  33. package/dist/channels/imessage.d.ts.map +1 -0
  34. package/dist/channels/imessage.js +214 -0
  35. package/dist/channels/imessage.js.map +1 -0
  36. package/dist/channels/slack.d.ts +19 -0
  37. package/dist/channels/slack.d.ts.map +1 -0
  38. package/dist/channels/slack.js +167 -0
  39. package/dist/channels/slack.js.map +1 -0
  40. package/dist/channels/telegram.d.ts +19 -0
  41. package/dist/channels/telegram.d.ts.map +1 -0
  42. package/dist/channels/telegram.js +274 -0
  43. package/dist/channels/telegram.js.map +1 -0
  44. package/dist/channels/types.d.ts +44 -0
  45. package/dist/channels/types.d.ts.map +1 -0
  46. package/dist/channels/types.js +18 -0
  47. package/dist/channels/types.js.map +1 -0
  48. package/dist/channels/whatsapp.d.ts +23 -0
  49. package/dist/channels/whatsapp.d.ts.map +1 -0
  50. package/dist/channels/whatsapp.js +189 -0
  51. package/dist/channels/whatsapp.js.map +1 -0
  52. package/dist/config.d.ts +134 -0
  53. package/dist/config.d.ts.map +1 -0
  54. package/dist/config.js +127 -0
  55. package/dist/config.js.map +1 -0
  56. package/dist/cron.d.ts +8 -0
  57. package/dist/cron.d.ts.map +1 -0
  58. package/dist/cron.js +35 -0
  59. package/dist/cron.js.map +1 -0
  60. package/dist/decrypt-keys.d.ts +7 -0
  61. package/dist/decrypt-keys.d.ts.map +1 -0
  62. package/dist/decrypt-keys.js +53 -0
  63. package/dist/decrypt-keys.js.map +1 -0
  64. package/dist/encrypt-keys.d.ts +8 -0
  65. package/dist/encrypt-keys.d.ts.map +1 -0
  66. package/dist/encrypt-keys.js +62 -0
  67. package/dist/encrypt-keys.js.map +1 -0
  68. package/dist/executor.d.ts +31 -0
  69. package/dist/executor.d.ts.map +1 -0
  70. package/dist/executor.js +2009 -0
  71. package/dist/executor.js.map +1 -0
  72. package/dist/gemini-executor.d.ts +27 -0
  73. package/dist/gemini-executor.d.ts.map +1 -0
  74. package/dist/gemini-executor.js +160 -0
  75. package/dist/gemini-executor.js.map +1 -0
  76. package/dist/goals.d.ts +24 -0
  77. package/dist/goals.d.ts.map +1 -0
  78. package/dist/goals.js +189 -0
  79. package/dist/goals.js.map +1 -0
  80. package/dist/gym/activity-digest.d.ts +30 -0
  81. package/dist/gym/activity-digest.d.ts.map +1 -0
  82. package/dist/gym/activity-digest.js +506 -0
  83. package/dist/gym/activity-digest.js.map +1 -0
  84. package/dist/gym/dimension-scorer.d.ts +76 -0
  85. package/dist/gym/dimension-scorer.d.ts.map +1 -0
  86. package/dist/gym/dimension-scorer.js +236 -0
  87. package/dist/gym/dimension-scorer.js.map +1 -0
  88. package/dist/gym/gym-router.d.ts +7 -0
  89. package/dist/gym/gym-router.d.ts.map +1 -0
  90. package/dist/gym/gym-router.js +718 -0
  91. package/dist/gym/gym-router.js.map +1 -0
  92. package/dist/gym/index.d.ts +11 -0
  93. package/dist/gym/index.d.ts.map +1 -0
  94. package/dist/gym/index.js +11 -0
  95. package/dist/gym/index.js.map +1 -0
  96. package/dist/heartbeat.d.ts +21 -0
  97. package/dist/heartbeat.d.ts.map +1 -0
  98. package/dist/heartbeat.js +163 -0
  99. package/dist/heartbeat.js.map +1 -0
  100. package/dist/index.d.ts +2 -0
  101. package/dist/index.d.ts.map +1 -0
  102. package/dist/index.js +254 -0
  103. package/dist/index.js.map +1 -0
  104. package/dist/keystore.d.ts +22 -0
  105. package/dist/keystore.d.ts.map +1 -0
  106. package/dist/keystore.js +178 -0
  107. package/dist/keystore.js.map +1 -0
  108. package/dist/logger.d.ts +9 -0
  109. package/dist/logger.d.ts.map +1 -0
  110. package/dist/logger.js +45 -0
  111. package/dist/logger.js.map +1 -0
  112. package/dist/memory/daily.d.ts +22 -0
  113. package/dist/memory/daily.d.ts.map +1 -0
  114. package/dist/memory/daily.js +82 -0
  115. package/dist/memory/daily.js.map +1 -0
  116. package/dist/memory/embeddings.d.ts +15 -0
  117. package/dist/memory/embeddings.d.ts.map +1 -0
  118. package/dist/memory/embeddings.js +154 -0
  119. package/dist/memory/embeddings.js.map +1 -0
  120. package/dist/memory/index.d.ts +32 -0
  121. package/dist/memory/index.d.ts.map +1 -0
  122. package/dist/memory/index.js +159 -0
  123. package/dist/memory/index.js.map +1 -0
  124. package/dist/memory/search.d.ts +21 -0
  125. package/dist/memory/search.d.ts.map +1 -0
  126. package/dist/memory/search.js +77 -0
  127. package/dist/memory/search.js.map +1 -0
  128. package/dist/memory/store.d.ts +23 -0
  129. package/dist/memory/store.d.ts.map +1 -0
  130. package/dist/memory/store.js +144 -0
  131. package/dist/memory/store.js.map +1 -0
  132. package/dist/ollama-executor.d.ts +17 -0
  133. package/dist/ollama-executor.d.ts.map +1 -0
  134. package/dist/ollama-executor.js +112 -0
  135. package/dist/ollama-executor.js.map +1 -0
  136. package/dist/openai-executor.d.ts +38 -0
  137. package/dist/openai-executor.d.ts.map +1 -0
  138. package/dist/openai-executor.js +197 -0
  139. package/dist/openai-executor.js.map +1 -0
  140. package/dist/router.d.ts +11 -0
  141. package/dist/router.d.ts.map +1 -0
  142. package/dist/router.js +185 -0
  143. package/dist/router.js.map +1 -0
  144. package/dist/test-message.d.ts +2 -0
  145. package/dist/test-message.d.ts.map +1 -0
  146. package/dist/test-message.js +60 -0
  147. package/dist/test-message.js.map +1 -0
  148. package/dist/utils/imsg-db-reader.d.ts +24 -0
  149. package/dist/utils/imsg-db-reader.d.ts.map +1 -0
  150. package/dist/utils/imsg-db-reader.js +92 -0
  151. package/dist/utils/imsg-db-reader.js.map +1 -0
  152. package/dist/utils/imsg-rpc.d.ts +25 -0
  153. package/dist/utils/imsg-rpc.d.ts.map +1 -0
  154. package/dist/utils/imsg-rpc.js +149 -0
  155. package/dist/utils/imsg-rpc.js.map +1 -0
  156. package/dist/utils/message-formatter.d.ts +3 -0
  157. package/dist/utils/message-formatter.d.ts.map +1 -0
  158. package/dist/utils/message-formatter.js +69 -0
  159. package/dist/utils/message-formatter.js.map +1 -0
  160. package/dist/web-ui.d.ts +12 -0
  161. package/dist/web-ui.d.ts.map +1 -0
  162. package/dist/web-ui.js +5784 -0
  163. package/dist/web-ui.js.map +1 -0
  164. package/dist/whatsapp-chats.d.ts +2 -0
  165. package/dist/whatsapp-chats.d.ts.map +1 -0
  166. package/dist/whatsapp-chats.js +76 -0
  167. package/dist/whatsapp-chats.js.map +1 -0
  168. package/dist/whatsapp-login.d.ts +2 -0
  169. package/dist/whatsapp-login.d.ts.map +1 -0
  170. package/dist/whatsapp-login.js +90 -0
  171. package/dist/whatsapp-login.js.map +1 -0
  172. package/dist/wiki-sync.d.ts +21 -0
  173. package/dist/wiki-sync.d.ts.map +1 -0
  174. package/dist/wiki-sync.js +147 -0
  175. package/dist/wiki-sync.js.map +1 -0
  176. package/docs/AddNewAgentGuide.md +100 -0
  177. package/docs/AddNewMcpGuide.md +72 -0
  178. package/docs/Architecture.md +795 -0
  179. package/docs/CLAUDE-AI-SETUP.md +166 -0
  180. package/docs/Setup.md +297 -0
  181. package/docs/ai-gym-architecture.md +1040 -0
  182. package/docs/ai-gym-build-plan.md +343 -0
  183. package/docs/ai-gym-onboarding.md +122 -0
  184. package/docs/appcreator_plan.md +348 -0
  185. package/docs/platform-mcp-audit.md +320 -0
  186. package/docs/server-deployment-plan.md +503 -0
  187. package/docs/superpowers/plans/2026-03-25-marketplace.md +1281 -0
  188. package/docs/superpowers/specs/2026-03-25-marketplace-design.md +287 -0
  189. package/docs/user-guide.md +2016 -0
  190. package/mcp-catalog.json +628 -0
  191. package/package.json +63 -0
  192. package/public/MyAIforOne-logomark-512.svg +16 -0
  193. package/public/MyAIforOne-logomark-transparent.svg +15 -0
  194. package/public/activity.html +314 -0
  195. package/public/admin.html +1674 -0
  196. package/public/agent-dashboard.html +670 -0
  197. package/public/api-docs.html +1106 -0
  198. package/public/automations.html +722 -0
  199. package/public/canvas.css +223 -0
  200. package/public/canvas.js +588 -0
  201. package/public/changelog.html +231 -0
  202. package/public/gym.html +2766 -0
  203. package/public/home.html +1930 -0
  204. package/public/index.html +2809 -0
  205. package/public/lab.html +1643 -0
  206. package/public/library.html +1442 -0
  207. package/public/marketplace.html +1101 -0
  208. package/public/mcp-docs.html +441 -0
  209. package/public/mini.html +390 -0
  210. package/public/monitor.html +584 -0
  211. package/public/org.html +4304 -0
  212. package/public/projects.html +734 -0
  213. package/public/settings.html +645 -0
  214. package/public/tasks.html +932 -0
  215. package/public/trainers/alex.svg +12 -0
  216. package/public/trainers/jordan.svg +12 -0
  217. package/public/trainers/morgan.svg +12 -0
  218. package/public/trainers/riley.svg +12 -0
  219. package/public/trainers/sam.svg +12 -0
  220. package/public/user-guide.html +218 -0
  221. package/registry/agents.json +3 -0
  222. package/registry/apps.json +20 -0
  223. package/registry/installed-drafts.json +3 -0
  224. package/registry/mcps.json +1084 -0
  225. package/registry/prompts/personal/mcp-test-prompt.md +6 -0
  226. package/registry/prompts/personal/memory-recall.md +6 -0
  227. package/registry/prompts/platform/brainstorm.md +15 -0
  228. package/registry/prompts/platform/code-review.md +16 -0
  229. package/registry/prompts/platform/explain.md +16 -0
  230. package/registry/prompts.json +58 -0
  231. package/registry/skills/external/brainstorming.md +5 -0
  232. package/registry/skills/external/code-review.md +40 -0
  233. package/registry/skills/external/frontend-patterns.md +642 -0
  234. package/registry/skills/external/frontend-slides.md +184 -0
  235. package/registry/skills/external/systematic-debugging.md +5 -0
  236. package/registry/skills/external/tdd.md +328 -0
  237. package/registry/skills/external/verification-before-completion.md +5 -0
  238. package/registry/skills/external/writing-plans.md +5 -0
  239. package/registry/skills/platform/ai41_app_build.md +930 -0
  240. package/registry/skills/platform/ai41_app_deploy.md +168 -0
  241. package/registry/skills/platform/ai41_app_orchestrator.md +239 -0
  242. package/registry/skills/platform/ai41_app_patterns.md +359 -0
  243. package/registry/skills/platform/ai41_app_register.md +85 -0
  244. package/registry/skills/platform/ai41_app_scaffold.md +421 -0
  245. package/registry/skills/platform/ai41_app_verify.md +107 -0
  246. package/registry/skills/platform/opProjectCreate.md +239 -0
  247. package/registry/skills/platform/op_devbrowser.md +136 -0
  248. package/registry/skills/platform/sop_brandguidelines.md +103 -0
  249. package/registry/skills/platform/sop_docx.md +117 -0
  250. package/registry/skills/platform/sop_frontenddesign.md +44 -0
  251. package/registry/skills/platform/sop_frontenddesign_v2.md +659 -0
  252. package/registry/skills/platform/sop_mcpbuilder.md +133 -0
  253. package/registry/skills/platform/sop_pdf.md +172 -0
  254. package/registry/skills/platform/sop_pptx.md +133 -0
  255. package/registry/skills/platform/sop_skillcreator.md +104 -0
  256. package/registry/skills/platform/sop_themefactory.md +128 -0
  257. package/registry/skills/platform/sop_webapptesting.md +75 -0
  258. package/registry/skills/platform/sop_webartifactsbuilder.md +97 -0
  259. package/registry/skills/platform/sop_xlsx.md +134 -0
  260. package/registry/skills.json +1055 -0
  261. package/scripts/discover-chats.sh +11 -0
  262. package/scripts/install-service-windows.ps1 +87 -0
  263. package/scripts/install-service.sh +52 -0
  264. package/scripts/seed-registry.ts +195 -0
  265. package/scripts/test-send.sh +5 -0
  266. package/scripts/tray-indicator.ps1 +35 -0
  267. package/scripts/uninstall-service-windows.ps1 +23 -0
  268. package/scripts/uninstall-service.sh +15 -0
  269. package/scripts/xbar-myagent.5s.sh +32 -0
  270. package/server/mcp-server/dist/index.d.ts +11 -0
  271. package/server/mcp-server/dist/index.js +1332 -0
  272. package/server/mcp-server/dist/lib/api-client.d.ts +165 -0
  273. package/server/mcp-server/dist/lib/api-client.js +241 -0
  274. package/server/mcp-server/index.ts +1545 -0
  275. package/server/mcp-server/lib/api-client.ts +366 -0
  276. package/server/mcp-server/tsconfig.json +14 -0
  277. package/src/agent-registry.ts +180 -0
  278. package/src/channels/discord.ts +129 -0
  279. package/src/channels/imessage.ts +261 -0
  280. package/src/channels/slack.ts +208 -0
  281. package/src/channels/telegram.ts +307 -0
  282. package/src/channels/types.ts +62 -0
  283. package/src/channels/whatsapp.ts +227 -0
  284. package/src/config.ts +281 -0
  285. package/src/cron.ts +43 -0
  286. package/src/decrypt-keys.ts +60 -0
  287. package/src/encrypt-keys.ts +70 -0
  288. package/src/executor.ts +2190 -0
  289. package/src/gemini-executor.ts +212 -0
  290. package/src/goals.ts +240 -0
  291. package/src/gym/activity-digest.ts +546 -0
  292. package/src/gym/dimension-scorer.ts +297 -0
  293. package/src/gym/gym-router.ts +801 -0
  294. package/src/gym/index.ts +19 -0
  295. package/src/heartbeat.ts +220 -0
  296. package/src/index.ts +275 -0
  297. package/src/keystore.ts +190 -0
  298. package/src/logger.ts +51 -0
  299. package/src/memory/daily.ts +101 -0
  300. package/src/memory/embeddings.ts +185 -0
  301. package/src/memory/index.ts +218 -0
  302. package/src/memory/search.ts +124 -0
  303. package/src/memory/store.ts +189 -0
  304. package/src/ollama-executor.ts +126 -0
  305. package/src/openai-executor.ts +259 -0
  306. package/src/router.ts +230 -0
  307. package/src/test-message.ts +72 -0
  308. package/src/utils/imsg-db-reader.ts +109 -0
  309. package/src/utils/imsg-rpc.ts +178 -0
  310. package/src/utils/message-formatter.ts +90 -0
  311. package/src/web-ui.ts +5778 -0
  312. package/src/whatsapp-chats.ts +91 -0
  313. package/src/whatsapp-login.ts +110 -0
  314. package/src/wiki-sync.ts +199 -0
  315. package/tsconfig.json +19 -0
@@ -0,0 +1,307 @@
1
+ import { writeFileSync, mkdirSync, createReadStream } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { tmpdir } from "node:os";
4
+ import { Bot } from "grammy";
5
+ import type { Context } from "grammy";
6
+ import type { Message, PhotoSize } from "grammy/types";
7
+ import type { ChannelDriver, InboundMessage, OutboundMessage } from "./types.js";
8
+ import { splitText } from "./types.js";
9
+ import { log } from "../logger.js";
10
+ import { InputFile } from "grammy";
11
+
12
+ export class TelegramDriver implements ChannelDriver {
13
+ readonly channelId = "telegram";
14
+
15
+ private bot: Bot;
16
+ private botToken: string;
17
+ private messageHandler: ((msg: InboundMessage) => Promise<void>) | null = null;
18
+ private botId: number | null = null;
19
+
20
+ constructor(config: Record<string, unknown>) {
21
+ const botToken = config.botToken as string;
22
+ if (!botToken) {
23
+ throw new Error("Telegram driver requires botToken in config");
24
+ }
25
+ this.botToken = botToken;
26
+ this.bot = new Bot(botToken);
27
+ }
28
+
29
+ async start(): Promise<void> {
30
+ const me = await this.bot.api.getMe();
31
+ this.botId = me.id;
32
+ log.info(`Telegram bot authenticated as @${me.username} (${me.id})`);
33
+
34
+ this.bot.on("message:text", (ctx) => this.handleMessage(ctx));
35
+ this.bot.on("message:photo", (ctx) => this.handleMessage(ctx));
36
+ this.bot.on("message:voice", (ctx) => this.handleVoiceMessage(ctx));
37
+ this.bot.on("message:audio", (ctx) => this.handleVoiceMessage(ctx));
38
+
39
+ this.bot.catch((err) => {
40
+ log.error(`Telegram bot error: ${err.message}`);
41
+ });
42
+
43
+ this.bot.start({
44
+ onStart: () => {
45
+ log.info("Telegram driver started — listening for messages (long polling)");
46
+ },
47
+ }).catch((err) => {
48
+ // 409 Conflict means another instance is already polling — log and swallow.
49
+ // Any other error is also caught here to prevent crashing the Node process.
50
+ const msg = err instanceof Error ? err.message : String(err);
51
+ if (msg.includes("409")) {
52
+ log.warn(`Telegram polling conflict (409): another instance may be running. Telegram disabled.`);
53
+ } else {
54
+ log.error(`Telegram bot.start() error: ${msg}`);
55
+ }
56
+ });
57
+ }
58
+
59
+ async stop(): Promise<void> {
60
+ await this.bot.stop();
61
+ log.info("Telegram driver stopped");
62
+ }
63
+
64
+ onMessage(handler: (msg: InboundMessage) => Promise<void>): void {
65
+ this.messageHandler = handler;
66
+ }
67
+
68
+ async send(msg: OutboundMessage): Promise<void> {
69
+ const chatId = parseInt(msg.chatId, 10);
70
+ const chunks = splitText(msg.text, 4096);
71
+ for (const chunk of chunks) {
72
+ await this.bot.api.sendMessage(chatId, chunk, {
73
+ ...(msg.replyToId ? { reply_parameters: { message_id: parseInt(msg.replyToId, 10) } } : {}),
74
+ });
75
+ }
76
+ }
77
+
78
+ async sendTyping(chatId: string): Promise<void> {
79
+ try {
80
+ await this.bot.api.sendChatAction(parseInt(chatId, 10), "typing");
81
+ } catch { /* ignore */ }
82
+ }
83
+
84
+ async sendFile(chatId: string, filePath: string, caption?: string): Promise<void> {
85
+ try {
86
+ const numericId = parseInt(chatId, 10);
87
+ const ext = filePath.split(".").pop()?.toLowerCase() || "";
88
+ const imageExts = ["png", "jpg", "jpeg", "gif", "webp"];
89
+
90
+ if (imageExts.includes(ext)) {
91
+ await this.bot.api.sendPhoto(numericId, new InputFile(filePath), {
92
+ caption: caption || undefined,
93
+ });
94
+ } else {
95
+ await this.bot.api.sendDocument(numericId, new InputFile(filePath), {
96
+ caption: caption || undefined,
97
+ });
98
+ }
99
+ } catch (err) {
100
+ log.warn(`Failed to send file to Telegram: ${err}`);
101
+ }
102
+ }
103
+
104
+ private async handleMessage(ctx: Context): Promise<void> {
105
+ const msg = ctx.message;
106
+ if (!msg) return;
107
+ if (msg.from?.id === this.botId) return;
108
+ if (msg.from?.is_bot) return;
109
+
110
+ const chatId = msg.chat.id.toString();
111
+ const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
112
+ const sender = msg.from?.id.toString() || "unknown";
113
+ const senderName = buildSenderName(msg.from);
114
+ const text = msg.text || msg.caption || "";
115
+
116
+ const attachments = await this.downloadPhotos(msg);
117
+
118
+ if (!text.trim() && attachments.length === 0) return;
119
+
120
+ let replyTo: InboundMessage["replyTo"] | undefined;
121
+ if (msg.reply_to_message) {
122
+ replyTo = {
123
+ id: msg.reply_to_message.message_id.toString(),
124
+ text: msg.reply_to_message.text || msg.reply_to_message.caption || "",
125
+ sender: msg.reply_to_message.from?.id.toString(),
126
+ };
127
+ }
128
+
129
+ const inbound: InboundMessage = {
130
+ id: msg.message_id.toString(),
131
+ channel: this.channelId,
132
+ chatId,
133
+ chatType: isGroup ? "group" : "dm",
134
+ sender,
135
+ senderName,
136
+ text,
137
+ timestamp: msg.date * 1000,
138
+ isFromMe: false,
139
+ isGroup,
140
+ groupName: isGroup ? (msg.chat as any).title : undefined,
141
+ replyTo,
142
+ attachments: attachments.length > 0 ? attachments : undefined,
143
+ raw: msg,
144
+ };
145
+
146
+ log.debug(
147
+ `Telegram received from ${senderName} (${sender}) in ${chatId}: ${text.slice(0, 100)}${attachments.length ? ` [${attachments.length} photo(s)]` : ""}`,
148
+ );
149
+
150
+ if (this.messageHandler) {
151
+ this.messageHandler(inbound).catch((err) => {
152
+ log.error(`Telegram message handler error: ${err}`);
153
+ });
154
+ }
155
+ }
156
+
157
+ // Feature 8: Voice note handling — download audio, transcribe if configured
158
+ private async handleVoiceMessage(ctx: Context): Promise<void> {
159
+ const msg = ctx.message;
160
+ if (!msg) return;
161
+ if (msg.from?.id === this.botId) return;
162
+ if (msg.from?.is_bot) return;
163
+
164
+ const voice = msg.voice || msg.audio;
165
+ if (!voice) return;
166
+
167
+ const chatId = msg.chat.id.toString();
168
+ const isGroup = msg.chat.type === "group" || msg.chat.type === "supergroup";
169
+ const sender = msg.from?.id.toString() || "unknown";
170
+ const senderName = buildSenderName(msg.from);
171
+
172
+ try {
173
+ const file = await this.bot.api.getFile(voice.file_id);
174
+ if (!file.file_path) return;
175
+
176
+ const url = `https://api.telegram.org/file/bot${this.botToken}/${file.file_path}`;
177
+ const resp = await fetch(url);
178
+ if (!resp.ok) return;
179
+
180
+ const buffer = Buffer.from(await resp.arrayBuffer());
181
+ const ext = file.file_path.split(".").pop() || "ogg";
182
+ const downloadDir = join(tmpdir(), "channelToAgent-telegram-voice");
183
+ mkdirSync(downloadDir, { recursive: true });
184
+ const localPath = join(downloadDir, `${msg.message_id}.${ext}`);
185
+ writeFileSync(localPath, buffer);
186
+
187
+ // Transcribe via OpenAI Whisper API if key is available
188
+ const transcription = await transcribeAudio(localPath);
189
+ if (!transcription) {
190
+ log.debug(`Voice message from ${senderName} — no transcription (no OPENAI_API_KEY)`);
191
+ return;
192
+ }
193
+
194
+ log.debug(`Transcribed voice from ${senderName}: ${transcription.slice(0, 100)}`);
195
+
196
+ const inbound: InboundMessage = {
197
+ id: msg.message_id.toString(),
198
+ channel: this.channelId,
199
+ chatId,
200
+ chatType: isGroup ? "group" : "dm",
201
+ sender,
202
+ senderName,
203
+ text: `[Voice message transcription]: ${transcription}`,
204
+ timestamp: msg.date * 1000,
205
+ isFromMe: false,
206
+ isGroup,
207
+ groupName: isGroup ? (msg.chat as any).title : undefined,
208
+ raw: msg,
209
+ };
210
+
211
+ if (this.messageHandler) {
212
+ this.messageHandler(inbound).catch((err) => {
213
+ log.error(`Telegram voice handler error: ${err}`);
214
+ });
215
+ }
216
+ } catch (err) {
217
+ log.warn(`Error handling voice message: ${err}`);
218
+ }
219
+ }
220
+
221
+ private async downloadPhotos(msg: Message): Promise<Array<{ path: string; mimeType?: string }>> {
222
+ if (!msg.photo?.length) return [];
223
+
224
+ const downloadDir = join(tmpdir(), "channelToAgent-telegram-images");
225
+ mkdirSync(downloadDir, { recursive: true });
226
+
227
+ const results: Array<{ path: string; mimeType?: string }> = [];
228
+
229
+ const largest = msg.photo.reduce((a: PhotoSize, b: PhotoSize) =>
230
+ (a.file_size || 0) > (b.file_size || 0) ? a : b,
231
+ );
232
+
233
+ try {
234
+ const file = await this.bot.api.getFile(largest.file_id);
235
+ if (!file.file_path) {
236
+ log.warn(`No file_path for Telegram photo ${largest.file_id}`);
237
+ return results;
238
+ }
239
+
240
+ const url = `https://api.telegram.org/file/bot${this.botToken}/${file.file_path}`;
241
+ const resp = await fetch(url);
242
+ if (!resp.ok) {
243
+ log.warn(`Failed to download Telegram photo: ${resp.status}`);
244
+ return results;
245
+ }
246
+
247
+ const buffer = Buffer.from(await resp.arrayBuffer());
248
+ if (buffer.length > 10_000_000) {
249
+ log.warn(`Skipping oversized Telegram photo (${buffer.length} bytes)`);
250
+ return results;
251
+ }
252
+
253
+ const ext = file.file_path.split(".").pop() || "jpg";
254
+ const localPath = join(downloadDir, `${msg.message_id}-${largest.file_id}.${ext}`);
255
+ writeFileSync(localPath, buffer);
256
+ results.push({ path: localPath, mimeType: `image/${ext === "jpg" ? "jpeg" : ext}` });
257
+ log.debug(`Downloaded Telegram photo: ${localPath} (${buffer.length} bytes)`);
258
+ } catch (err) {
259
+ log.warn(`Error downloading Telegram photo: ${err}`);
260
+ }
261
+
262
+ return results;
263
+ }
264
+ }
265
+
266
+ // ─── Helpers ─────────────────────────────────────────────────────────
267
+
268
+ function buildSenderName(from?: { first_name?: string; last_name?: string; username?: string }): string {
269
+ if (!from) return "unknown";
270
+ const parts = [from.first_name, from.last_name].filter(Boolean);
271
+ if (parts.length > 0) return parts.join(" ");
272
+ return from.username || "unknown";
273
+ }
274
+
275
+ // Feature 8: Voice transcription via OpenAI Whisper API
276
+ async function transcribeAudio(filePath: string): Promise<string | null> {
277
+ const apiKey = process.env.OPENAI_API_KEY;
278
+ if (!apiKey) return null;
279
+
280
+ try {
281
+ const { readFileSync } = await import("node:fs");
282
+ const { basename } = await import("node:path");
283
+ const audioData = readFileSync(filePath);
284
+ const fileName = basename(filePath);
285
+
286
+ const formData = new FormData();
287
+ formData.append("file", new Blob([audioData]), fileName);
288
+ formData.append("model", "whisper-1");
289
+
290
+ const resp = await fetch("https://api.openai.com/v1/audio/transcriptions", {
291
+ method: "POST",
292
+ headers: { Authorization: `Bearer ${apiKey}` },
293
+ body: formData,
294
+ });
295
+
296
+ if (!resp.ok) {
297
+ log.warn(`Whisper API error: ${resp.status}`);
298
+ return null;
299
+ }
300
+
301
+ const result = await resp.json() as { text: string };
302
+ return result.text;
303
+ } catch (err) {
304
+ log.warn(`Transcription failed: ${err}`);
305
+ return null;
306
+ }
307
+ }
@@ -0,0 +1,62 @@
1
+ export interface InboundMessage {
2
+ id: string;
3
+ channel: string;
4
+ chatId: string;
5
+ chatType: "dm" | "group";
6
+ sender: string;
7
+ senderName?: string;
8
+ text: string;
9
+ timestamp: number;
10
+ isFromMe: boolean;
11
+ isGroup: boolean;
12
+ groupName?: string;
13
+ participants?: string[];
14
+ replyTo?: {
15
+ id: string;
16
+ text: string;
17
+ sender?: string;
18
+ };
19
+ attachments?: Array<{
20
+ path: string;
21
+ mimeType?: string;
22
+ }>;
23
+ raw: unknown;
24
+ }
25
+
26
+ export interface OutboundMessage {
27
+ text: string;
28
+ chatId: string;
29
+ replyToId?: string;
30
+ attachments?: Array<{
31
+ path: string;
32
+ mimeType?: string;
33
+ }>;
34
+ }
35
+
36
+ export interface ChannelDriver {
37
+ readonly channelId: string;
38
+ start(): Promise<void>;
39
+ stop(): Promise<void>;
40
+ onMessage(handler: (msg: InboundMessage) => Promise<void>): void;
41
+ send(msg: OutboundMessage): Promise<void>;
42
+ sendTyping?(chatId: string): Promise<void>;
43
+ sendFile?(chatId: string, filePath: string, caption?: string): Promise<void>;
44
+ }
45
+
46
+ // ─── Shared utilities ────────────────────────────────────────────────
47
+
48
+ export function splitText(text: string, maxLen: number): string[] {
49
+ const chunks: string[] = [];
50
+ let remaining = text;
51
+ while (remaining.length > 0) {
52
+ if (remaining.length <= maxLen) {
53
+ chunks.push(remaining);
54
+ break;
55
+ }
56
+ let splitAt = remaining.lastIndexOf("\n", maxLen);
57
+ if (splitAt < maxLen * 0.5) splitAt = maxLen;
58
+ chunks.push(remaining.slice(0, splitAt));
59
+ remaining = remaining.slice(splitAt);
60
+ }
61
+ return chunks;
62
+ }
@@ -0,0 +1,227 @@
1
+ import {
2
+ makeWASocket,
3
+ useMultiFileAuthState,
4
+ fetchLatestBaileysVersion,
5
+ makeCacheableSignalKeyStore,
6
+ DisconnectReason,
7
+ type WASocket,
8
+ type BaileysEventMap,
9
+ } from "@whiskeysockets/baileys";
10
+ import { Boom } from "@hapi/boom";
11
+ import { mkdirSync } from "node:fs";
12
+ import { resolve } from "node:path";
13
+ import type { ChannelDriver, InboundMessage, OutboundMessage } from "./types.js";
14
+ import { log } from "../logger.js";
15
+
16
+ // Baileys is noisy — suppress its internal logger
17
+ const silentLogger = {
18
+ level: "silent" as const,
19
+ child: () => silentLogger,
20
+ trace: () => {},
21
+ debug: () => {},
22
+ info: () => {},
23
+ warn: () => {},
24
+ error: () => {},
25
+ fatal: () => {},
26
+ };
27
+
28
+ export class WhatsAppDriver implements ChannelDriver {
29
+ readonly channelId = "whatsapp";
30
+
31
+ private sock: WASocket | null = null;
32
+ private messageHandler: ((msg: InboundMessage) => Promise<void>) | null = null;
33
+ private authDir: string;
34
+ private selfId: string | null = null;
35
+ private retryCount = 0;
36
+ private maxRetries = 5;
37
+ private stopping = false;
38
+ /** Track message IDs sent by this socket so we don't echo agent replies */
39
+ private sentIds = new Set<string>();
40
+
41
+ constructor(config: Record<string, unknown>) {
42
+ const baseAuthDir = (config.authDir as string) ?? "./data/whatsapp-auth";
43
+ this.authDir = resolve(baseAuthDir);
44
+ mkdirSync(this.authDir, { recursive: true });
45
+ }
46
+
47
+ async start(): Promise<void> {
48
+ this.stopping = false;
49
+ await this.connect();
50
+ }
51
+
52
+ async stop(): Promise<void> {
53
+ this.stopping = true;
54
+ if (this.sock) {
55
+ this.sock.ev.removeAllListeners("messages.upsert");
56
+ this.sock.ev.removeAllListeners("connection.update");
57
+ this.sock.end(undefined);
58
+ this.sock = null;
59
+ }
60
+ log.info("WhatsApp driver stopped");
61
+ }
62
+
63
+ onMessage(handler: (msg: InboundMessage) => Promise<void>): void {
64
+ this.messageHandler = handler;
65
+ }
66
+
67
+ async send(msg: OutboundMessage): Promise<void> {
68
+ if (!this.sock) throw new Error("WhatsApp not connected");
69
+ const sent = await this.sock.sendMessage(msg.chatId, { text: msg.text });
70
+ if (sent?.key?.id) this.sentIds.add(sent.key.id);
71
+ }
72
+
73
+ private async connect(): Promise<void> {
74
+ const { state, saveCreds } = await useMultiFileAuthState(this.authDir);
75
+ const { version } = await fetchLatestBaileysVersion();
76
+
77
+ this.sock = makeWASocket({
78
+ auth: {
79
+ creds: state.creds,
80
+ keys: makeCacheableSignalKeyStore(state.keys, silentLogger as any),
81
+ },
82
+ version,
83
+ logger: silentLogger as any,
84
+ printQRInTerminal: false,
85
+ browser: ["channelToAgentToClaude", "Desktop", "1.0.0"],
86
+ syncFullHistory: false,
87
+ markOnlineOnConnect: false,
88
+ });
89
+
90
+ // Save credentials on update
91
+ this.sock.ev.on("creds.update", saveCreds);
92
+
93
+ // Handle connection state changes
94
+ this.sock.ev.on("connection.update", (update) => {
95
+ this.handleConnectionUpdate(update, saveCreds);
96
+ });
97
+
98
+ // Handle incoming messages
99
+ this.sock.ev.on("messages.upsert", (upsert) => {
100
+ this.handleMessagesUpsert(upsert);
101
+ });
102
+ }
103
+
104
+ private handleConnectionUpdate(
105
+ update: Partial<BaileysEventMap["connection.update"]>,
106
+ saveCreds: () => Promise<void>,
107
+ ): void {
108
+ const { connection, lastDisconnect, qr } = update;
109
+
110
+ if (qr) {
111
+ // Display QR code — this should only happen during initial login
112
+ log.warn("WhatsApp QR code generated — run the login script first: npm run whatsapp-login");
113
+ log.warn("QR code cannot be displayed from the background service");
114
+ }
115
+
116
+ if (connection === "open") {
117
+ this.selfId = this.sock?.user?.id ?? null;
118
+ this.retryCount = 0;
119
+ log.info(`WhatsApp connected as ${this.sock?.user?.name ?? this.selfId}`);
120
+ }
121
+
122
+ if (connection === "close") {
123
+ const reason = (lastDisconnect?.error as Boom)?.output?.statusCode;
124
+
125
+ if (reason === DisconnectReason.loggedOut) {
126
+ log.error("WhatsApp logged out — credentials cleared. Run: npm run whatsapp-login");
127
+ return;
128
+ }
129
+
130
+ if (reason === 515) {
131
+ // Restart requested by WhatsApp
132
+ log.info("WhatsApp requested restart, reconnecting...");
133
+ this.reconnect();
134
+ return;
135
+ }
136
+
137
+ if (!this.stopping && this.retryCount < this.maxRetries) {
138
+ this.retryCount++;
139
+ const delay = Math.min(5000 * Math.pow(2, this.retryCount - 1), 300_000);
140
+ log.warn(`WhatsApp disconnected (reason: ${reason}), reconnecting in ${delay}ms (attempt ${this.retryCount}/${this.maxRetries})`);
141
+ setTimeout(() => this.reconnect(), delay);
142
+ } else if (!this.stopping) {
143
+ log.error(`WhatsApp disconnected after ${this.maxRetries} retries, giving up`);
144
+ }
145
+ }
146
+ }
147
+
148
+ private async reconnect(): Promise<void> {
149
+ if (this.sock) {
150
+ this.sock.ev.removeAllListeners("messages.upsert");
151
+ this.sock.ev.removeAllListeners("connection.update");
152
+ this.sock.ev.removeAllListeners("creds.update");
153
+ this.sock.end(undefined);
154
+ this.sock = null;
155
+ }
156
+ await this.connect();
157
+ }
158
+
159
+ private handleMessagesUpsert(upsert: BaileysEventMap["messages.upsert"]): void {
160
+ if (upsert.type !== "notify") return;
161
+
162
+ for (const msg of upsert.messages) {
163
+ // Skip status broadcasts
164
+ if (msg.key.remoteJid === "status@broadcast") continue;
165
+
166
+ // Skip messages sent by this socket (agent replies) to avoid echo loops.
167
+ // But do NOT skip all fromMe — the user's phone shares the same WhatsApp
168
+ // account as this linked device, so user messages also appear as fromMe.
169
+ const msgId = msg.key.id || "";
170
+ if (this.sentIds.has(msgId)) {
171
+ this.sentIds.delete(msgId);
172
+ continue;
173
+ }
174
+
175
+ // Extract text from various message types
176
+ const text =
177
+ msg.message?.conversation ||
178
+ msg.message?.extendedTextMessage?.text ||
179
+ msg.message?.imageMessage?.caption ||
180
+ msg.message?.videoMessage?.caption ||
181
+ null;
182
+
183
+ if (!text?.trim()) continue;
184
+
185
+ const remoteJid = msg.key.remoteJid!;
186
+ const isGroup = remoteJid.endsWith("@g.us");
187
+ const sender = isGroup
188
+ ? msg.key.participant || remoteJid
189
+ : remoteJid;
190
+
191
+ // Extract reply context
192
+ const quotedMsg = msg.message?.extendedTextMessage?.contextInfo;
193
+ const replyTo = quotedMsg?.quotedMessage
194
+ ? {
195
+ id: quotedMsg.stanzaId || "",
196
+ text:
197
+ quotedMsg.quotedMessage.conversation ||
198
+ quotedMsg.quotedMessage.extendedTextMessage?.text ||
199
+ "",
200
+ sender: quotedMsg.participant || undefined,
201
+ }
202
+ : undefined;
203
+
204
+ const inbound: InboundMessage = {
205
+ id: msg.key.id || String(Date.now()),
206
+ channel: this.channelId,
207
+ chatId: remoteJid,
208
+ chatType: isGroup ? "group" : "dm",
209
+ sender,
210
+ senderName: msg.pushName || undefined,
211
+ text,
212
+ timestamp: (msg.messageTimestamp as number) * 1000,
213
+ isFromMe: false,
214
+ isGroup,
215
+ groupName: undefined,
216
+ replyTo,
217
+ raw: msg,
218
+ };
219
+
220
+ if (this.messageHandler) {
221
+ this.messageHandler(inbound).catch((err) => {
222
+ log.error(`WhatsApp message handler error: ${err}`);
223
+ });
224
+ }
225
+ }
226
+ }
227
+ }