verybot 0.1.8

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 (277) hide show
  1. package/README.md +167 -0
  2. package/dist/aliases/store.d.ts +21 -0
  3. package/dist/aliases/store.js +148 -0
  4. package/dist/aliases/types.d.ts +6 -0
  5. package/dist/aliases/types.js +1 -0
  6. package/dist/brain/agent-registry.d.ts +96 -0
  7. package/dist/brain/agent-registry.js +141 -0
  8. package/dist/brain/agent.d.ts +167 -0
  9. package/dist/brain/agent.js +932 -0
  10. package/dist/brain/channel-store.d.ts +27 -0
  11. package/dist/brain/channel-store.js +78 -0
  12. package/dist/brain/compaction.d.ts +37 -0
  13. package/dist/brain/compaction.js +214 -0
  14. package/dist/brain/context.d.ts +43 -0
  15. package/dist/brain/context.js +139 -0
  16. package/dist/brain/delegation-store.d.ts +33 -0
  17. package/dist/brain/delegation-store.js +106 -0
  18. package/dist/brain/loop.d.ts +24 -0
  19. package/dist/brain/loop.js +318 -0
  20. package/dist/brain/mcp-adapter.d.ts +43 -0
  21. package/dist/brain/mcp-adapter.js +244 -0
  22. package/dist/brain/memory-extractor.d.ts +26 -0
  23. package/dist/brain/memory-extractor.js +82 -0
  24. package/dist/brain/providers.d.ts +14 -0
  25. package/dist/brain/providers.js +85 -0
  26. package/dist/brain/queue.d.ts +18 -0
  27. package/dist/brain/queue.js +111 -0
  28. package/dist/brain/run-tools.d.ts +50 -0
  29. package/dist/brain/run-tools.js +136 -0
  30. package/dist/brain/session-key.d.ts +23 -0
  31. package/dist/brain/session-key.js +41 -0
  32. package/dist/brain/session-state.d.ts +36 -0
  33. package/dist/brain/session-state.js +51 -0
  34. package/dist/brain/session-store.d.ts +50 -0
  35. package/dist/brain/session-store.js +207 -0
  36. package/dist/brain/session.d.ts +32 -0
  37. package/dist/brain/session.js +75 -0
  38. package/dist/brain/task-subscriber.d.ts +56 -0
  39. package/dist/brain/task-subscriber.js +317 -0
  40. package/dist/brain/user-content.d.ts +16 -0
  41. package/dist/brain/user-content.js +32 -0
  42. package/dist/brain/utils.d.ts +4 -0
  43. package/dist/brain/utils.js +26 -0
  44. package/dist/brain/worker-coordinator.d.ts +25 -0
  45. package/dist/brain/worker-coordinator.js +83 -0
  46. package/dist/channels/commands.d.ts +50 -0
  47. package/dist/channels/commands.js +132 -0
  48. package/dist/channels/discord/channel.d.ts +29 -0
  49. package/dist/channels/discord/channel.js +159 -0
  50. package/dist/channels/discord/markdown.d.ts +19 -0
  51. package/dist/channels/discord/markdown.js +62 -0
  52. package/dist/channels/manager.d.ts +29 -0
  53. package/dist/channels/manager.js +100 -0
  54. package/dist/channels/slack/channel.d.ts +37 -0
  55. package/dist/channels/slack/channel.js +227 -0
  56. package/dist/channels/slack/markdown.d.ts +19 -0
  57. package/dist/channels/slack/markdown.js +62 -0
  58. package/dist/channels/specs.d.ts +32 -0
  59. package/dist/channels/specs.js +99 -0
  60. package/dist/channels/telegram/channel.d.ts +29 -0
  61. package/dist/channels/telegram/channel.js +182 -0
  62. package/dist/channels/telegram/markdown.d.ts +17 -0
  63. package/dist/channels/telegram/markdown.js +66 -0
  64. package/dist/channels/types.d.ts +26 -0
  65. package/dist/channels/types.js +1 -0
  66. package/dist/channels/whatsapp/channel.d.ts +34 -0
  67. package/dist/channels/whatsapp/channel.js +276 -0
  68. package/dist/channels/whatsapp/markdown.d.ts +20 -0
  69. package/dist/channels/whatsapp/markdown.js +51 -0
  70. package/dist/cli/claude-login.d.ts +5 -0
  71. package/dist/cli/claude-login.js +47 -0
  72. package/dist/cli/config.d.ts +5 -0
  73. package/dist/cli/config.js +78 -0
  74. package/dist/cli/index.d.ts +11 -0
  75. package/dist/cli/index.js +96 -0
  76. package/dist/computer/browser/actions.d.ts +31 -0
  77. package/dist/computer/browser/actions.js +148 -0
  78. package/dist/computer/browser/context-manager.d.ts +28 -0
  79. package/dist/computer/browser/context-manager.js +78 -0
  80. package/dist/computer/browser/manager.d.ts +91 -0
  81. package/dist/computer/browser/manager.js +344 -0
  82. package/dist/computer/browser/profile-badge.d.ts +13 -0
  83. package/dist/computer/browser/profile-badge.js +67 -0
  84. package/dist/computer/browser/screenshot.d.ts +5 -0
  85. package/dist/computer/browser/screenshot.js +21 -0
  86. package/dist/computer/browser/snapshot.d.ts +30 -0
  87. package/dist/computer/browser/snapshot.js +242 -0
  88. package/dist/computer/browser/tools.d.ts +5 -0
  89. package/dist/computer/browser/tools.js +167 -0
  90. package/dist/computer/browser/types.d.ts +26 -0
  91. package/dist/computer/browser/types.js +1 -0
  92. package/dist/computer/desktop/adapter.d.ts +25 -0
  93. package/dist/computer/desktop/adapter.js +11 -0
  94. package/dist/computer/desktop/macos.d.ts +24 -0
  95. package/dist/computer/desktop/macos.js +223 -0
  96. package/dist/computer/desktop/tools.d.ts +25 -0
  97. package/dist/computer/desktop/tools.js +114 -0
  98. package/dist/config/agent-config.d.ts +55 -0
  99. package/dist/config/agent-config.js +16 -0
  100. package/dist/config/model-catalog.d.ts +22 -0
  101. package/dist/config/model-catalog.js +112 -0
  102. package/dist/config/model-spec.d.ts +8 -0
  103. package/dist/config/model-spec.js +66 -0
  104. package/dist/config/store.d.ts +25 -0
  105. package/dist/config/store.js +143 -0
  106. package/dist/config.d.ts +110 -0
  107. package/dist/config.js +259 -0
  108. package/dist/control-ui/assets/index-Cbl7G5Sc.css +1 -0
  109. package/dist/control-ui/assets/index-Cu1P4C62.js +266 -0
  110. package/dist/control-ui/assets/noto-sans-cyrillic-ext-wght-normal-DSNfmdVt.woff2 +0 -0
  111. package/dist/control-ui/assets/noto-sans-cyrillic-wght-normal-B2hlT84T.woff2 +0 -0
  112. package/dist/control-ui/assets/noto-sans-devanagari-wght-normal-Cv-Vwajv.woff2 +0 -0
  113. package/dist/control-ui/assets/noto-sans-greek-ext-wght-normal-12T8GTDR.woff2 +0 -0
  114. package/dist/control-ui/assets/noto-sans-greek-wght-normal-Ymb6dZNd.woff2 +0 -0
  115. package/dist/control-ui/assets/noto-sans-latin-ext-wght-normal-W1qJv59z.woff2 +0 -0
  116. package/dist/control-ui/assets/noto-sans-latin-wght-normal-BYSzYMf3.woff2 +0 -0
  117. package/dist/control-ui/assets/noto-sans-vietnamese-wght-normal-DLTJy58D.woff2 +0 -0
  118. package/dist/control-ui/index.html +14 -0
  119. package/dist/control-ui/vite.svg +1 -0
  120. package/dist/events.d.ts +2 -0
  121. package/dist/events.js +11 -0
  122. package/dist/gateway/broadcast.d.ts +5 -0
  123. package/dist/gateway/broadcast.js +33 -0
  124. package/dist/gateway/methods/aliases.d.ts +17 -0
  125. package/dist/gateway/methods/aliases.js +22 -0
  126. package/dist/gateway/methods/chat.d.ts +33 -0
  127. package/dist/gateway/methods/chat.js +37 -0
  128. package/dist/gateway/methods/config.d.ts +14 -0
  129. package/dist/gateway/methods/config.js +24 -0
  130. package/dist/gateway/methods/models.d.ts +10 -0
  131. package/dist/gateway/methods/models.js +14 -0
  132. package/dist/gateway/methods/playbooks.d.ts +45 -0
  133. package/dist/gateway/methods/playbooks.js +488 -0
  134. package/dist/gateway/methods/prompt-templates.d.ts +27 -0
  135. package/dist/gateway/methods/prompt-templates.js +106 -0
  136. package/dist/gateway/methods/scheduler.d.ts +62 -0
  137. package/dist/gateway/methods/scheduler.js +129 -0
  138. package/dist/gateway/methods/sessions.d.ts +44 -0
  139. package/dist/gateway/methods/sessions.js +111 -0
  140. package/dist/gateway/methods/system.d.ts +12 -0
  141. package/dist/gateway/methods/system.js +39 -0
  142. package/dist/gateway/methods/tasks.d.ts +40 -0
  143. package/dist/gateway/methods/tasks.js +151 -0
  144. package/dist/gateway/methods/teams.d.ts +69 -0
  145. package/dist/gateway/methods/teams.js +376 -0
  146. package/dist/gateway/methods/tools.d.ts +6 -0
  147. package/dist/gateway/methods/tools.js +7 -0
  148. package/dist/gateway/methods/whatsapp.d.ts +19 -0
  149. package/dist/gateway/methods/whatsapp.js +35 -0
  150. package/dist/gateway/rpc.d.ts +38 -0
  151. package/dist/gateway/rpc.js +79 -0
  152. package/dist/gateway/server.d.ts +9 -0
  153. package/dist/gateway/server.js +137 -0
  154. package/dist/index.d.ts +1 -0
  155. package/dist/index.js +254 -0
  156. package/dist/integrations/github.d.ts +7 -0
  157. package/dist/integrations/github.js +133 -0
  158. package/dist/integrations/mcp.d.ts +7 -0
  159. package/dist/integrations/mcp.js +106 -0
  160. package/dist/integrations/registry.d.ts +47 -0
  161. package/dist/integrations/registry.js +332 -0
  162. package/dist/integrations/scanner.d.ts +10 -0
  163. package/dist/integrations/scanner.js +122 -0
  164. package/dist/integrations/twitter.d.ts +10 -0
  165. package/dist/integrations/twitter.js +120 -0
  166. package/dist/integrations/types.d.ts +72 -0
  167. package/dist/integrations/types.js +1 -0
  168. package/dist/logger.d.ts +16 -0
  169. package/dist/logger.js +104 -0
  170. package/dist/markdown/chunk.d.ts +9 -0
  171. package/dist/markdown/chunk.js +52 -0
  172. package/dist/markdown/ir.d.ts +37 -0
  173. package/dist/markdown/ir.js +529 -0
  174. package/dist/markdown/render.d.ts +22 -0
  175. package/dist/markdown/render.js +148 -0
  176. package/dist/markdown/table-render.d.ts +43 -0
  177. package/dist/markdown/table-render.js +219 -0
  178. package/dist/markdown/tables.d.ts +17 -0
  179. package/dist/markdown/tables.js +27 -0
  180. package/dist/memory/embedding.d.ts +16 -0
  181. package/dist/memory/embedding.js +66 -0
  182. package/dist/memory/explicit.d.ts +16 -0
  183. package/dist/memory/explicit.js +29 -0
  184. package/dist/memory/extractor.d.ts +13 -0
  185. package/dist/memory/extractor.js +82 -0
  186. package/dist/memory/search.d.ts +15 -0
  187. package/dist/memory/search.js +57 -0
  188. package/dist/memory/session-learning.d.ts +23 -0
  189. package/dist/memory/session-learning.js +55 -0
  190. package/dist/memory/store.d.ts +36 -0
  191. package/dist/memory/store.js +334 -0
  192. package/dist/memory/types.d.ts +9 -0
  193. package/dist/memory/types.js +2 -0
  194. package/dist/paths.d.ts +28 -0
  195. package/dist/paths.js +48 -0
  196. package/dist/prompt-templates/builtins/index.d.ts +4 -0
  197. package/dist/prompt-templates/builtins/index.js +5 -0
  198. package/dist/prompt-templates/builtins/planner.d.ts +4 -0
  199. package/dist/prompt-templates/builtins/planner.js +77 -0
  200. package/dist/prompt-templates/store.d.ts +45 -0
  201. package/dist/prompt-templates/store.js +224 -0
  202. package/dist/prompt-templates/types.d.ts +10 -0
  203. package/dist/prompt-templates/types.js +1 -0
  204. package/dist/scheduler/connected-channels.d.ts +24 -0
  205. package/dist/scheduler/connected-channels.js +57 -0
  206. package/dist/scheduler/scheduler.d.ts +22 -0
  207. package/dist/scheduler/scheduler.js +132 -0
  208. package/dist/scheduler/store.d.ts +27 -0
  209. package/dist/scheduler/store.js +205 -0
  210. package/dist/scheduler/types.d.ts +29 -0
  211. package/dist/scheduler/types.js +1 -0
  212. package/dist/security/command-validator.d.ts +22 -0
  213. package/dist/security/command-validator.js +160 -0
  214. package/dist/security/docker-sandbox.d.ts +48 -0
  215. package/dist/security/docker-sandbox.js +218 -0
  216. package/dist/security/env-filter.d.ts +8 -0
  217. package/dist/security/env-filter.js +41 -0
  218. package/dist/skills/loader.d.ts +33 -0
  219. package/dist/skills/loader.js +132 -0
  220. package/dist/skills/prompt.d.ts +6 -0
  221. package/dist/skills/prompt.js +17 -0
  222. package/dist/skills/read-tool.d.ts +7 -0
  223. package/dist/skills/read-tool.js +24 -0
  224. package/dist/skills/scanner.d.ts +6 -0
  225. package/dist/skills/scanner.js +73 -0
  226. package/dist/skills/types.d.ts +15 -0
  227. package/dist/skills/types.js +1 -0
  228. package/dist/tasks/inline-attachment-content.d.ts +9 -0
  229. package/dist/tasks/inline-attachment-content.js +64 -0
  230. package/dist/tasks/store.d.ts +112 -0
  231. package/dist/tasks/store.js +519 -0
  232. package/dist/tasks/types.d.ts +129 -0
  233. package/dist/tasks/types.js +80 -0
  234. package/dist/teams/status-config.d.ts +8 -0
  235. package/dist/teams/status-config.js +40 -0
  236. package/dist/teams/store.d.ts +111 -0
  237. package/dist/teams/store.js +671 -0
  238. package/dist/teams/types.d.ts +30 -0
  239. package/dist/teams/types.js +1 -0
  240. package/dist/tools/bash.d.ts +18 -0
  241. package/dist/tools/bash.js +64 -0
  242. package/dist/tools/channel-history.d.ts +10 -0
  243. package/dist/tools/channel-history.js +43 -0
  244. package/dist/tools/delegate.d.ts +20 -0
  245. package/dist/tools/delegate.js +299 -0
  246. package/dist/tools/fs.d.ts +4 -0
  247. package/dist/tools/fs.js +335 -0
  248. package/dist/tools/integration-toggle.d.ts +14 -0
  249. package/dist/tools/integration-toggle.js +47 -0
  250. package/dist/tools/memory.d.ts +13 -0
  251. package/dist/tools/memory.js +59 -0
  252. package/dist/tools/prompt-templates.d.ts +7 -0
  253. package/dist/tools/prompt-templates.js +133 -0
  254. package/dist/tools/registry.d.ts +6 -0
  255. package/dist/tools/registry.js +9 -0
  256. package/dist/tools/schedule.d.ts +8 -0
  257. package/dist/tools/schedule.js +219 -0
  258. package/dist/tools/speak.d.ts +10 -0
  259. package/dist/tools/speak.js +56 -0
  260. package/dist/tools/tasks.d.ts +67 -0
  261. package/dist/tools/tasks.js +288 -0
  262. package/dist/tools/teams.d.ts +22 -0
  263. package/dist/tools/teams.js +470 -0
  264. package/dist/tools/web-fetch.d.ts +3 -0
  265. package/dist/tools/web-fetch.js +22 -0
  266. package/dist/tts/edge.d.ts +10 -0
  267. package/dist/tts/edge.js +60 -0
  268. package/dist/tts/speak.d.ts +12 -0
  269. package/dist/tts/speak.js +81 -0
  270. package/dist/tts/transcribe.d.ts +5 -0
  271. package/dist/tts/transcribe.js +40 -0
  272. package/dist/utils.d.ts +5 -0
  273. package/dist/utils.js +22 -0
  274. package/dist/version.d.ts +1 -0
  275. package/dist/version.js +13 -0
  276. package/package.json +102 -0
  277. package/verybot.js +2 -0
@@ -0,0 +1,227 @@
1
+ import { App } from "@slack/bolt";
2
+ import { logger } from "../../logger.js";
3
+ import { markdownToSlackMrkdwnChunks, escapeSlackMrkdwn, SLACK_TEXT_LIMIT } from "./markdown.js";
4
+ import { CommandRouter } from "../commands.js";
5
+ /** Emoji added while the bot is processing a message. */
6
+ const TYPING_EMOJI = "eyes";
7
+ /** Send chunked Slack mrkdwn to a channel. Optionally in-thread via threadTs. */
8
+ async function sendMrkdwnChunks(app, channelId, text, resolveTableMode, threadTs) {
9
+ const tableMode = resolveTableMode();
10
+ const chunks = markdownToSlackMrkdwnChunks(text, SLACK_TEXT_LIMIT, { tableMode });
11
+ if (chunks.length === 0 && text) {
12
+ await app.client.chat.postMessage({ channel: channelId, text, thread_ts: threadTs });
13
+ return;
14
+ }
15
+ for (const chunk of chunks) {
16
+ await app.client.chat.postMessage({ channel: channelId, text: chunk, thread_ts: threadTs });
17
+ }
18
+ }
19
+ /** Strip the bot mention prefix (e.g. `<@U12345>`) from message text. */
20
+ function stripBotMention(text, botUserId) {
21
+ const prefix = `<@${botUserId}>`;
22
+ const trimmed = text.trimStart();
23
+ if (trimmed.startsWith(prefix)) {
24
+ return trimmed.slice(prefix.length).trim();
25
+ }
26
+ return text.trim();
27
+ }
28
+ export function createSlackChannel(opts) {
29
+ const { botToken, appToken, onMessage, onClear, onLearn, onRemember, listTeams, defaultTeamId, schedulerOpts, resolveTableMode = () => "code", } = opts;
30
+ const app = new App({
31
+ token: botToken,
32
+ appToken,
33
+ socketMode: true,
34
+ });
35
+ const commands = new CommandRouter({ onClear, onLearn, onRemember, listTeams, defaultTeamId });
36
+ /** Map<channelId, { teamId, connectionKey }> for active scheduler connections. */
37
+ const schedulerConnections = new Map();
38
+ /** Track active typing-proxy reaction timestamps per channel to avoid duplicate removals. */
39
+ const typingReactionTs = new Map();
40
+ /**
41
+ * Track thread context per channelId so replies stay in-thread.
42
+ * Note: in channels with concurrent @mentions, the latest message wins.
43
+ * DMs are safe since each DM conversation has a unique channelId.
44
+ */
45
+ const threadMap = new Map();
46
+ /** Format a CommandResult using Slack mrkdwn syntax. */
47
+ function formatSlackResult(result) {
48
+ return result.parts
49
+ .map((p) => {
50
+ if (typeof p === "string")
51
+ return escapeSlackMrkdwn(p);
52
+ if ("bold" in p)
53
+ return `*${escapeSlackMrkdwn(p.bold)}*`;
54
+ return `\`${p.code}\``;
55
+ })
56
+ .join("");
57
+ }
58
+ /**
59
+ * Handle text-based commands (e.g. "/clear", "/team foo").
60
+ * Returns true if the message was consumed as a command.
61
+ */
62
+ async function handleTextCommand(text, channelId) {
63
+ const result = await commands.handle("slack", channelId, text);
64
+ if (!result)
65
+ return false;
66
+ await app.client.chat.postMessage({ channel: channelId, text: formatSlackResult(result) });
67
+ return true;
68
+ }
69
+ /** Add a reaction as typing proxy; silently ignore failures. */
70
+ async function addTypingReaction(channelId, ts) {
71
+ let activeTs = typingReactionTs.get(channelId);
72
+ if (!activeTs) {
73
+ activeTs = new Set();
74
+ typingReactionTs.set(channelId, activeTs);
75
+ }
76
+ activeTs.add(ts);
77
+ try {
78
+ await app.client.reactions.add({ channel: channelId, timestamp: ts, name: TYPING_EMOJI });
79
+ }
80
+ catch {
81
+ // Reaction may already exist or bot lacks permission — safe to ignore
82
+ }
83
+ }
84
+ /** Remove typing reaction; silently ignore failures. */
85
+ async function removeTypingReaction(channelId, ts) {
86
+ const activeTs = typingReactionTs.get(channelId);
87
+ if (!activeTs?.has(ts))
88
+ return;
89
+ activeTs.delete(ts);
90
+ if (activeTs.size === 0)
91
+ typingReactionTs.delete(channelId);
92
+ try {
93
+ await app.client.reactions.remove({ channel: channelId, timestamp: ts, name: TYPING_EMOJI });
94
+ }
95
+ catch {
96
+ // Safe to ignore
97
+ }
98
+ }
99
+ const channel = {
100
+ name: "slack",
101
+ start: async () => {
102
+ // --- @mention events (channels/groups) ---
103
+ app.event("app_mention", async ({ event, context }) => {
104
+ const channelId = event.channel;
105
+ const userId = event.user ?? "unknown";
106
+ const botUserId = context.botUserId ?? "";
107
+ const rawText = event.text ?? "";
108
+ const text = stripBotMention(rawText, botUserId);
109
+ const ts = event.ts;
110
+ const threadTs = event.thread_ts ?? ts;
111
+ if (!text)
112
+ return;
113
+ if (await handleTextCommand(text, channelId))
114
+ return;
115
+ logger.info(`[slack] mention from ${userId} in ${channelId}`);
116
+ threadMap.set(channelId, { threadTs, messageTs: ts });
117
+ await addTypingReaction(channelId, ts);
118
+ try {
119
+ await onMessage({
120
+ channelType: "slack",
121
+ channelId,
122
+ userId,
123
+ text,
124
+ teamId: commands.resolveTeamId(channelId),
125
+ }, channel);
126
+ }
127
+ catch (err) {
128
+ const msg = err instanceof Error ? err.message : String(err);
129
+ logger.error(`[slack] error handling mention in ${channelId}: ${msg}`);
130
+ }
131
+ finally {
132
+ await removeTypingReaction(channelId, ts);
133
+ }
134
+ });
135
+ // --- DM events ---
136
+ app.event("message", async ({ event, context }) => {
137
+ // Only handle direct messages (im channel type)
138
+ // Skip bot messages, changed messages, etc.
139
+ if (!("channel_type" in event) || event.channel_type !== "im")
140
+ return;
141
+ if ("subtype" in event && event.subtype)
142
+ return;
143
+ if (!("user" in event) || !event.user)
144
+ return;
145
+ if (!("text" in event) || !event.text)
146
+ return;
147
+ // Ignore messages from the bot itself
148
+ if (event.user === context.botUserId)
149
+ return;
150
+ const channelId = event.channel;
151
+ const userId = event.user;
152
+ const botUserId = context.botUserId ?? "";
153
+ // Strip bot mention so "@bot /clear" is matched as "/clear"
154
+ const text = stripBotMention(event.text, botUserId);
155
+ const ts = event.ts;
156
+ const threadTs = ("thread_ts" in event ? event.thread_ts : undefined) ?? ts;
157
+ if (!text)
158
+ return;
159
+ if (await handleTextCommand(text, channelId))
160
+ return;
161
+ logger.info(`[slack] DM from ${userId}`);
162
+ threadMap.set(channelId, { threadTs, messageTs: ts });
163
+ await addTypingReaction(channelId, ts);
164
+ try {
165
+ await onMessage({
166
+ channelType: "slack",
167
+ channelId,
168
+ userId,
169
+ text,
170
+ teamId: commands.resolveTeamId(channelId),
171
+ }, channel);
172
+ }
173
+ catch (err) {
174
+ const msg = err instanceof Error ? err.message : String(err);
175
+ logger.error(`[slack] error handling DM from ${userId}: ${msg}`);
176
+ }
177
+ finally {
178
+ await removeTypingReaction(channelId, ts);
179
+ }
180
+ });
181
+ await app.start();
182
+ logger.info("[slack] connected via Socket Mode");
183
+ },
184
+ stop: async () => {
185
+ for (const [, conn] of schedulerConnections) {
186
+ schedulerOpts?.disconnectScheduler(conn.teamId, conn.connectionKey);
187
+ }
188
+ schedulerConnections.clear();
189
+ await app.stop();
190
+ logger.info("[slack] disconnected");
191
+ },
192
+ send: async (channelId, text) => {
193
+ const entry = threadMap.get(channelId);
194
+ await sendMrkdwnChunks(app, channelId, text, resolveTableMode, entry?.threadTs);
195
+ if (entry) {
196
+ await removeTypingReaction(channelId, entry.messageTs);
197
+ }
198
+ },
199
+ readHistory: async (channelId, limit = 20, threadTs) => {
200
+ logger.info(`[slack] readHistory channel=${channelId} limit=${limit} threadTs=${threadTs ?? "none"}`);
201
+ let raw;
202
+ if (threadTs) {
203
+ const result = await app.client.conversations.replies({
204
+ channel: channelId,
205
+ ts: threadTs,
206
+ limit,
207
+ });
208
+ raw = result.messages ?? [];
209
+ }
210
+ else {
211
+ const result = await app.client.conversations.history({
212
+ channel: channelId,
213
+ limit,
214
+ });
215
+ // conversations.history returns newest-first, reverse for chronological
216
+ raw = (result.messages ?? []).reverse();
217
+ }
218
+ logger.info(`[slack] readHistory returned ${raw.length} messages for ${channelId}`);
219
+ return raw.map((msg) => ({
220
+ user: msg.user ?? "bot",
221
+ text: msg.text ?? "",
222
+ ts: msg.ts ?? "",
223
+ }));
224
+ },
225
+ };
226
+ return channel;
227
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Markdown → Slack mrkdwn conversion.
3
+ * Slack uses a custom markdown-like format called "mrkdwn".
4
+ */
5
+ import { type MarkdownTableMode } from "../../markdown/ir.js";
6
+ /** Slack mrkdwn uses &, <, > as control chars — escape them in plain text. */
7
+ export declare function escapeSlackMrkdwn(text: string): string;
8
+ export declare function markdownToSlackMrkdwn(markdown: string, options?: {
9
+ tableMode?: MarkdownTableMode;
10
+ }): string;
11
+ /** Slack message limit (text blocks). */
12
+ export declare const SLACK_TEXT_LIMIT = 4000;
13
+ /**
14
+ * Convert markdown to chunked Slack mrkdwn strings.
15
+ * Each chunk is ≤ `limit` characters of plain-text IR.
16
+ */
17
+ export declare function markdownToSlackMrkdwnChunks(markdown: string, limit?: number, options?: {
18
+ tableMode?: MarkdownTableMode;
19
+ }): string[];
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Markdown → Slack mrkdwn conversion.
3
+ * Slack uses a custom markdown-like format called "mrkdwn".
4
+ */
5
+ import { chunkMarkdownIR, markdownToIR, } from "../../markdown/ir.js";
6
+ import { renderMarkdownWithMarkers } from "../../markdown/render.js";
7
+ const SAFE_LINK_PROTOCOL = /^https?:\/\/|^mailto:|^tel:/i;
8
+ /** Slack mrkdwn uses &, <, > as control chars — escape them in plain text. */
9
+ export function escapeSlackMrkdwn(text) {
10
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
11
+ }
12
+ function buildSlackLink(link, _text) {
13
+ const href = link.href.trim();
14
+ if (!href || link.start === link.end)
15
+ return null;
16
+ if (!SAFE_LINK_PROTOCOL.test(href))
17
+ return null;
18
+ return {
19
+ start: link.start,
20
+ end: link.end,
21
+ open: `<${escapeSlackMrkdwn(href)}|`,
22
+ close: ">",
23
+ };
24
+ }
25
+ function renderSlackMrkdwn(ir) {
26
+ return renderMarkdownWithMarkers(ir, {
27
+ styleMarkers: {
28
+ bold: { open: "*", close: "*" },
29
+ italic: { open: "_", close: "_" },
30
+ strikethrough: { open: "~", close: "~" },
31
+ code: { open: "`", close: "`" },
32
+ code_block: { open: "```\n", close: "\n```" },
33
+ },
34
+ escapeText: escapeSlackMrkdwn,
35
+ buildLink: buildSlackLink,
36
+ });
37
+ }
38
+ export function markdownToSlackMrkdwn(markdown, options = {}) {
39
+ const ir = markdownToIR(markdown ?? "", {
40
+ linkify: true,
41
+ headingStyle: "none",
42
+ blockquotePrefix: "> ",
43
+ tableMode: options.tableMode,
44
+ });
45
+ return renderSlackMrkdwn(ir);
46
+ }
47
+ /** Slack message limit (text blocks). */
48
+ export const SLACK_TEXT_LIMIT = 4000;
49
+ /**
50
+ * Convert markdown to chunked Slack mrkdwn strings.
51
+ * Each chunk is ≤ `limit` characters of plain-text IR.
52
+ */
53
+ export function markdownToSlackMrkdwnChunks(markdown, limit = SLACK_TEXT_LIMIT, options = {}) {
54
+ const ir = markdownToIR(markdown ?? "", {
55
+ linkify: true,
56
+ headingStyle: "none",
57
+ blockquotePrefix: "> ",
58
+ tableMode: options.tableMode,
59
+ });
60
+ const chunks = chunkMarkdownIR(ir, limit);
61
+ return chunks.map((chunk) => renderSlackMrkdwn(chunk));
62
+ }
@@ -0,0 +1,32 @@
1
+ import type { MessageHandler } from "./types.js";
2
+ import type { ChannelSpec } from "./manager.js";
3
+ import type { MarkdownTableMode } from "../markdown/ir.js";
4
+ import type { ConnectedChannelRegistry } from "../scheduler/connected-channels.js";
5
+ import type { Config } from "../config.js";
6
+ export interface ChannelCallbacks {
7
+ onMessage: MessageHandler;
8
+ onClear: (channelType: string, channelId: string, teamId?: string) => Promise<void>;
9
+ onLearn: (channelType: string, channelId: string, topic?: string, teamId?: string) => Promise<{
10
+ topic?: string;
11
+ extracted: number;
12
+ saved: number;
13
+ skipped: number;
14
+ savedFacts: string[];
15
+ }>;
16
+ onRemember: (channelType: string, channelId: string, fact: string, teamId?: string) => Promise<{
17
+ saved: boolean;
18
+ fact: string;
19
+ }>;
20
+ listTeams: () => {
21
+ id: string;
22
+ name: string;
23
+ }[];
24
+ connectedChannels: ConnectedChannelRegistry | null;
25
+ /** Return the raw per-channel table config value (resolved lazily at send time). */
26
+ getChannelTableConfig: (channel: string) => MarkdownTableMode | string | null | undefined;
27
+ }
28
+ /**
29
+ * Build channel specs from config. Pure function — all side effects are
30
+ * deferred via callbacks so agent.ts stays thin.
31
+ */
32
+ export declare function buildChannelSpecs(config: Config, cb: ChannelCallbacks): ChannelSpec[];
@@ -0,0 +1,99 @@
1
+ import { channelFingerprint } from "./manager.js";
2
+ import { createTelegramChannel } from "./telegram/channel.js";
3
+ import { createDiscordChannel } from "./discord/channel.js";
4
+ import { createSlackChannel } from "./slack/channel.js";
5
+ import { createWhatsAppChannel } from "./whatsapp/channel.js";
6
+ import { resolveMarkdownTableMode } from "../markdown/tables.js";
7
+ /** Build scheduler opts shared across channels. */
8
+ function buildSchedulerOpts(channelType, connectedChannels) {
9
+ return {
10
+ connectScheduler: (teamId, channelId, sendFn) => connectedChannels.connect(teamId, {
11
+ channelType,
12
+ channelId,
13
+ send: sendFn,
14
+ }),
15
+ disconnectScheduler: (teamId, key) => connectedChannels.disconnect(teamId, key),
16
+ };
17
+ }
18
+ /**
19
+ * Build channel specs from config. Pure function — all side effects are
20
+ * deferred via callbacks so agent.ts stays thin.
21
+ */
22
+ export function buildChannelSpecs(config, cb) {
23
+ const specs = [];
24
+ if (config.channels.telegram) {
25
+ const token = config.channels.telegram.token;
26
+ const resolveTableMode = () => resolveMarkdownTableMode({ channel: "telegram", channelTableMode: cb.getChannelTableConfig("telegram") });
27
+ specs.push({
28
+ name: "telegram",
29
+ fingerprint: channelFingerprint(token),
30
+ create: () => createTelegramChannel({
31
+ token,
32
+ listTeams: cb.listTeams,
33
+ onMessage: cb.onMessage,
34
+ onClear: cb.onClear,
35
+ onLearn: cb.onLearn,
36
+ onRemember: cb.onRemember,
37
+ resolveTableMode,
38
+ }),
39
+ });
40
+ }
41
+ if (config.channels.discord) {
42
+ const token = config.channels.discord.token;
43
+ const resolveTableMode = () => resolveMarkdownTableMode({ channel: "discord", channelTableMode: cb.getChannelTableConfig("discord") });
44
+ specs.push({
45
+ name: "discord",
46
+ fingerprint: channelFingerprint(token),
47
+ create: () => createDiscordChannel({
48
+ token,
49
+ listTeams: cb.listTeams,
50
+ onMessage: cb.onMessage,
51
+ onClear: cb.onClear,
52
+ onLearn: cb.onLearn,
53
+ onRemember: cb.onRemember,
54
+ resolveTableMode,
55
+ }),
56
+ });
57
+ }
58
+ if (config.channels.slack) {
59
+ const { botToken, appToken } = config.channels.slack;
60
+ const resolveTableMode = () => resolveMarkdownTableMode({ channel: "slack", channelTableMode: cb.getChannelTableConfig("slack") });
61
+ const schedulerOpts = cb.connectedChannels
62
+ ? buildSchedulerOpts("slack", cb.connectedChannels)
63
+ : undefined;
64
+ specs.push({
65
+ name: "slack",
66
+ fingerprint: channelFingerprint(botToken + appToken),
67
+ create: () => createSlackChannel({
68
+ botToken,
69
+ appToken,
70
+ listTeams: cb.listTeams,
71
+ onMessage: cb.onMessage,
72
+ onClear: cb.onClear,
73
+ onLearn: cb.onLearn,
74
+ onRemember: cb.onRemember,
75
+ schedulerOpts,
76
+ resolveTableMode,
77
+ }),
78
+ });
79
+ }
80
+ if (config.channels.whatsapp) {
81
+ const { phoneId } = config.channels.whatsapp;
82
+ const resolveTableMode = () => resolveMarkdownTableMode({ channel: "whatsapp", channelTableMode: cb.getChannelTableConfig("whatsapp") });
83
+ specs.push({
84
+ name: "whatsapp",
85
+ fingerprint: channelFingerprint(phoneId),
86
+ create: () => createWhatsAppChannel({
87
+ phoneId,
88
+ selfOnly: config.channels.whatsapp?.selfOnly,
89
+ listTeams: cb.listTeams,
90
+ onMessage: cb.onMessage,
91
+ onClear: cb.onClear,
92
+ onLearn: cb.onLearn,
93
+ onRemember: cb.onRemember,
94
+ resolveTableMode,
95
+ }),
96
+ });
97
+ }
98
+ return specs;
99
+ }
@@ -0,0 +1,29 @@
1
+ import type { Channel, MessageHandler } from "../types.js";
2
+ import type { MarkdownTableMode } from "../../markdown/ir.js";
3
+ /** Callback that resolves the current table mode from config at send time. */
4
+ export type ResolveTableMode = () => MarkdownTableMode;
5
+ export interface CreateTelegramChannelOpts {
6
+ token: string;
7
+ onMessage: MessageHandler;
8
+ onClear?: (channelType: string, channelId: string, teamId?: string) => Promise<void>;
9
+ onLearn?: (channelType: string, channelId: string, topic?: string, teamId?: string) => Promise<{
10
+ topic?: string;
11
+ extracted: number;
12
+ saved: number;
13
+ skipped: number;
14
+ savedFacts: string[];
15
+ }>;
16
+ onRemember?: (channelType: string, channelId: string, fact: string, teamId?: string) => Promise<{
17
+ saved: boolean;
18
+ fact: string;
19
+ }>;
20
+ /** List available teams for the /team command. */
21
+ listTeams?: () => {
22
+ id: string;
23
+ name: string;
24
+ }[];
25
+ /** Fallback team when no active team is set for a chat. */
26
+ defaultTeamId?: string;
27
+ resolveTableMode?: ResolveTableMode;
28
+ }
29
+ export declare function createTelegramChannel(opts: CreateTelegramChannelOpts): Channel;
@@ -0,0 +1,182 @@
1
+ import { Bot, InputFile } from "grammy";
2
+ import { transcribe } from "../../tts/transcribe.js";
3
+ import { logger } from "../../logger.js";
4
+ import { markdownToTelegramHtmlChunks, escapeHtml } from "./markdown.js";
5
+ import { CommandRouter } from "../commands.js";
6
+ const TELEGRAM_TEXT_LIMIT = 4000;
7
+ /** Send chunked HTML to a Telegram chat. Handles markdown→HTML conversion + splitting. */
8
+ async function sendHtmlChunks(bot, chatId, text, resolveTableMode) {
9
+ const tableMode = resolveTableMode();
10
+ const chunks = markdownToTelegramHtmlChunks(text, TELEGRAM_TEXT_LIMIT, { tableMode });
11
+ if (chunks.length === 0 && text) {
12
+ // Fallback: if conversion produced nothing, send raw text
13
+ await bot.api.sendMessage(chatId, text);
14
+ return;
15
+ }
16
+ for (const chunk of chunks) {
17
+ await bot.api.sendMessage(chatId, chunk, { parse_mode: "HTML" });
18
+ }
19
+ }
20
+ export function createTelegramChannel(opts) {
21
+ const { token, onMessage, onClear, onLearn, onRemember, listTeams, defaultTeamId, resolveTableMode = () => "code", } = opts;
22
+ const bot = new Bot(token);
23
+ const typingIntervals = new Map();
24
+ const commands = new CommandRouter({ onClear, onLearn, onRemember, listTeams, defaultTeamId });
25
+ /** Format a CommandResult using Telegram HTML syntax. */
26
+ function formatTelegramResult(result) {
27
+ return result.parts
28
+ .map((p) => {
29
+ if (typeof p === "string")
30
+ return escapeHtml(p);
31
+ if ("bold" in p)
32
+ return `<b>${escapeHtml(p.bold)}</b>`;
33
+ return `<code>${escapeHtml(p.code)}</code>`;
34
+ })
35
+ .join("");
36
+ }
37
+ function startTyping(chatId) {
38
+ stopTyping(chatId);
39
+ const numId = Number(chatId);
40
+ // Send immediately, then every 4s (Telegram typing expires after ~5s)
41
+ bot.api.sendChatAction(numId, "typing").catch((err) => logger.error(`typing error: ${err}`));
42
+ logger.info(`typing started for ${chatId}`);
43
+ const interval = setInterval(() => {
44
+ bot.api.sendChatAction(numId, "typing").catch((err) => logger.error(`typing interval error: ${err}`));
45
+ }, 4000);
46
+ typingIntervals.set(chatId, interval);
47
+ }
48
+ function stopTyping(chatId) {
49
+ const interval = typingIntervals.get(chatId);
50
+ if (interval) {
51
+ clearInterval(interval);
52
+ typingIntervals.delete(chatId);
53
+ }
54
+ }
55
+ const channel = {
56
+ name: "telegram",
57
+ start: async () => {
58
+ for (const cmd of ["clear", "reset"]) {
59
+ bot.command(cmd, async (ctx) => {
60
+ const chatId = String(ctx.chat.id);
61
+ const result = await commands.handleClear("telegram", chatId);
62
+ await ctx.reply(formatTelegramResult(result), { parse_mode: "HTML" });
63
+ });
64
+ }
65
+ // --- /team command for multi-team switching ---
66
+ if (listTeams) {
67
+ bot.command("team", async (ctx) => {
68
+ try {
69
+ const chatId = String(ctx.chat.id);
70
+ const arg = ctx.match?.trim() ?? "";
71
+ const result = await commands.handleTeam("telegram", chatId, arg);
72
+ await ctx.reply(formatTelegramResult(result), { parse_mode: "HTML" });
73
+ }
74
+ catch (err) {
75
+ const msg = err instanceof Error ? err.message : String(err);
76
+ logger.error(`[telegram] /team command failed: ${msg}`);
77
+ await ctx.reply("Failed to process team command. Please try again.");
78
+ }
79
+ });
80
+ }
81
+ for (const cmd of ["learn", "remember"]) {
82
+ bot.command(cmd, async (ctx) => {
83
+ try {
84
+ const chatId = String(ctx.chat.id);
85
+ const arg = ctx.match?.trim() ?? "";
86
+ const text = arg ? `/${cmd} ${arg}` : `/${cmd}`;
87
+ const result = await commands.handle("telegram", chatId, text);
88
+ if (!result)
89
+ return;
90
+ await ctx.reply(formatTelegramResult(result), { parse_mode: "HTML" });
91
+ }
92
+ catch (err) {
93
+ const msg = err instanceof Error ? err.message : String(err);
94
+ logger.error(`[telegram] /${cmd} command failed: ${msg}`);
95
+ await ctx.reply("Failed to process memory command. Please try again.");
96
+ }
97
+ });
98
+ }
99
+ bot.on("message:voice", async (ctx) => {
100
+ const chatId = String(ctx.chat.id);
101
+ logger.info(`[telegram] voice message received from ${chatId}`);
102
+ startTyping(chatId);
103
+ try {
104
+ const file = await ctx.getFile();
105
+ const url = `https://api.telegram.org/file/bot${token}/${file.file_path}`;
106
+ const res = await fetch(url);
107
+ if (!res.ok)
108
+ throw new Error(`Download failed: ${res.status}`);
109
+ const buffer = Buffer.from(await res.arrayBuffer());
110
+ const text = await transcribe(buffer, file.file_path ?? "voice.ogg");
111
+ logger.info(`[telegram] transcribed voice: "${text.slice(0, 100)}"`);
112
+ await onMessage({
113
+ channelType: "telegram",
114
+ channelId: chatId,
115
+ userId: String(ctx.from.id),
116
+ text,
117
+ isVoice: true,
118
+ teamId: commands.resolveTeamId(chatId),
119
+ }, channel);
120
+ }
121
+ catch (err) {
122
+ const msg = err instanceof Error ? err.message : String(err);
123
+ logger.error(`[telegram] voice transcription failed: ${msg}`);
124
+ await ctx.reply("Sorry, I couldn't process your voice message.");
125
+ }
126
+ finally {
127
+ stopTyping(chatId);
128
+ }
129
+ });
130
+ bot.on("message:text", async (ctx) => {
131
+ // Skip bot commands — registered ones are handled by bot.command(),
132
+ // unregistered ones (e.g. /help) should not be forwarded as text
133
+ const entities = ctx.message.entities ?? [];
134
+ if (entities.some((e) => e.type === "bot_command" && e.offset === 0))
135
+ return;
136
+ const chatId = String(ctx.chat.id);
137
+ logger.info(`[telegram] message received from ${chatId}`);
138
+ startTyping(chatId);
139
+ try {
140
+ await onMessage({
141
+ channelType: "telegram",
142
+ channelId: chatId,
143
+ userId: String(ctx.from.id),
144
+ text: ctx.message.text,
145
+ teamId: commands.resolveTeamId(chatId),
146
+ }, channel);
147
+ }
148
+ finally {
149
+ stopTyping(chatId);
150
+ }
151
+ });
152
+ // Register commands with Telegram so they appear in the "/" menu
153
+ const menuCommands = [
154
+ { command: "clear", description: "Clear the current session." },
155
+ { command: "reset", description: "Clear the current session." },
156
+ { command: "learn", description: "Teach me a fact to remember." },
157
+ ];
158
+ if (listTeams) {
159
+ menuCommands.push({ command: "team", description: "List or switch teams." });
160
+ }
161
+ bot.start();
162
+ // setMyCommands after start() so Grammy has initialized the bot
163
+ bot.api.setMyCommands(menuCommands).then(() => logger.info(`[telegram] registered ${menuCommands.length} commands`), (err) => logger.error(`[telegram] setMyCommands failed: ${err}`));
164
+ },
165
+ stop: async () => {
166
+ for (const chatId of typingIntervals.keys())
167
+ stopTyping(chatId);
168
+ await bot.stop();
169
+ },
170
+ send: async (channelId, text) => {
171
+ stopTyping(channelId);
172
+ await sendHtmlChunks(bot, Number(channelId), text, resolveTableMode);
173
+ },
174
+ sendVoice: async (channelId, audioPath) => {
175
+ stopTyping(channelId);
176
+ const chatId = Number(channelId);
177
+ await bot.api.sendVoice(chatId, new InputFile(audioPath));
178
+ logger.info(`[telegram] voice message sent to ${channelId}`);
179
+ },
180
+ };
181
+ return channel;
182
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Markdown → Telegram HTML conversion.
3
+ * Markdown to Telegram HTML conversion.
4
+ */
5
+ import { type MarkdownTableMode } from "../../markdown/ir.js";
6
+ export declare function escapeHtml(text: string): string;
7
+ export declare function markdownToTelegramHtml(markdown: string, options?: {
8
+ tableMode?: MarkdownTableMode;
9
+ }): string;
10
+ /**
11
+ * Convert markdown to chunked Telegram HTML strings.
12
+ * Each chunk is ≤ `limit` characters of the plain-text IR (the HTML tags
13
+ * add overhead but Telegram counts the visible text).
14
+ */
15
+ export declare function markdownToTelegramHtmlChunks(markdown: string, limit: number, options?: {
16
+ tableMode?: MarkdownTableMode;
17
+ }): string[];