talon-agent 1.12.0 → 1.34.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 (451) hide show
  1. package/README.md +111 -52
  2. package/package.json +39 -12
  3. package/prompts/README.md +112 -0
  4. package/prompts/base.md +12 -12
  5. package/prompts/discord.md +10 -47
  6. package/prompts/heartbeat.md +23 -8
  7. package/prompts/identity.md +12 -22
  8. package/prompts/native.md +23 -0
  9. package/prompts/system/contract-text-or-tools.md +8 -0
  10. package/prompts/system/contract-text-preferred.md +5 -0
  11. package/prompts/system/contract-tool-only.md +10 -0
  12. package/prompts/system/cron.md +3 -0
  13. package/prompts/system/daily-memory.md +3 -0
  14. package/prompts/system/goals.md +7 -0
  15. package/prompts/system/heartbeat-agent.md +44 -0
  16. package/prompts/system/persistent-memory.md +8 -0
  17. package/prompts/system/skills.md +9 -0
  18. package/prompts/system/triggers.md +5 -0
  19. package/prompts/system/workspace.md +9 -0
  20. package/prompts/teams.md +4 -14
  21. package/prompts/telegram.md +17 -89
  22. package/src/app.ts +278 -0
  23. package/src/backend/claude-sdk/constants.ts +22 -10
  24. package/src/backend/claude-sdk/factory.ts +79 -22
  25. package/src/backend/claude-sdk/handler.ts +431 -160
  26. package/src/backend/claude-sdk/index.ts +6 -2
  27. package/src/backend/claude-sdk/mcp-ready.ts +54 -0
  28. package/src/backend/claude-sdk/model-drift.ts +65 -0
  29. package/src/backend/claude-sdk/model-provider.ts +12 -6
  30. package/src/backend/claude-sdk/models/convert.ts +169 -0
  31. package/src/backend/claude-sdk/models/discovery.ts +203 -0
  32. package/src/backend/claude-sdk/models/index.ts +18 -0
  33. package/src/backend/claude-sdk/models/parsing.ts +264 -0
  34. package/src/backend/claude-sdk/models/static.ts +45 -0
  35. package/src/backend/claude-sdk/one-shot.ts +19 -13
  36. package/src/backend/claude-sdk/options.ts +228 -104
  37. package/src/backend/claude-sdk/state.ts +1 -1
  38. package/src/backend/claude-sdk/stream.ts +153 -45
  39. package/src/backend/claude-sdk/warm.ts +12 -5
  40. package/src/backend/codex/auth.ts +216 -40
  41. package/src/backend/codex/constants.ts +63 -22
  42. package/src/backend/codex/discovery.ts +427 -0
  43. package/src/backend/codex/factory.ts +63 -33
  44. package/src/backend/codex/handler/events.ts +239 -0
  45. package/src/backend/codex/handler/index.ts +15 -0
  46. package/src/backend/codex/handler/message.ts +669 -0
  47. package/src/backend/codex/handler/state.ts +14 -0
  48. package/src/backend/codex/handler/usage.ts +68 -0
  49. package/src/backend/codex/index.ts +2 -2
  50. package/src/backend/codex/init.ts +92 -11
  51. package/src/backend/codex/mcp-config.ts +89 -62
  52. package/src/backend/codex/models.ts +305 -54
  53. package/src/backend/codex/oauth-incompat.ts +302 -0
  54. package/src/backend/codex/one-shot.ts +91 -8
  55. package/src/backend/codex/state.ts +54 -3
  56. package/src/backend/codex/token-usage.ts +434 -0
  57. package/src/backend/kilo/factory.ts +60 -35
  58. package/src/backend/kilo/handler/index.ts +13 -0
  59. package/src/backend/kilo/handler/message.ts +317 -0
  60. package/src/backend/kilo/handler/state.ts +14 -0
  61. package/src/backend/kilo/handler/turn.ts +321 -0
  62. package/src/backend/kilo/index.ts +5 -5
  63. package/src/backend/kilo/model-provider.ts +26 -168
  64. package/src/backend/kilo/models/index.ts +52 -0
  65. package/src/backend/kilo/one-shot.ts +20 -142
  66. package/src/backend/kilo/server.ts +14 -21
  67. package/src/backend/openai-agents/builtins.ts +215 -114
  68. package/src/backend/openai-agents/constants.ts +41 -15
  69. package/src/backend/openai-agents/discovery.ts +332 -0
  70. package/src/backend/openai-agents/factory.ts +66 -28
  71. package/src/backend/openai-agents/handler/events.ts +170 -0
  72. package/src/backend/openai-agents/handler/index.ts +13 -0
  73. package/src/backend/openai-agents/handler/message.ts +527 -0
  74. package/src/backend/openai-agents/handler/state.ts +13 -0
  75. package/src/backend/openai-agents/index.ts +22 -4
  76. package/src/backend/openai-agents/init.ts +51 -88
  77. package/src/backend/openai-agents/mcp-pool.ts +252 -0
  78. package/src/backend/openai-agents/models.ts +118 -11
  79. package/src/backend/openai-agents/session.ts +277 -0
  80. package/src/backend/openai-agents/state.ts +82 -11
  81. package/src/backend/opencode/factory.ts +54 -25
  82. package/src/backend/opencode/handler/index.ts +13 -0
  83. package/src/backend/opencode/handler/message.ts +325 -0
  84. package/src/backend/opencode/handler/state.ts +14 -0
  85. package/src/backend/opencode/handler/turn.ts +304 -0
  86. package/src/backend/opencode/index.ts +2 -2
  87. package/src/backend/opencode/model-provider.ts +26 -168
  88. package/src/backend/opencode/models/index.ts +51 -0
  89. package/src/backend/opencode/one-shot.ts +20 -137
  90. package/src/backend/opencode/server.ts +12 -11
  91. package/src/backend/remote-server/client.ts +13 -6
  92. package/src/backend/remote-server/events.ts +59 -9
  93. package/src/backend/remote-server/lifecycle.ts +3 -1
  94. package/src/backend/remote-server/mcp.ts +52 -19
  95. package/src/backend/remote-server/model-catalog/catalog.ts +270 -0
  96. package/src/backend/remote-server/model-catalog/index.ts +100 -0
  97. package/src/backend/remote-server/model-catalog/presentation.ts +298 -0
  98. package/src/backend/remote-server/model-catalog/provider.ts +193 -0
  99. package/src/backend/remote-server/model-catalog/resolve.ts +225 -0
  100. package/src/backend/remote-server/model-catalog/types.ts +136 -0
  101. package/src/backend/remote-server/one-shot.ts +225 -0
  102. package/src/backend/remote-server/session-helpers.ts +3 -4
  103. package/src/backend/remote-server/state.ts +1 -1
  104. package/src/backend/shared/delivery-contract.ts +161 -0
  105. package/src/backend/shared/delivery.ts +49 -0
  106. package/src/backend/shared/flow-violation.ts +38 -7
  107. package/src/backend/shared/frontends.ts +66 -0
  108. package/src/backend/shared/handle-retry.ts +244 -0
  109. package/src/backend/shared/handler-to-events.ts +186 -0
  110. package/src/backend/shared/handler-types.ts +57 -0
  111. package/src/backend/shared/index.ts +51 -5
  112. package/src/backend/shared/metrics.ts +199 -0
  113. package/src/backend/shared/model-retry.ts +1 -1
  114. package/src/backend/shared/prompt-format.ts +69 -0
  115. package/src/backend/shared/session-name.ts +1 -1
  116. package/src/backend/shared/stream-state.ts +36 -2
  117. package/src/backend/shared/system-prompt.ts +200 -23
  118. package/src/bootstrap.ts +338 -38
  119. package/src/cli/chat.ts +49 -0
  120. package/src/cli/config-view.ts +100 -0
  121. package/src/cli/config.ts +105 -0
  122. package/src/cli/context.ts +23 -0
  123. package/src/cli/daemon.ts +95 -0
  124. package/src/cli/doctor.ts +53 -0
  125. package/src/cli/index.ts +139 -0
  126. package/src/cli/logs.ts +60 -0
  127. package/src/cli/menu.ts +104 -0
  128. package/src/cli/setup.ts +455 -0
  129. package/src/cli/status.ts +75 -0
  130. package/src/cli.ts +20 -1211
  131. package/src/core/agent-runtime/README.md +173 -0
  132. package/src/{backend/registry.ts → core/agent-runtime/backend-registry.ts} +15 -6
  133. package/src/core/agent-runtime/capabilities.ts +330 -0
  134. package/src/core/agent-runtime/contract-tests.ts +378 -0
  135. package/src/core/agent-runtime/events.ts +291 -0
  136. package/src/core/agent-runtime/index.ts +62 -0
  137. package/src/core/agent-runtime/model-ref.ts +130 -0
  138. package/src/core/background/cron.ts +469 -0
  139. package/src/core/{dream.ts → background/dream.ts} +118 -53
  140. package/src/core/background/heartbeat/agent.ts +333 -0
  141. package/src/core/background/heartbeat/index.ts +25 -0
  142. package/src/core/background/heartbeat/scheduler.ts +211 -0
  143. package/src/core/background/heartbeat/state.ts +153 -0
  144. package/src/core/background/index.ts +21 -0
  145. package/src/core/background/isolated-agent.ts +115 -0
  146. package/src/core/background/job-health.ts +140 -0
  147. package/src/core/background/job-oneshot.ts +165 -0
  148. package/src/core/background/job-prompt.ts +54 -0
  149. package/src/core/{pulse.ts → background/pulse.ts} +6 -5
  150. package/src/core/background/triggers/command.ts +60 -0
  151. package/src/core/background/triggers/exit.ts +202 -0
  152. package/src/core/background/triggers/index.ts +38 -0
  153. package/src/core/background/triggers/output.ts +142 -0
  154. package/src/core/background/triggers/resume.ts +127 -0
  155. package/src/core/background/triggers/spawn.ts +282 -0
  156. package/src/core/background/triggers/state.ts +52 -0
  157. package/src/core/constants.ts +57 -28
  158. package/src/core/daemon/control.ts +229 -0
  159. package/src/core/daemon/discovery.ts +169 -0
  160. package/src/core/daemon/pidfile.ts +98 -0
  161. package/src/core/doctor.ts +459 -0
  162. package/src/core/engine/backend-controller/holders.ts +17 -0
  163. package/src/core/engine/backend-controller/index.ts +71 -0
  164. package/src/core/engine/backend-controller/legacy.ts +111 -0
  165. package/src/core/engine/backend-controller/pool.ts +296 -0
  166. package/src/core/engine/backend-controller/rebind.ts +205 -0
  167. package/src/core/engine/backend-controller/state.ts +150 -0
  168. package/src/core/engine/backend-controller/types.ts +51 -0
  169. package/src/core/engine/dispatcher.ts +68 -0
  170. package/src/core/engine/gateway-actions/cron.ts +518 -0
  171. package/src/core/engine/gateway-actions/fetch-url.ts +120 -0
  172. package/src/core/engine/gateway-actions/goals.ts +178 -0
  173. package/src/core/engine/gateway-actions/history.ts +53 -0
  174. package/src/core/engine/gateway-actions/index.ts +60 -0
  175. package/src/core/engine/gateway-actions/models.ts +120 -0
  176. package/src/core/engine/gateway-actions/plugins.ts +83 -0
  177. package/src/core/engine/gateway-actions/scripts.ts +127 -0
  178. package/src/core/engine/gateway-actions/shared.ts +66 -0
  179. package/src/core/engine/gateway-actions/skills.ts +120 -0
  180. package/src/core/engine/gateway-actions/triggers.ts +237 -0
  181. package/src/core/engine/gateway-actions/types.ts +18 -0
  182. package/src/core/{gateway.ts → engine/gateway.ts} +223 -60
  183. package/src/core/engine/index.ts +20 -0
  184. package/src/core/engine/model-audit.ts +121 -0
  185. package/src/core/errors.ts +68 -6
  186. package/src/core/mcp-hub/children.ts +254 -0
  187. package/src/core/mcp-hub/index.ts +335 -0
  188. package/src/core/mcp-hub/proxy-server.ts +45 -0
  189. package/src/core/mcp-hub/talon-server.ts +71 -0
  190. package/src/core/memory/retrieval.ts +90 -0
  191. package/src/core/models/active-model.ts +441 -0
  192. package/src/core/{models.ts → models/catalog.ts} +6 -0
  193. package/src/core/models/index.ts +17 -0
  194. package/src/core/models/reasoning-levels.ts +57 -0
  195. package/src/core/plugin/actions.ts +32 -0
  196. package/src/core/plugin/builtins.ts +162 -0
  197. package/src/core/plugin/index.ts +37 -0
  198. package/src/core/plugin/loader.ts +289 -0
  199. package/src/core/plugin/mcp.ts +86 -0
  200. package/src/core/plugin/registry.ts +124 -0
  201. package/src/core/plugin/types.ts +126 -0
  202. package/src/core/prompt/assemble.ts +273 -0
  203. package/src/core/prompt/disk-prompts.ts +41 -0
  204. package/src/core/prompt/embed.ts +107 -0
  205. package/src/core/prompt/embedded-prompts.ts +85 -0
  206. package/src/core/prompt/index.ts +30 -0
  207. package/src/core/prompt/templates.ts +124 -0
  208. package/src/core/prompt/workspace-listing.ts +91 -0
  209. package/src/core/scripting/lua-runner.ts +210 -0
  210. package/src/core/scripts/runner.ts +125 -0
  211. package/src/core/soul/README.md +110 -0
  212. package/src/core/soul/RESEARCH.md +98 -0
  213. package/src/core/soul/associative.ts +98 -0
  214. package/src/core/soul/centrality.ts +98 -0
  215. package/src/core/soul/cluster.ts +83 -0
  216. package/src/core/soul/compiler.ts +207 -0
  217. package/src/core/soul/consolidate.ts +179 -0
  218. package/src/core/soul/critic.ts +159 -0
  219. package/src/core/soul/dag.ts +265 -0
  220. package/src/core/soul/delta.ts +123 -0
  221. package/src/core/soul/drift.ts +99 -0
  222. package/src/core/soul/embedder.ts +129 -0
  223. package/src/core/soul/emergent-critic.ts +96 -0
  224. package/src/core/soul/forgetting.ts +131 -0
  225. package/src/core/soul/governance.ts +93 -0
  226. package/src/core/soul/hash.ts +97 -0
  227. package/src/core/soul/hdc.ts +154 -0
  228. package/src/core/soul/index.ts +140 -0
  229. package/src/core/soul/kernel.ts +540 -0
  230. package/src/core/soul/lattice.ts +103 -0
  231. package/src/core/soul/lens.ts +110 -0
  232. package/src/core/soul/projector.ts +240 -0
  233. package/src/core/soul/reflect.ts +170 -0
  234. package/src/core/soul/reflex.ts +164 -0
  235. package/src/core/soul/retrieve.ts +146 -0
  236. package/src/core/soul/salience.ts +142 -0
  237. package/src/core/soul/service.ts +204 -0
  238. package/src/core/soul/settings.ts +47 -0
  239. package/src/core/soul/signals.ts +117 -0
  240. package/src/core/soul/talon-embedder.ts +80 -0
  241. package/src/core/soul/taps.ts +199 -0
  242. package/src/core/soul/types.ts +298 -0
  243. package/src/core/soul/valence.ts +83 -0
  244. package/src/core/tools/goals.ts +117 -0
  245. package/src/core/tools/index.ts +10 -0
  246. package/src/core/tools/mcp-env.ts +94 -0
  247. package/src/core/tools/mcp-server.ts +27 -4
  248. package/src/core/tools/messaging.ts +122 -29
  249. package/src/core/tools/models.ts +39 -0
  250. package/src/core/tools/scheduling.ts +133 -14
  251. package/src/core/tools/schemas.ts +16 -4
  252. package/src/core/tools/scripts.ts +78 -0
  253. package/src/core/tools/skills.ts +90 -0
  254. package/src/core/tools/stickers.ts +1 -1
  255. package/src/core/tools/triggers.ts +19 -3
  256. package/src/core/tools/types.ts +6 -6
  257. package/src/core/types.ts +62 -112
  258. package/src/core/update/self-update.ts +269 -0
  259. package/src/core/weaver/index.ts +10 -0
  260. package/src/core/weaver/loom.ts +125 -0
  261. package/src/core/weaver/thread-session.ts +51 -0
  262. package/src/core/weaver/thread.ts +196 -0
  263. package/src/core/weaver/weaver.ts +357 -0
  264. package/src/frontend/discord/actions/chat-info.ts +268 -0
  265. package/src/frontend/discord/actions/index.ts +77 -0
  266. package/src/frontend/discord/actions/media.ts +190 -0
  267. package/src/frontend/discord/actions/messaging.ts +277 -0
  268. package/src/frontend/discord/actions/shared.ts +100 -0
  269. package/src/frontend/discord/actions/types.ts +33 -0
  270. package/src/frontend/discord/admin.ts +13 -23
  271. package/src/frontend/discord/callbacks/autocomplete.ts +55 -0
  272. package/src/frontend/discord/{callbacks.ts → callbacks/components.ts} +150 -190
  273. package/src/frontend/discord/callbacks/index.ts +14 -0
  274. package/src/frontend/discord/callbacks/modals.ts +76 -0
  275. package/src/frontend/discord/callbacks/shared.ts +22 -0
  276. package/src/frontend/discord/commands/admin.ts +88 -0
  277. package/src/frontend/discord/commands/definitions.ts +200 -0
  278. package/src/frontend/discord/commands/index.ts +19 -0
  279. package/src/frontend/discord/commands/info.ts +106 -0
  280. package/src/frontend/discord/commands/router.ts +149 -0
  281. package/src/frontend/discord/commands/session.ts +89 -0
  282. package/src/frontend/discord/commands/settings.ts +405 -0
  283. package/src/frontend/discord/commands/shared.ts +50 -0
  284. package/src/frontend/discord/formatting.ts +8 -34
  285. package/src/frontend/discord/handlers/access.ts +248 -0
  286. package/src/frontend/discord/handlers/context.ts +161 -0
  287. package/src/frontend/discord/handlers/delivery.ts +94 -0
  288. package/src/frontend/discord/handlers/index.ts +34 -0
  289. package/src/frontend/discord/handlers/messages.ts +120 -0
  290. package/src/frontend/discord/handlers/queue.ts +135 -0
  291. package/src/frontend/discord/handlers/registry.ts +29 -0
  292. package/src/frontend/discord/handlers/state.ts +94 -0
  293. package/src/frontend/discord/helpers.ts +46 -57
  294. package/src/frontend/discord/index.ts +10 -7
  295. package/src/frontend/discord/middleware.ts +2 -2
  296. package/src/frontend/native/actions.ts +133 -0
  297. package/src/frontend/native/chats.ts +146 -0
  298. package/src/frontend/native/discovery.ts +50 -0
  299. package/src/frontend/native/index.ts +1227 -0
  300. package/src/frontend/native/logs.ts +95 -0
  301. package/src/frontend/native/protocol.ts +253 -0
  302. package/src/frontend/native/server.ts +612 -0
  303. package/src/frontend/native/settings.ts +205 -0
  304. package/src/frontend/native/turn-meta.ts +43 -0
  305. package/src/frontend/shared/format.ts +51 -0
  306. package/src/frontend/shared/reasoning-levels.ts +46 -0
  307. package/src/frontend/shared/session-status.ts +189 -0
  308. package/src/frontend/shared/status-context.ts +125 -0
  309. package/src/frontend/teams/actions.ts +7 -5
  310. package/src/frontend/teams/formatting.ts +7 -18
  311. package/src/frontend/teams/graph.ts +2 -1
  312. package/src/frontend/teams/index.ts +76 -54
  313. package/src/frontend/telegram/actions/chat-info.ts +323 -0
  314. package/src/frontend/telegram/actions/index.ts +68 -0
  315. package/src/frontend/telegram/actions/media.ts +224 -0
  316. package/src/frontend/telegram/actions/messaging.ts +295 -0
  317. package/src/frontend/telegram/actions/shared.ts +58 -0
  318. package/src/frontend/telegram/actions/types.ts +31 -0
  319. package/src/frontend/telegram/admin.ts +12 -22
  320. package/src/frontend/telegram/callbacks/effort.ts +68 -0
  321. package/src/frontend/telegram/callbacks/index.ts +68 -0
  322. package/src/frontend/telegram/callbacks/model.ts +311 -0
  323. package/src/frontend/telegram/callbacks/pulse.ts +50 -0
  324. package/src/frontend/telegram/callbacks/settings.ts +177 -0
  325. package/src/frontend/telegram/callbacks/shared.ts +71 -0
  326. package/src/frontend/telegram/commands/admin.ts +226 -0
  327. package/src/frontend/telegram/commands/definitions.ts +60 -0
  328. package/src/frontend/telegram/commands/index.ts +39 -0
  329. package/src/frontend/telegram/commands/info.ts +125 -0
  330. package/src/frontend/telegram/commands/session.ts +77 -0
  331. package/src/frontend/telegram/commands/settings.ts +345 -0
  332. package/src/frontend/telegram/commands/state.ts +34 -0
  333. package/src/frontend/telegram/formatting.ts +14 -25
  334. package/src/frontend/telegram/handlers/access.ts +287 -0
  335. package/src/frontend/telegram/handlers/context.ts +210 -0
  336. package/src/frontend/telegram/handlers/delivery.ts +236 -0
  337. package/src/frontend/telegram/handlers/index.ts +39 -0
  338. package/src/frontend/telegram/handlers/messages.ts +477 -0
  339. package/src/frontend/telegram/handlers/queue.ts +241 -0
  340. package/src/frontend/telegram/handlers/state.ts +75 -0
  341. package/src/frontend/telegram/helpers/diagnostics.ts +188 -0
  342. package/src/frontend/telegram/helpers/format.ts +41 -0
  343. package/src/frontend/telegram/helpers/index.ts +13 -0
  344. package/src/frontend/telegram/{helpers.ts → helpers/menu.ts} +209 -210
  345. package/src/frontend/telegram/index.ts +24 -27
  346. package/src/frontend/telegram/middleware.ts +16 -3
  347. package/src/frontend/telegram/model-callbacks.ts +14 -0
  348. package/src/frontend/telegram/model-menu.ts +317 -0
  349. package/src/frontend/telegram/sticker-library.ts +178 -0
  350. package/src/frontend/telegram/userbot.ts +2 -44
  351. package/src/frontend/terminal/commands.ts +47 -41
  352. package/src/frontend/terminal/index.ts +46 -31
  353. package/src/frontend/terminal/input.ts +5 -0
  354. package/src/frontend/terminal/renderer.ts +4 -2
  355. package/src/index.ts +13 -171
  356. package/src/login.ts +3 -2
  357. package/src/native/blake3-wasm-bytes.ts +7 -0
  358. package/src/native/blake3.ts +234 -0
  359. package/src/native/htmlents-wasm-bytes.ts +7 -0
  360. package/src/native/htmlents.ts +56 -0
  361. package/src/native/prelude.d.mts +168 -0
  362. package/src/native/prelude.mjs +1561 -0
  363. package/src/native/registry.ts +142 -0
  364. package/src/native/runtime.ts +164 -0
  365. package/src/native/scheduler-core/gleam.d.mts +2 -0
  366. package/src/native/scheduler-core/gleam.mjs +1 -0
  367. package/src/native/scheduler-core/scheduler_core.d.mts +76 -0
  368. package/src/native/scheduler-core/scheduler_core.mjs +252 -0
  369. package/src/native/scheduler-core.ts +124 -0
  370. package/src/native/sqlguard-wasm-bytes.ts +7 -0
  371. package/src/native/sqlguard.ts +107 -0
  372. package/src/native/strsim-wasm-bytes.ts +7 -0
  373. package/src/native/strsim.ts +98 -0
  374. package/src/native/textops-wasm-bytes.ts +7 -0
  375. package/src/native/textops.ts +83 -0
  376. package/src/native/warden.ts +287 -0
  377. package/src/plugins/github/index.ts +1 -1
  378. package/src/plugins/mempalace/index.ts +1 -1
  379. package/src/plugins/playwright/index.ts +1 -1
  380. package/src/storage/chat-settings.ts +335 -85
  381. package/src/storage/cron-store.ts +266 -109
  382. package/src/storage/daily-log.ts +15 -3
  383. package/src/storage/db.ts +206 -0
  384. package/src/storage/goal-store.ts +186 -0
  385. package/src/storage/history.ts +137 -188
  386. package/src/storage/kv.ts +52 -0
  387. package/src/storage/legacy-import.ts +47 -0
  388. package/src/storage/media-index.ts +118 -61
  389. package/src/storage/repositories/chat-settings-repo.ts +60 -0
  390. package/src/storage/repositories/cron-repo.ts +128 -0
  391. package/src/storage/repositories/goals-repo.ts +122 -0
  392. package/src/storage/repositories/history-repo.ts +207 -0
  393. package/src/storage/repositories/kv-repo.ts +25 -0
  394. package/src/storage/repositories/media-index-repo.ts +147 -0
  395. package/src/storage/repositories/scripts-repo.ts +81 -0
  396. package/src/storage/repositories/sessions-repo.ts +129 -0
  397. package/src/storage/repositories/triggers-repo.ts +158 -0
  398. package/src/storage/repositories/turn-meta-repo.ts +32 -0
  399. package/src/storage/scheduled-store.ts +84 -0
  400. package/src/storage/script-store.ts +198 -0
  401. package/src/storage/sessions.ts +227 -102
  402. package/src/storage/skill-store.ts +325 -0
  403. package/src/storage/sql/README.md +18 -0
  404. package/src/storage/sql/chat-settings.sql +11 -0
  405. package/src/storage/sql/cron.sql +40 -0
  406. package/src/storage/sql/db.sql +11 -0
  407. package/src/storage/sql/embed.ts +118 -0
  408. package/src/storage/sql/goals.sql +41 -0
  409. package/src/storage/sql/history.sql +83 -0
  410. package/src/storage/sql/kv.sql +11 -0
  411. package/src/storage/sql/media-index.sql +45 -0
  412. package/src/storage/sql/schema.sql +233 -0
  413. package/src/storage/sql/scripts.sql +28 -0
  414. package/src/storage/sql/sessions.sql +22 -0
  415. package/src/storage/sql/statements.generated.ts +500 -0
  416. package/src/storage/sql/triggers.sql +69 -0
  417. package/src/storage/sql/turn-meta.sql +24 -0
  418. package/src/storage/sticker-store.ts +141 -0
  419. package/src/storage/trigger-store.ts +104 -126
  420. package/src/storage/turn-meta.ts +118 -0
  421. package/src/util/chat-id.ts +30 -0
  422. package/src/util/config.ts +313 -195
  423. package/src/util/fs-path.ts +32 -0
  424. package/src/util/log.ts +53 -31
  425. package/src/util/mcp-launcher.ts +351 -33
  426. package/src/util/paths.ts +45 -17
  427. package/src/util/tail-file.ts +24 -0
  428. package/src/util/workspace.ts +161 -10
  429. package/tsconfig.json +1 -0
  430. package/src/backend/claude-sdk/models.ts +0 -501
  431. package/src/backend/codex/handler.ts +0 -595
  432. package/src/backend/kilo/handler.ts +0 -707
  433. package/src/backend/kilo/models.ts +0 -762
  434. package/src/backend/openai-agents/handler.ts +0 -539
  435. package/src/backend/openai-agents/mcp.ts +0 -139
  436. package/src/backend/opencode/handler.ts +0 -663
  437. package/src/backend/opencode/models.ts +0 -741
  438. package/src/core/cron.ts +0 -184
  439. package/src/core/dispatcher.ts +0 -144
  440. package/src/core/gateway-actions.ts +0 -603
  441. package/src/core/heartbeat.ts +0 -575
  442. package/src/core/plugin.ts +0 -776
  443. package/src/core/triggers.ts +0 -640
  444. package/src/frontend/discord/actions.ts +0 -729
  445. package/src/frontend/discord/commands.ts +0 -1036
  446. package/src/frontend/discord/handlers.ts +0 -798
  447. package/src/frontend/telegram/actions.ts +0 -731
  448. package/src/frontend/telegram/callbacks.ts +0 -419
  449. package/src/frontend/telegram/commands.ts +0 -642
  450. package/src/frontend/telegram/handlers.ts +0 -1352
  451. package/src/util/mcp-launcher.mjs +0 -140
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Access control — DM whitelist, guild/channel allowlists, admin checks,
3
+ * mention/respond gating, unauthorized notifications, DM-user tracking, and
4
+ * per-user rate limiting.
5
+ */
6
+
7
+ import type { Client, Message } from "discord.js";
8
+ import { ChannelType } from "discord.js";
9
+ import { appendDailyLog } from "../../../storage/daily-log.js";
10
+ import { log, logWarn, logDebug } from "../../../util/log.js";
11
+ import { escapeMarkdown } from "../formatting.js";
12
+ import {
13
+ accessState,
14
+ knownDmUsers,
15
+ KNOWN_DM_USERS_CAP,
16
+ unauthorizedCooldown,
17
+ UNAUTHORIZED_COOLDOWN_MS,
18
+ MAX_UNAUTHORIZED_COOLDOWNS,
19
+ userMessageTimestamps,
20
+ RATE_LIMIT_WINDOW_MS,
21
+ RATE_LIMIT_MAX_MESSAGES,
22
+ type AccessConfig,
23
+ } from "./state.js";
24
+
25
+ export function setAccessControl(cfg: {
26
+ allowedUsers: string[];
27
+ allowedGuilds: string[];
28
+ allowedChannels: string[];
29
+ adminUserIds: string[];
30
+ respondMode: "mention" | "channel";
31
+ }): void {
32
+ accessState.access = {
33
+ allowedUsers: new Set(cfg.allowedUsers),
34
+ allowedGuilds: new Set(cfg.allowedGuilds),
35
+ allowedChannels: new Set(cfg.allowedChannels),
36
+ adminUserIds: new Set(cfg.adminUserIds),
37
+ respondMode: cfg.respondMode,
38
+ };
39
+ }
40
+
41
+ export function isAdmin(userId: string): boolean {
42
+ return accessState.access.adminUserIds.has(userId);
43
+ }
44
+
45
+ export function getAccessSnapshot(): AccessConfig {
46
+ return accessState.access;
47
+ }
48
+
49
+ export function trackDmUser(
50
+ senderId: string,
51
+ senderName: string,
52
+ tag?: string,
53
+ ): void {
54
+ if (knownDmUsers.has(senderId)) return;
55
+ if (knownDmUsers.size >= KNOWN_DM_USERS_CAP) {
56
+ const evictCount = Math.floor(KNOWN_DM_USERS_CAP * 0.1);
57
+ const iter = knownDmUsers.values();
58
+ for (let i = 0; i < evictCount; i++) {
59
+ knownDmUsers.delete(iter.next().value as string);
60
+ }
61
+ }
62
+ knownDmUsers.add(senderId);
63
+ const tagStr = tag ? ` (${tag})` : "";
64
+ log("users", `New DM user: ${senderName}${tagStr} [id:${senderId}]`);
65
+ appendDailyLog(
66
+ "System",
67
+ `New DM user: ${senderName}${tagStr} [id:${senderId}]`,
68
+ );
69
+ }
70
+
71
+ async function notifyUnauthorized(
72
+ client: Client,
73
+ msg: Message,
74
+ type: "dm" | "guild" | "channel",
75
+ ): Promise<void> {
76
+ const userId = msg.author.id;
77
+ const guildId = msg.guildId ?? "";
78
+ const channelId = msg.channelId;
79
+ const key =
80
+ type === "dm"
81
+ ? `dm:${userId}`
82
+ : type === "guild"
83
+ ? `guild:${guildId}`
84
+ : `channel:${channelId}`;
85
+ const now = Date.now();
86
+ const last = unauthorizedCooldown.get(key);
87
+ if (last && now - last < UNAUTHORIZED_COOLDOWN_MS) return;
88
+ if (unauthorizedCooldown.size >= MAX_UNAUTHORIZED_COOLDOWNS) {
89
+ unauthorizedCooldown.clear();
90
+ }
91
+ unauthorizedCooldown.set(key, now);
92
+
93
+ const senderTag = msg.author.username ? ` (@${msg.author.username})` : "";
94
+ const senderName = msg.author.globalName || msg.author.username || "User";
95
+
96
+ // Warn the user
97
+ try {
98
+ if (msg.channel.isSendable()) {
99
+ await msg.channel.send({
100
+ content:
101
+ "⚠️ Unauthorized access. This bot is private. This attempt has been reported to the bot owner.",
102
+ allowedMentions: { parse: [] },
103
+ });
104
+ }
105
+ } catch {
106
+ /* can't send — ignore */
107
+ }
108
+
109
+ // Notify admins via DM. User-supplied names (sender, guild, channel) are
110
+ // escaped so a nick containing ** or || can't deform the admin's message.
111
+ const safeSender = escapeMarkdown(`${senderName}${senderTag}`);
112
+ const safeGuildName = escapeMarkdown(msg.guild?.name ?? guildId);
113
+ const safeChannelName = escapeMarkdown(
114
+ (msg.channel as { name?: string }).name ?? channelId,
115
+ );
116
+ for (const adminId of accessState.access.adminUserIds) {
117
+ try {
118
+ const admin = await client.users.fetch(adminId);
119
+ const detail =
120
+ type === "dm"
121
+ ? `🚨 Unauthorized DM from ${safeSender} [id:${userId}]`
122
+ : type === "guild"
123
+ ? `🚨 Unauthorized guild access: "${safeGuildName}" [id:${guildId}] by ${safeSender}`
124
+ : `🚨 Unauthorized channel access: #${safeChannelName} in "${safeGuildName}" by ${safeSender}`;
125
+ await admin.send({ content: detail, allowedMentions: { parse: [] } });
126
+ } catch {
127
+ /* admin unreachable */
128
+ }
129
+ }
130
+
131
+ logWarn(
132
+ "access",
133
+ `Unauthorized ${type}: ${senderName}${senderTag} [id:${userId}] guild=${guildId} channel=${channelId}`,
134
+ );
135
+ }
136
+
137
+ /** Check if a DM user is allowed. */
138
+ function isDmAllowed(userId: string): boolean {
139
+ return accessState.access.allowedUsers.has(userId);
140
+ }
141
+
142
+ /**
143
+ * Full access check for plain messages. Returns true if message should be processed.
144
+ * Notifies user + admins on rejection (with cooldown).
145
+ */
146
+ export async function isAccessAllowed(
147
+ client: Client,
148
+ msg: Message,
149
+ ): Promise<boolean> {
150
+ if (msg.channel.type === ChannelType.DM) {
151
+ if (isDmAllowed(msg.author.id)) return true;
152
+ await notifyUnauthorized(client, msg, "dm");
153
+ return false;
154
+ }
155
+ // Guild context
156
+ const guildId = msg.guildId;
157
+ if (!guildId) return false; // unknown — deny
158
+
159
+ if (!accessState.access.allowedGuilds.has(guildId)) {
160
+ await notifyUnauthorized(client, msg, "guild");
161
+ return false;
162
+ }
163
+ if (
164
+ accessState.access.allowedChannels.size > 0 &&
165
+ !accessState.access.allowedChannels.has(msg.channelId)
166
+ ) {
167
+ await notifyUnauthorized(client, msg, "channel");
168
+ return false;
169
+ }
170
+ return true;
171
+ }
172
+
173
+ /**
174
+ * For interactions (slash commands, buttons): synchronous predicate without
175
+ * side effects, since the interaction has its own ephemeral reply path.
176
+ */
177
+ export function isInteractionAllowed(
178
+ inGuild: boolean,
179
+ userId: string,
180
+ guildId: string | null,
181
+ channelId: string | null,
182
+ ): { ok: true } | { ok: false; reason: string } {
183
+ if (!inGuild) {
184
+ if (isDmAllowed(userId)) return { ok: true };
185
+ return { ok: false, reason: "DM access not authorized." };
186
+ }
187
+ if (!guildId)
188
+ return { ok: false, reason: "Could not determine guild context." };
189
+ if (!accessState.access.allowedGuilds.has(guildId))
190
+ return { ok: false, reason: "This server is not authorized." };
191
+ if (
192
+ accessState.access.allowedChannels.size > 0 &&
193
+ channelId &&
194
+ !accessState.access.allowedChannels.has(channelId)
195
+ )
196
+ return { ok: false, reason: "This channel is not authorized." };
197
+ return { ok: true };
198
+ }
199
+
200
+ /**
201
+ * Whether a guild message should activate the bot.
202
+ * - mode "mention": only if @bot or reply-to-bot
203
+ * - mode "channel": any message inside an allowedChannels channel (whitelist
204
+ * must be set; otherwise we fall back to mention to avoid being noisy).
205
+ */
206
+ export function shouldHandleInGuild(client: Client, msg: Message): boolean {
207
+ if (msg.channel.type === ChannelType.DM) return true;
208
+
209
+ const botId = client.user?.id;
210
+ const mentioned = botId ? msg.mentions.users.has(botId) : false;
211
+ const repliedToBot =
212
+ msg.reference?.messageId && msg.mentions.repliedUser?.id === botId;
213
+
214
+ if (
215
+ accessState.access.respondMode === "channel" &&
216
+ accessState.access.allowedChannels.size > 0
217
+ ) {
218
+ return accessState.access.allowedChannels.has(msg.channelId);
219
+ }
220
+ return Boolean(mentioned || repliedToBot);
221
+ }
222
+
223
+ export function isUserRateLimited(senderId: string): boolean {
224
+ const now = Date.now();
225
+ let timestamps = userMessageTimestamps.get(senderId);
226
+ if (!timestamps) {
227
+ timestamps = [];
228
+ userMessageTimestamps.set(senderId, timestamps);
229
+ }
230
+ while (timestamps.length > 0 && timestamps[0] < now - RATE_LIMIT_WINDOW_MS) {
231
+ timestamps.shift();
232
+ }
233
+ if (timestamps.length >= RATE_LIMIT_MAX_MESSAGES) {
234
+ logDebug("bot", `Rate-limited user ${senderId}`);
235
+ return true;
236
+ }
237
+ timestamps.push(now);
238
+ if (userMessageTimestamps.size > 5_000) {
239
+ const cutoff = now - 10 * 60_000;
240
+ for (const [userId, ts] of userMessageTimestamps) {
241
+ if (ts.length === 0 || ts[ts.length - 1] < cutoff) {
242
+ userMessageTimestamps.delete(userId);
243
+ }
244
+ if (userMessageTimestamps.size <= 2_500) break;
245
+ }
246
+ }
247
+ return false;
248
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Message-context helpers — sender name, reply context, attachment download +
3
+ * classification, and the chunked sender used by both replies and tool calls.
4
+ */
5
+
6
+ import { writeFileSync, mkdirSync, existsSync } from "node:fs";
7
+ import { resolve } from "node:path";
8
+ import type { Message, Attachment, TextBasedChannel } from "discord.js";
9
+ import {
10
+ splitMessage,
11
+ suppressMentions,
12
+ DISCORD_MAX_TEXT,
13
+ } from "../formatting.js";
14
+
15
+ export function getSenderName(msg: Message): string {
16
+ return (
17
+ (msg.member?.displayName || msg.author.globalName || msg.author.username) ??
18
+ "User"
19
+ );
20
+ }
21
+
22
+ export function buildReplyContext(
23
+ msg: Message,
24
+ botId: string | undefined,
25
+ ): string {
26
+ const ref = msg.reference;
27
+ if (!ref || !ref.messageId) return "";
28
+ // We don't always have the message cached. Discord.js provides
29
+ // msg.fetchReference() but that's async; we keep this synchronous and rely
30
+ // on the partial info already available via mentions.repliedUser.
31
+ const replied = msg.mentions.repliedUser;
32
+ if (!replied) return `[Replying to msg_id:${ref.messageId}]\n\n`;
33
+ const author =
34
+ replied.id === botId
35
+ ? "bot"
36
+ : replied.globalName || replied.username || "User";
37
+ return `[Replying to ${author} msg_id:${ref.messageId}]\n\n`;
38
+ }
39
+
40
+ // ── Attachment download ──────────────────────────────────────────────────────
41
+
42
+ const ATTACHMENT_MAX_BYTES = 50 * 1024 * 1024;
43
+
44
+ export async function downloadAttachment(
45
+ attachment: Attachment,
46
+ workspace: string,
47
+ ): Promise<string> {
48
+ if (attachment.size && attachment.size > ATTACHMENT_MAX_BYTES) {
49
+ throw new Error(
50
+ `File too large (${Math.round(attachment.size / 1024 / 1024)}MB, max ${ATTACHMENT_MAX_BYTES / 1024 / 1024}MB).`,
51
+ );
52
+ }
53
+ const resp = await fetch(attachment.url);
54
+ if (!resp.ok) throw new Error(`Download failed: ${resp.status}`);
55
+ const buffer = Buffer.from(await resp.arrayBuffer());
56
+ if (buffer.length === 0) throw new Error("Downloaded file is empty.");
57
+
58
+ const safeName = (attachment.name || `att_${attachment.id}`).replace(
59
+ /[^a-zA-Z0-9._-]/g,
60
+ "_",
61
+ );
62
+ const uploadsDir = resolve(workspace, "uploads");
63
+ if (!existsSync(uploadsDir)) mkdirSync(uploadsDir, { recursive: true });
64
+ const dest = resolve(uploadsDir, `${Date.now()}-${safeName}`);
65
+ writeFileSync(dest, buffer);
66
+ return dest;
67
+ }
68
+
69
+ export function classifyAttachment(att: Attachment): {
70
+ type: "photo" | "document" | "voice" | "video" | "audio" | "animation";
71
+ promptLines: (savedPath: string) => string[];
72
+ } {
73
+ const ct = att.contentType ?? "";
74
+ const name = (att.name ?? "").toLowerCase();
75
+
76
+ if (ct.startsWith("image/") || /\.(png|jpe?g|webp|gif|bmp)$/.test(name)) {
77
+ if (ct === "image/gif" || name.endsWith(".gif")) {
78
+ return {
79
+ type: "animation",
80
+ promptLines: (p) => [
81
+ `User sent a GIF: "${att.name ?? att.id}".`,
82
+ `Saved to: ${p}`,
83
+ ],
84
+ };
85
+ }
86
+ return {
87
+ type: "photo",
88
+ promptLines: (p) => [
89
+ `User sent an image saved to: ${p}`,
90
+ "Read this file to view it. If you need to reference this image in future turns, re-read the file — image data does not persist between turns.",
91
+ ],
92
+ };
93
+ }
94
+ if (ct.startsWith("video/") || /\.(mp4|mov|webm|mkv)$/.test(name)) {
95
+ return {
96
+ type: "video",
97
+ promptLines: (p) => [
98
+ `User sent a video: "${att.name ?? att.id}".`,
99
+ `Saved to: ${p}`,
100
+ ],
101
+ };
102
+ }
103
+ if (
104
+ ct === "audio/ogg" ||
105
+ /voice/.test(name) ||
106
+ (att.waveform != null && ct.startsWith("audio/"))
107
+ ) {
108
+ return {
109
+ type: "voice",
110
+ promptLines: (p) => [
111
+ `User sent a voice message (${att.duration ?? "?"}s).`,
112
+ `Audio saved to: ${p}. You cannot transcribe audio — acknowledge it and respond based on context.`,
113
+ ],
114
+ };
115
+ }
116
+ if (ct.startsWith("audio/") || /\.(mp3|wav|flac|m4a|opus)$/.test(name)) {
117
+ return {
118
+ type: "audio",
119
+ promptLines: (p) => [
120
+ `User sent an audio file: "${att.name ?? att.id}".`,
121
+ `Saved to: ${p}`,
122
+ ],
123
+ };
124
+ }
125
+ return {
126
+ type: "document",
127
+ promptLines: (p) => [
128
+ `User sent a document: "${att.name ?? att.id}" (${ct || "unknown"}).`,
129
+ `Saved to: ${p}`,
130
+ "Read and process this file.",
131
+ ],
132
+ };
133
+ }
134
+
135
+ // ── Send helpers ─────────────────────────────────────────────────────────────
136
+
137
+ export async function sendChunked(
138
+ channel: TextBasedChannel,
139
+ text: string,
140
+ replyToId?: string,
141
+ ): Promise<string[]> {
142
+ const ids: string[] = [];
143
+ if (!text.trim()) return ids;
144
+ const chunks = splitMessage(suppressMentions(text), DISCORD_MAX_TEXT);
145
+ for (let i = 0; i < chunks.length; i++) {
146
+ const chunk = chunks[i];
147
+ if (!channel.isSendable()) continue;
148
+ // Let errors propagate so withRetry can classify + retry transient failures.
149
+ // Previously we swallowed all errors here, making the outer withRetry dead code.
150
+ const sent = await channel.send({
151
+ content: chunk,
152
+ allowedMentions: { parse: [] },
153
+ reply:
154
+ i === 0 && replyToId
155
+ ? { messageReference: replyToId, failIfNotExists: false }
156
+ : undefined,
157
+ });
158
+ ids.push(sent.id);
159
+ }
160
+ return ids;
161
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Response delivery — runs the agent and delivers each text block as a
3
+ * discrete (chunked) Discord message. Discord has no draft-edit streaming, so
4
+ * blocks are sent as they arrive.
5
+ */
6
+
7
+ import type { TextBasedChannel } from "discord.js";
8
+ import type { TalonConfig } from "../../../util/config.js";
9
+ import { execute } from "../../../core/engine/dispatcher.js";
10
+ import { toolInputToRecord } from "../../../core/agent-runtime/events.js";
11
+ import { appendDailyLogResponse } from "../../../storage/daily-log.js";
12
+ import { log } from "../../../util/log.js";
13
+ import { trackDmUser } from "./access.js";
14
+ import { sendChunked } from "./context.js";
15
+
16
+ export type ProcessAndReplyParams = {
17
+ config: TalonConfig;
18
+ chatId: string;
19
+ numericChatId: number;
20
+ replyToId: string;
21
+ messageId: string;
22
+ numericMessageId: number;
23
+ prompt: string;
24
+ senderName: string;
25
+ isGroup: boolean;
26
+ senderUsername?: string;
27
+ senderId: string;
28
+ channel: TextBasedChannel;
29
+ chatTitle?: string;
30
+ };
31
+
32
+ export async function processAndReply(p: ProcessAndReplyParams): Promise<void> {
33
+ const sentTextBlock = { value: false };
34
+ let firstChunkReplyTo: string | undefined = p.replyToId;
35
+
36
+ // Discord doesn't support draft message edits, so we use onTextBlock to
37
+ // deliver each text block as a discrete message (chunked if necessary).
38
+ const onTextBlock = async (text: string) => {
39
+ if (!text.trim()) return;
40
+ const ids = await sendChunked(p.channel, text, firstChunkReplyTo);
41
+ if (ids.length > 0) sentTextBlock.value = true;
42
+ // Subsequent blocks should not reply to the original — too noisy.
43
+ firstChunkReplyTo = undefined;
44
+ };
45
+
46
+ if (!p.isGroup && p.senderId) {
47
+ trackDmUser(p.senderId, p.senderName, p.senderUsername);
48
+ }
49
+
50
+ const result = await execute({
51
+ chatId: p.chatId,
52
+ numericChatId: p.numericChatId,
53
+ prompt: p.prompt,
54
+ senderName: p.senderName,
55
+ isGroup: p.isGroup,
56
+ // Use the real Discord snowflake string, not the hashed numeric.
57
+ // The hash collides with Telegram-style 32-bit IDs and Discord's API
58
+ // rejects it as "Unknown Message" when the model tries to react/edit.
59
+ messageId: p.messageId,
60
+ source: "message",
61
+ onEvent: async (event) => {
62
+ switch (event.type) {
63
+ case "assistant_message":
64
+ // Throw on delivery failure — the dispatcher rejects the ack.
65
+ await onTextBlock(event.text);
66
+ break;
67
+ case "tool_call": {
68
+ const input = toolInputToRecord(event.name, event.input);
69
+ if (
70
+ event.name === "send" &&
71
+ input.type === "text" &&
72
+ typeof input.text === "string"
73
+ ) {
74
+ appendDailyLogResponse("Talon", input.text, {
75
+ chatTitle: p.chatTitle,
76
+ });
77
+ }
78
+ break;
79
+ }
80
+ }
81
+ },
82
+ });
83
+
84
+ if (
85
+ result.bridgeMessageCount === 0 &&
86
+ !sentTextBlock.value &&
87
+ result.text?.trim()
88
+ ) {
89
+ log(
90
+ "bot",
91
+ `Suppressed fallback text (${result.text.length} chars) — no send tool used`,
92
+ );
93
+ }
94
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Discord message handlers — mirrors the telegram handlers/ layout.
3
+ *
4
+ * - `state` — shared maps/sets (chat registry, access config, queues,
5
+ * rate-limit windows, cooldowns)
6
+ * - `registry` — numeric/string chatId → Discord channel info accessors
7
+ * - `access` — DM/guild/channel allowlists, admin checks, mention gating,
8
+ * unauthorized handling, rate limiting
9
+ * - `context` — sender/reply context, attachment download/classify,
10
+ * chunked send
11
+ * - `delivery` — the agent run + reply pipeline (processAndReply)
12
+ * - `queue` — per-chat debounce queue
13
+ * - `messages` — the messageCreate handler (handleMessage)
14
+ *
15
+ * Re-exports the same public surface the old single-file module exposed.
16
+ */
17
+
18
+ export type { DiscordChatInfo } from "./registry.js";
19
+ export {
20
+ registerDiscordChat,
21
+ lookupDiscordChat,
22
+ lookupDiscordChatByString,
23
+ } from "./registry.js";
24
+ export {
25
+ setAccessControl,
26
+ isAdmin,
27
+ getAccessSnapshot,
28
+ isAccessAllowed,
29
+ isInteractionAllowed,
30
+ shouldHandleInGuild,
31
+ isUserRateLimited,
32
+ } from "./access.js";
33
+ export { getSenderName, sendChunked } from "./context.js";
34
+ export { handleMessage } from "./messages.js";
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Public message handler — the messageCreate entry point. Filters bots/self,
3
+ * applies access + rate limits, downloads attachments, builds the prompt, and
4
+ * enqueues for debounced processing.
5
+ */
6
+
7
+ import type { Client, Message } from "discord.js";
8
+ import { ChannelType } from "discord.js";
9
+ import type { TalonConfig } from "../../../util/config.js";
10
+ import { setMessageFilePath } from "../../../storage/history.js";
11
+ import { addMedia } from "../../../storage/media-index.js";
12
+ import { deriveNumericChatId } from "../../../util/chat-id.js";
13
+ import { logError } from "../../../util/log.js";
14
+ import {
15
+ shouldHandleInGuild,
16
+ isAccessAllowed,
17
+ isUserRateLimited,
18
+ } from "./access.js";
19
+ import {
20
+ getSenderName,
21
+ buildReplyContext,
22
+ downloadAttachment,
23
+ classifyAttachment,
24
+ } from "./context.js";
25
+ import { registerDiscordChat } from "./registry.js";
26
+ import { enqueueMessage } from "./queue.js";
27
+
28
+ export async function handleMessage(
29
+ client: Client,
30
+ msg: Message,
31
+ config: TalonConfig,
32
+ ): Promise<void> {
33
+ // Ignore bots, system messages, and our own messages
34
+ if (msg.author.bot || msg.system) return;
35
+ if (msg.author.id === client.user?.id) return;
36
+
37
+ if (!shouldHandleInGuild(client, msg)) return;
38
+ if (!(await isAccessAllowed(client, msg))) return;
39
+ if (isUserRateLimited(msg.author.id)) return;
40
+
41
+ const isGroup = msg.channel.type !== ChannelType.DM;
42
+ const chatId = isGroup
43
+ ? `discord_guild_${msg.guildId}_${msg.channelId}`
44
+ : `discord_dm_${msg.author.id}`;
45
+ const numericChatId = deriveNumericChatId(chatId);
46
+ const numericMessageId = deriveNumericChatId(msg.id);
47
+ const sender = getSenderName(msg);
48
+ const senderUsername = msg.author.username;
49
+ const channel = msg.channel;
50
+ const chatTitle = isGroup
51
+ ? `${msg.guild?.name ?? msg.guildId} #${(msg.channel as { name?: string }).name ?? msg.channelId}`
52
+ : undefined;
53
+
54
+ // Strip the leading bot mention from the text — common when users invoke the bot.
55
+ const botId = client.user?.id;
56
+ const mentionPattern = botId ? new RegExp(`<@!?${botId}>`, "g") : null;
57
+ const cleanedContent = mentionPattern
58
+ ? msg.content.replace(mentionPattern, "").trim()
59
+ : msg.content.trim();
60
+
61
+ // Build prompt parts
62
+ const replyCtx = buildReplyContext(msg, botId);
63
+ const promptParts: string[] = [replyCtx];
64
+
65
+ // Handle attachments — download each, register in media index/history, add to prompt.
66
+ if (msg.attachments.size > 0) {
67
+ for (const att of msg.attachments.values()) {
68
+ try {
69
+ const savedPath = await downloadAttachment(att, config.workspace);
70
+ const cls = classifyAttachment(att);
71
+ setMessageFilePath(chatId, numericMessageId, savedPath);
72
+ addMedia({
73
+ chatId,
74
+ msgId: numericMessageId,
75
+ senderName: sender,
76
+ type: cls.type,
77
+ filePath: savedPath,
78
+ caption: cleanedContent || undefined,
79
+ timestamp: Date.now(),
80
+ });
81
+ promptParts.push(...cls.promptLines(savedPath));
82
+ } catch (err) {
83
+ logError(
84
+ "bot",
85
+ `[${chatId}] attachment "${att.name}" download failed: ${err instanceof Error ? err.message : err}`,
86
+ );
87
+ promptParts.push(
88
+ `[Attachment "${att.name ?? "file"}" failed to download: ${err instanceof Error ? err.message : err}]`,
89
+ );
90
+ }
91
+ }
92
+ if (cleanedContent) promptParts.push(`Caption: ${cleanedContent}`);
93
+ } else if (cleanedContent) {
94
+ promptParts.push(cleanedContent);
95
+ }
96
+
97
+ const prompt = promptParts.filter(Boolean).join("\n");
98
+ if (!prompt.trim()) return;
99
+
100
+ registerDiscordChat({
101
+ channelId: msg.channelId,
102
+ guildId: msg.guildId,
103
+ userId: isGroup ? null : msg.author.id,
104
+ numericChatId,
105
+ chatId,
106
+ });
107
+
108
+ enqueueMessage(config, chatId, numericChatId, {
109
+ prompt,
110
+ replyToId: msg.id,
111
+ messageId: msg.id,
112
+ numericMessageId,
113
+ senderName: sender,
114
+ senderUsername,
115
+ senderId: msg.author.id,
116
+ isGroup,
117
+ channel,
118
+ chatTitle,
119
+ });
120
+ }