@geminixiang/mikan 0.2.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 (316) hide show
  1. package/CHANGELOG.md +324 -0
  2. package/LICENSE +22 -0
  3. package/README.md +297 -0
  4. package/dist/adapter.d.ts +134 -0
  5. package/dist/adapter.d.ts.map +1 -0
  6. package/dist/adapter.js +2 -0
  7. package/dist/adapter.js.map +1 -0
  8. package/dist/adapters/discord/bot.d.ts +63 -0
  9. package/dist/adapters/discord/bot.d.ts.map +1 -0
  10. package/dist/adapters/discord/bot.js +577 -0
  11. package/dist/adapters/discord/bot.js.map +1 -0
  12. package/dist/adapters/discord/context.d.ts +9 -0
  13. package/dist/adapters/discord/context.d.ts.map +1 -0
  14. package/dist/adapters/discord/context.js +245 -0
  15. package/dist/adapters/discord/context.js.map +1 -0
  16. package/dist/adapters/discord/index.d.ts +3 -0
  17. package/dist/adapters/discord/index.d.ts.map +1 -0
  18. package/dist/adapters/discord/index.js +3 -0
  19. package/dist/adapters/discord/index.js.map +1 -0
  20. package/dist/adapters/shared.d.ts +91 -0
  21. package/dist/adapters/shared.d.ts.map +1 -0
  22. package/dist/adapters/shared.js +191 -0
  23. package/dist/adapters/shared.js.map +1 -0
  24. package/dist/adapters/slack/bot.d.ts +139 -0
  25. package/dist/adapters/slack/bot.d.ts.map +1 -0
  26. package/dist/adapters/slack/bot.js +1272 -0
  27. package/dist/adapters/slack/bot.js.map +1 -0
  28. package/dist/adapters/slack/branch-manager.d.ts +28 -0
  29. package/dist/adapters/slack/branch-manager.d.ts.map +1 -0
  30. package/dist/adapters/slack/branch-manager.js +117 -0
  31. package/dist/adapters/slack/branch-manager.js.map +1 -0
  32. package/dist/adapters/slack/context.d.ts +12 -0
  33. package/dist/adapters/slack/context.d.ts.map +1 -0
  34. package/dist/adapters/slack/context.js +327 -0
  35. package/dist/adapters/slack/context.js.map +1 -0
  36. package/dist/adapters/slack/index.d.ts +3 -0
  37. package/dist/adapters/slack/index.d.ts.map +1 -0
  38. package/dist/adapters/slack/index.js +3 -0
  39. package/dist/adapters/slack/index.js.map +1 -0
  40. package/dist/adapters/slack/session.d.ts +38 -0
  41. package/dist/adapters/slack/session.d.ts.map +1 -0
  42. package/dist/adapters/slack/session.js +66 -0
  43. package/dist/adapters/slack/session.js.map +1 -0
  44. package/dist/adapters/slack/tools/attach.d.ts +12 -0
  45. package/dist/adapters/slack/tools/attach.d.ts.map +1 -0
  46. package/dist/adapters/slack/tools/attach.js +40 -0
  47. package/dist/adapters/slack/tools/attach.js.map +1 -0
  48. package/dist/adapters/telegram/bot.d.ts +51 -0
  49. package/dist/adapters/telegram/bot.d.ts.map +1 -0
  50. package/dist/adapters/telegram/bot.js +430 -0
  51. package/dist/adapters/telegram/bot.js.map +1 -0
  52. package/dist/adapters/telegram/context.d.ts +9 -0
  53. package/dist/adapters/telegram/context.d.ts.map +1 -0
  54. package/dist/adapters/telegram/context.js +190 -0
  55. package/dist/adapters/telegram/context.js.map +1 -0
  56. package/dist/adapters/telegram/html.d.ts +3 -0
  57. package/dist/adapters/telegram/html.d.ts.map +1 -0
  58. package/dist/adapters/telegram/html.js +98 -0
  59. package/dist/adapters/telegram/html.js.map +1 -0
  60. package/dist/adapters/telegram/index.d.ts +3 -0
  61. package/dist/adapters/telegram/index.d.ts.map +1 -0
  62. package/dist/adapters/telegram/index.js +3 -0
  63. package/dist/adapters/telegram/index.js.map +1 -0
  64. package/dist/agent.d.ts +36 -0
  65. package/dist/agent.d.ts.map +1 -0
  66. package/dist/agent.js +1147 -0
  67. package/dist/agent.js.map +1 -0
  68. package/dist/commands/auto-reply.d.ts +5 -0
  69. package/dist/commands/auto-reply.d.ts.map +1 -0
  70. package/dist/commands/auto-reply.js +79 -0
  71. package/dist/commands/auto-reply.js.map +1 -0
  72. package/dist/commands/index.d.ts +5 -0
  73. package/dist/commands/index.d.ts.map +1 -0
  74. package/dist/commands/index.js +18 -0
  75. package/dist/commands/index.js.map +1 -0
  76. package/dist/commands/login.d.ts +5 -0
  77. package/dist/commands/login.d.ts.map +1 -0
  78. package/dist/commands/login.js +91 -0
  79. package/dist/commands/login.js.map +1 -0
  80. package/dist/commands/model.d.ts +14 -0
  81. package/dist/commands/model.d.ts.map +1 -0
  82. package/dist/commands/model.js +110 -0
  83. package/dist/commands/model.js.map +1 -0
  84. package/dist/commands/new.d.ts +5 -0
  85. package/dist/commands/new.d.ts.map +1 -0
  86. package/dist/commands/new.js +24 -0
  87. package/dist/commands/new.js.map +1 -0
  88. package/dist/commands/parse.d.ts +7 -0
  89. package/dist/commands/parse.d.ts.map +1 -0
  90. package/dist/commands/parse.js +17 -0
  91. package/dist/commands/parse.js.map +1 -0
  92. package/dist/commands/registry.d.ts +4 -0
  93. package/dist/commands/registry.d.ts.map +1 -0
  94. package/dist/commands/registry.js +9 -0
  95. package/dist/commands/registry.js.map +1 -0
  96. package/dist/commands/sandbox.d.ts +10 -0
  97. package/dist/commands/sandbox.d.ts.map +1 -0
  98. package/dist/commands/sandbox.js +83 -0
  99. package/dist/commands/sandbox.js.map +1 -0
  100. package/dist/commands/session-view.d.ts +5 -0
  101. package/dist/commands/session-view.d.ts.map +1 -0
  102. package/dist/commands/session-view.js +62 -0
  103. package/dist/commands/session-view.js.map +1 -0
  104. package/dist/commands/types.d.ts +41 -0
  105. package/dist/commands/types.d.ts.map +1 -0
  106. package/dist/commands/types.js +2 -0
  107. package/dist/commands/types.js.map +1 -0
  108. package/dist/commands/utils.d.ts +8 -0
  109. package/dist/commands/utils.d.ts.map +1 -0
  110. package/dist/commands/utils.js +14 -0
  111. package/dist/commands/utils.js.map +1 -0
  112. package/dist/config.d.ts +59 -0
  113. package/dist/config.d.ts.map +1 -0
  114. package/dist/config.js +370 -0
  115. package/dist/config.js.map +1 -0
  116. package/dist/context.d.ts +17 -0
  117. package/dist/context.d.ts.map +1 -0
  118. package/dist/context.js +24 -0
  119. package/dist/context.js.map +1 -0
  120. package/dist/conversation-history.d.ts +16 -0
  121. package/dist/conversation-history.d.ts.map +1 -0
  122. package/dist/conversation-history.js +144 -0
  123. package/dist/conversation-history.js.map +1 -0
  124. package/dist/download.d.ts +2 -0
  125. package/dist/download.d.ts.map +1 -0
  126. package/dist/download.js +89 -0
  127. package/dist/download.js.map +1 -0
  128. package/dist/env.d.ts +3 -0
  129. package/dist/env.d.ts.map +1 -0
  130. package/dist/env.js +12 -0
  131. package/dist/env.js.map +1 -0
  132. package/dist/events.d.ts +85 -0
  133. package/dist/events.d.ts.map +1 -0
  134. package/dist/events.js +483 -0
  135. package/dist/events.js.map +1 -0
  136. package/dist/execution-resolver.d.ts +25 -0
  137. package/dist/execution-resolver.d.ts.map +1 -0
  138. package/dist/execution-resolver.js +167 -0
  139. package/dist/execution-resolver.js.map +1 -0
  140. package/dist/file-guards.d.ts +9 -0
  141. package/dist/file-guards.d.ts.map +1 -0
  142. package/dist/file-guards.js +56 -0
  143. package/dist/file-guards.js.map +1 -0
  144. package/dist/fs-atomic.d.ts +10 -0
  145. package/dist/fs-atomic.d.ts.map +1 -0
  146. package/dist/fs-atomic.js +45 -0
  147. package/dist/fs-atomic.js.map +1 -0
  148. package/dist/index.d.ts +10 -0
  149. package/dist/index.d.ts.map +1 -0
  150. package/dist/index.js +7 -0
  151. package/dist/index.js.map +1 -0
  152. package/dist/instrument.d.ts +2 -0
  153. package/dist/instrument.d.ts.map +1 -0
  154. package/dist/instrument.js +10 -0
  155. package/dist/instrument.js.map +1 -0
  156. package/dist/log.d.ts +36 -0
  157. package/dist/log.d.ts.map +1 -0
  158. package/dist/log.js +206 -0
  159. package/dist/log.js.map +1 -0
  160. package/dist/login/index.d.ts +42 -0
  161. package/dist/login/index.d.ts.map +1 -0
  162. package/dist/login/index.js +239 -0
  163. package/dist/login/index.js.map +1 -0
  164. package/dist/login/portal.d.ts +19 -0
  165. package/dist/login/portal.d.ts.map +1 -0
  166. package/dist/login/portal.js +1544 -0
  167. package/dist/login/portal.js.map +1 -0
  168. package/dist/login/session.d.ts +26 -0
  169. package/dist/login/session.d.ts.map +1 -0
  170. package/dist/login/session.js +56 -0
  171. package/dist/login/session.js.map +1 -0
  172. package/dist/main.d.ts +3 -0
  173. package/dist/main.d.ts.map +1 -0
  174. package/dist/main.js +366 -0
  175. package/dist/main.js.map +1 -0
  176. package/dist/provisioner.d.ts +83 -0
  177. package/dist/provisioner.d.ts.map +1 -0
  178. package/dist/provisioner.js +500 -0
  179. package/dist/provisioner.js.map +1 -0
  180. package/dist/runtime/conversation-orchestrator.d.ts +40 -0
  181. package/dist/runtime/conversation-orchestrator.d.ts.map +1 -0
  182. package/dist/runtime/conversation-orchestrator.js +183 -0
  183. package/dist/runtime/conversation-orchestrator.js.map +1 -0
  184. package/dist/runtime/index.d.ts +2 -0
  185. package/dist/runtime/index.d.ts.map +1 -0
  186. package/dist/runtime/index.js +2 -0
  187. package/dist/runtime/index.js.map +1 -0
  188. package/dist/runtime/session-runtime.d.ts +26 -0
  189. package/dist/runtime/session-runtime.d.ts.map +1 -0
  190. package/dist/runtime/session-runtime.js +221 -0
  191. package/dist/runtime/session-runtime.js.map +1 -0
  192. package/dist/sandbox/cloudflare.d.ts +15 -0
  193. package/dist/sandbox/cloudflare.d.ts.map +1 -0
  194. package/dist/sandbox/cloudflare.js +138 -0
  195. package/dist/sandbox/cloudflare.js.map +1 -0
  196. package/dist/sandbox/container.d.ts +16 -0
  197. package/dist/sandbox/container.d.ts.map +1 -0
  198. package/dist/sandbox/container.js +138 -0
  199. package/dist/sandbox/container.js.map +1 -0
  200. package/dist/sandbox/errors.d.ts +6 -0
  201. package/dist/sandbox/errors.d.ts.map +1 -0
  202. package/dist/sandbox/errors.js +11 -0
  203. package/dist/sandbox/errors.js.map +1 -0
  204. package/dist/sandbox/firecracker.d.ts +17 -0
  205. package/dist/sandbox/firecracker.d.ts.map +1 -0
  206. package/dist/sandbox/firecracker.js +212 -0
  207. package/dist/sandbox/firecracker.js.map +1 -0
  208. package/dist/sandbox/host.d.ts +11 -0
  209. package/dist/sandbox/host.d.ts.map +1 -0
  210. package/dist/sandbox/host.js +89 -0
  211. package/dist/sandbox/host.js.map +1 -0
  212. package/dist/sandbox/image.d.ts +5 -0
  213. package/dist/sandbox/image.d.ts.map +1 -0
  214. package/dist/sandbox/image.js +30 -0
  215. package/dist/sandbox/image.js.map +1 -0
  216. package/dist/sandbox/index.d.ts +22 -0
  217. package/dist/sandbox/index.d.ts.map +1 -0
  218. package/dist/sandbox/index.js +54 -0
  219. package/dist/sandbox/index.js.map +1 -0
  220. package/dist/sandbox/path-context.d.ts +4 -0
  221. package/dist/sandbox/path-context.d.ts.map +1 -0
  222. package/dist/sandbox/path-context.js +20 -0
  223. package/dist/sandbox/path-context.js.map +1 -0
  224. package/dist/sandbox/types.d.ts +67 -0
  225. package/dist/sandbox/types.d.ts.map +1 -0
  226. package/dist/sandbox/types.js +2 -0
  227. package/dist/sandbox/types.js.map +1 -0
  228. package/dist/sandbox/utils.d.ts +4 -0
  229. package/dist/sandbox/utils.d.ts.map +1 -0
  230. package/dist/sandbox/utils.js +51 -0
  231. package/dist/sandbox/utils.js.map +1 -0
  232. package/dist/sentry.d.ts +50 -0
  233. package/dist/sentry.d.ts.map +1 -0
  234. package/dist/sentry.js +257 -0
  235. package/dist/sentry.js.map +1 -0
  236. package/dist/session-view/command.d.ts +5 -0
  237. package/dist/session-view/command.d.ts.map +1 -0
  238. package/dist/session-view/command.js +7 -0
  239. package/dist/session-view/command.js.map +1 -0
  240. package/dist/session-view/portal.d.ts +16 -0
  241. package/dist/session-view/portal.d.ts.map +1 -0
  242. package/dist/session-view/portal.js +1822 -0
  243. package/dist/session-view/portal.js.map +1 -0
  244. package/dist/session-view/service.d.ts +34 -0
  245. package/dist/session-view/service.d.ts.map +1 -0
  246. package/dist/session-view/service.js +434 -0
  247. package/dist/session-view/service.js.map +1 -0
  248. package/dist/session-view/store.d.ts +18 -0
  249. package/dist/session-view/store.d.ts.map +1 -0
  250. package/dist/session-view/store.js +36 -0
  251. package/dist/session-view/store.js.map +1 -0
  252. package/dist/sessions/metadata.d.ts +15 -0
  253. package/dist/sessions/metadata.d.ts.map +1 -0
  254. package/dist/sessions/metadata.js +11 -0
  255. package/dist/sessions/metadata.js.map +1 -0
  256. package/dist/sessions/policy.d.ts +13 -0
  257. package/dist/sessions/policy.d.ts.map +1 -0
  258. package/dist/sessions/policy.js +23 -0
  259. package/dist/sessions/policy.js.map +1 -0
  260. package/dist/sessions/store.d.ts +103 -0
  261. package/dist/sessions/store.d.ts.map +1 -0
  262. package/dist/sessions/store.js +349 -0
  263. package/dist/sessions/store.js.map +1 -0
  264. package/dist/store.d.ts +58 -0
  265. package/dist/store.d.ts.map +1 -0
  266. package/dist/store.js +152 -0
  267. package/dist/store.js.map +1 -0
  268. package/dist/tool-diagnostics.d.ts +2 -0
  269. package/dist/tool-diagnostics.d.ts.map +1 -0
  270. package/dist/tool-diagnostics.js +7 -0
  271. package/dist/tool-diagnostics.js.map +1 -0
  272. package/dist/tools/bash.d.ts +10 -0
  273. package/dist/tools/bash.d.ts.map +1 -0
  274. package/dist/tools/bash.js +80 -0
  275. package/dist/tools/bash.js.map +1 -0
  276. package/dist/tools/edit.d.ts +11 -0
  277. package/dist/tools/edit.d.ts.map +1 -0
  278. package/dist/tools/edit.js +133 -0
  279. package/dist/tools/edit.js.map +1 -0
  280. package/dist/tools/event.d.ts +62 -0
  281. package/dist/tools/event.d.ts.map +1 -0
  282. package/dist/tools/event.js +138 -0
  283. package/dist/tools/event.js.map +1 -0
  284. package/dist/tools/index.d.ts +14 -0
  285. package/dist/tools/index.d.ts.map +1 -0
  286. package/dist/tools/index.js +23 -0
  287. package/dist/tools/index.js.map +1 -0
  288. package/dist/tools/read.d.ts +11 -0
  289. package/dist/tools/read.d.ts.map +1 -0
  290. package/dist/tools/read.js +136 -0
  291. package/dist/tools/read.js.map +1 -0
  292. package/dist/tools/truncate.d.ts +57 -0
  293. package/dist/tools/truncate.d.ts.map +1 -0
  294. package/dist/tools/truncate.js +184 -0
  295. package/dist/tools/truncate.js.map +1 -0
  296. package/dist/tools/write.d.ts +10 -0
  297. package/dist/tools/write.d.ts.map +1 -0
  298. package/dist/tools/write.js +33 -0
  299. package/dist/tools/write.js.map +1 -0
  300. package/dist/trigger.d.ts +31 -0
  301. package/dist/trigger.d.ts.map +1 -0
  302. package/dist/trigger.js +98 -0
  303. package/dist/trigger.js.map +1 -0
  304. package/dist/ui-copy.d.ts +12 -0
  305. package/dist/ui-copy.d.ts.map +1 -0
  306. package/dist/ui-copy.js +36 -0
  307. package/dist/ui-copy.js.map +1 -0
  308. package/dist/vault-routing.d.ts +4 -0
  309. package/dist/vault-routing.d.ts.map +1 -0
  310. package/dist/vault-routing.js +16 -0
  311. package/dist/vault-routing.js.map +1 -0
  312. package/dist/vault.d.ts +72 -0
  313. package/dist/vault.d.ts.map +1 -0
  314. package/dist/vault.js +281 -0
  315. package/dist/vault.js.map +1 -0
  316. package/package.json +83 -0
@@ -0,0 +1,577 @@
1
+ import { ApplicationCommandOptionType, Client, Events, GatewayIntentBits, Partials, } from "discord.js";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { basename, join } from "path";
4
+ import * as log from "../../log.js";
5
+ import { resolveChatSessionKey } from "../../sessions/policy.js";
6
+ import { evaluateAutoReplyPolicy } from "../../trigger.js";
7
+ import { formatNothingRunning } from "../../ui-copy.js";
8
+ import { appendBotResponseLog, appendChannelLog, ChannelQueue, resolveOnlyScopedStopTarget, resolveStopTarget, withRetry, } from "../shared.js";
9
+ import { createDiscordAdapters } from "./context.js";
10
+ // discord.js: DiscordAPIError exposes `.status` (HTTP status) and a `.code`.
11
+ // RateLimitError fires when the internal queue gives up. Both should retry.
12
+ function discordIsRateLimited(err) {
13
+ if (err.status === 429)
14
+ return true;
15
+ if (err.httpStatus === 429)
16
+ return true;
17
+ if (err.name === "RateLimitError")
18
+ return true;
19
+ return false;
20
+ }
21
+ const discordRetry = (fn) => withRetry(fn, { isRateLimited: discordIsRateLimited });
22
+ // ============================================================================
23
+ // DiscordBot
24
+ // ============================================================================
25
+ export class DiscordBot {
26
+ constructor(handler, config) {
27
+ this.botUserId = null;
28
+ this.queues = new Map();
29
+ this.startupTime = 0;
30
+ this.channels = new Map();
31
+ this.users = new Map();
32
+ this.handler = handler;
33
+ this.token = config.token;
34
+ this.workingDir = config.workingDir;
35
+ this.client = new Client({
36
+ intents: [
37
+ GatewayIntentBits.Guilds,
38
+ GatewayIntentBits.GuildMessages,
39
+ GatewayIntentBits.MessageContent,
40
+ GatewayIntentBits.DirectMessages,
41
+ ],
42
+ partials: [Partials.Channel, Partials.Message],
43
+ });
44
+ }
45
+ // ==========================================================================
46
+ // Public API (implements Bot)
47
+ // ==========================================================================
48
+ async start() {
49
+ await new Promise((resolve, reject) => {
50
+ this.client.once(Events.ClientReady, async (readyClient) => {
51
+ this.botUserId = readyClient.user.id;
52
+ this.startupTime = Date.now();
53
+ log.logConnected("Discord");
54
+ log.logInfo(`Discord bot started as ${readyClient.user.tag}`);
55
+ this.loadCachedGuildData();
56
+ this.setupEventHandlers();
57
+ try {
58
+ await readyClient.application.commands.set([
59
+ {
60
+ name: "login",
61
+ description: "Store credentials in your private vault",
62
+ },
63
+ {
64
+ name: "session",
65
+ description: "Open the current session in the web viewer",
66
+ },
67
+ {
68
+ name: "new",
69
+ description: "Reset conversation history and start fresh",
70
+ },
71
+ {
72
+ name: "stop",
73
+ description: "Stop the current conversation",
74
+ },
75
+ {
76
+ name: "model",
77
+ description: "Switch this conversation's LLM model",
78
+ options: [
79
+ {
80
+ name: "model",
81
+ description: "provider/model[:thinking], e.g. anthropic/claude-sonnet-4-6:off",
82
+ type: ApplicationCommandOptionType.String,
83
+ required: false,
84
+ },
85
+ ],
86
+ },
87
+ {
88
+ name: "sandbox",
89
+ description: "Show or temporarily boost this conversation's sandbox limits",
90
+ options: [
91
+ {
92
+ name: "action",
93
+ description: "Use 'boost' to temporarily apply the configured boost limits",
94
+ type: ApplicationCommandOptionType.String,
95
+ required: false,
96
+ },
97
+ ],
98
+ },
99
+ ]);
100
+ }
101
+ catch (err) {
102
+ log.logWarning("Failed to register Discord slash commands", err instanceof Error ? err.message : String(err));
103
+ }
104
+ resolve();
105
+ });
106
+ this.client.once(Events.Error, reject);
107
+ this.client.login(this.token).catch(reject);
108
+ });
109
+ }
110
+ async postMessage(channel, text) {
111
+ return discordRetry(async () => {
112
+ const ch = await this.fetchTextChannel(channel);
113
+ const msg = await ch.send(text);
114
+ return msg.id;
115
+ });
116
+ }
117
+ async updateMessage(channel, ts, text) {
118
+ await this.updateMessageRaw(channel, ts, text);
119
+ }
120
+ enqueueEvent(event) {
121
+ const conversationId = event.conversationId;
122
+ const queue = this.getQueue(conversationId);
123
+ if (queue.size() >= 5) {
124
+ log.logWarning(`Event queue full for ${conversationId}, discarding: ${event.text.substring(0, 50)}`);
125
+ return false;
126
+ }
127
+ log.logInfo(`Enqueueing event for ${conversationId}: ${event.text.substring(0, 50)}`);
128
+ queue.enqueue(() => {
129
+ const adapters = createDiscordAdapters(event, this);
130
+ return this.handler.handleEvent(event, this, adapters);
131
+ });
132
+ return true;
133
+ }
134
+ getPlatformInfo() {
135
+ return {
136
+ name: "discord",
137
+ formattingGuide: "## Discord Formatting (Markdown)\nBold: **text**, Italic: *text*, Code: `code`, Block: ```language\ncode```\nLinks: [text](url)",
138
+ channels: this.getAllChannels(),
139
+ users: this.getAllUsers(),
140
+ diagnostics: {
141
+ showUsageSummary: false,
142
+ },
143
+ };
144
+ }
145
+ // ==========================================================================
146
+ // Internal helpers (used by context.ts)
147
+ // ==========================================================================
148
+ async updateMessageRaw(channelId, messageId, text) {
149
+ return discordRetry(async () => {
150
+ const ch = await this.fetchTextChannel(channelId);
151
+ const msg = await ch.messages.fetch(messageId);
152
+ await msg.edit(text);
153
+ });
154
+ }
155
+ async postReply(channelId, replyToId, text) {
156
+ return discordRetry(async () => {
157
+ const ch = await this.fetchTextChannel(channelId);
158
+ const replyTarget = await ch.messages.fetch(replyToId);
159
+ const sent = await replyTarget.reply(text);
160
+ return sent.id;
161
+ });
162
+ }
163
+ async postInThread(channelId, threadOrMessageId, text) {
164
+ // Try as a thread channel first, then fall back to posting in the channel
165
+ try {
166
+ const thread = await this.client.channels.fetch(threadOrMessageId);
167
+ if (thread && (thread.isThread() || thread.isTextBased())) {
168
+ return discordRetry(async () => {
169
+ const msg = await thread.send(text);
170
+ return msg.id;
171
+ });
172
+ }
173
+ }
174
+ catch {
175
+ // Not a thread channel, treat as message ID for reply
176
+ }
177
+ return this.postReply(channelId, threadOrMessageId, text);
178
+ }
179
+ async deleteMessageRaw(channelId, messageId) {
180
+ try {
181
+ const ch = await this.fetchTextChannel(channelId);
182
+ const msg = await ch.messages.fetch(messageId);
183
+ await msg.delete();
184
+ }
185
+ catch {
186
+ // Ignore if already deleted
187
+ }
188
+ }
189
+ async sendTyping(channelId) {
190
+ try {
191
+ const ch = await this.fetchTextChannel(channelId);
192
+ await ch.sendTyping();
193
+ }
194
+ catch {
195
+ // Non-fatal
196
+ }
197
+ }
198
+ async uploadFile(channelId, filePath, title) {
199
+ return discordRetry(async () => {
200
+ const ch = await this.fetchTextChannel(channelId);
201
+ const fileName = title ?? basename(filePath);
202
+ const fileContent = readFileSync(filePath);
203
+ await ch.send({ files: [{ attachment: fileContent, name: fileName }] });
204
+ });
205
+ }
206
+ async sendDirectMessage(userId, text) {
207
+ return discordRetry(async () => {
208
+ const user = await this.client.users.fetch(userId);
209
+ const msg = await user.send(text);
210
+ return msg.id;
211
+ });
212
+ }
213
+ async postPrivate(_conversationId, userId, text) {
214
+ await this.sendDirectMessage(userId, text);
215
+ }
216
+ getAllChannels() {
217
+ return Array.from(this.channels.values());
218
+ }
219
+ getAllUsers() {
220
+ return Array.from(this.users.values());
221
+ }
222
+ logToFile(channelId, entry) {
223
+ appendChannelLog(this.workingDir, channelId, entry);
224
+ }
225
+ logBotResponse(channelId, text, ts) {
226
+ appendBotResponseLog(this.workingDir, channelId, text, ts);
227
+ }
228
+ /**
229
+ * Process attachments from a Discord message.
230
+ * Downloads files before returning so the agent can read them immediately.
231
+ */
232
+ async processAttachments(channelId, attachments, _messageId) {
233
+ const downloads = [];
234
+ // Discord attachments Collection - iterate over values
235
+ for (const attachment of attachments.values()) {
236
+ if (!attachment.name) {
237
+ log.logWarning("Discord attachment missing name, skipping", attachment.url);
238
+ continue;
239
+ }
240
+ const ts = Date.now();
241
+ const sanitizedName = attachment.name.replace(/[^a-zA-Z0-9._-]/g, "_");
242
+ const filename = `${ts}_${sanitizedName}`;
243
+ const localPath = `${channelId}/attachments/${filename}`;
244
+ const fullDir = join(this.workingDir, channelId, "attachments");
245
+ const result = {
246
+ name: attachment.name,
247
+ localPath,
248
+ };
249
+ downloads.push(this.downloadAttachment(fullDir, filename, attachment.url)
250
+ .then(() => result)
251
+ .catch((err) => {
252
+ log.logWarning(`Failed to download Discord attachment`, `${filename}: ${err}`);
253
+ return null;
254
+ }));
255
+ }
256
+ const results = await Promise.all(downloads);
257
+ return results.filter((attachment) => attachment !== null);
258
+ }
259
+ /**
260
+ * Download an attachment from URL to local file
261
+ */
262
+ async downloadAttachment(dir, filename, url) {
263
+ if (!existsSync(dir))
264
+ mkdirSync(dir, { recursive: true });
265
+ try {
266
+ const response = await fetch(url);
267
+ if (!response.ok) {
268
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
269
+ }
270
+ const buffer = await response.arrayBuffer();
271
+ writeFileSync(join(dir, filename), Buffer.from(buffer));
272
+ }
273
+ catch (err) {
274
+ throw new Error(`Download failed: ${err instanceof Error ? err.message : String(err)}`, {
275
+ cause: err,
276
+ });
277
+ }
278
+ }
279
+ // ==========================================================================
280
+ // Private - Event Handlers
281
+ // ==========================================================================
282
+ getQueue(channelId) {
283
+ let queue = this.queues.get(channelId);
284
+ if (!queue) {
285
+ queue = new ChannelQueue("Discord");
286
+ this.queues.set(channelId, queue);
287
+ }
288
+ return queue;
289
+ }
290
+ resolveStopTarget(channelId, sessionKey) {
291
+ const directTarget = resolveStopTarget({
292
+ handler: this.handler,
293
+ conversationId: channelId,
294
+ sessionKey,
295
+ });
296
+ if (directTarget)
297
+ return directTarget;
298
+ if (sessionKey !== channelId)
299
+ return null;
300
+ return resolveOnlyScopedStopTarget(this.handler, channelId);
301
+ }
302
+ loadCachedGuildData() {
303
+ for (const guild of this.client.guilds.cache.values()) {
304
+ for (const channel of guild.channels.cache.values()) {
305
+ if (channel.isTextBased() && "name" in channel) {
306
+ this.channels.set(channel.id, { id: channel.id, name: channel.name ?? channel.id });
307
+ }
308
+ }
309
+ for (const member of guild.members.cache.values()) {
310
+ this.users.set(member.id, {
311
+ id: member.id,
312
+ userName: member.user.username,
313
+ displayName: member.displayName,
314
+ });
315
+ }
316
+ }
317
+ }
318
+ stripBotMention(text) {
319
+ if (!this.botUserId)
320
+ return text;
321
+ return text.replace(new RegExp(`<@!?${this.botUserId}>`, "g"), "").trim();
322
+ }
323
+ resolveConversationContext(input) {
324
+ if (!input.inGuild) {
325
+ return {
326
+ conversationId: input.channelId,
327
+ threadTs: input.referencedMsgId,
328
+ };
329
+ }
330
+ if (input.isThread) {
331
+ return {
332
+ conversationId: input.parentChannelId ?? input.channelId,
333
+ threadTs: input.channelId,
334
+ };
335
+ }
336
+ return {
337
+ conversationId: input.channelId,
338
+ threadTs: input.referencedMsgId,
339
+ };
340
+ }
341
+ createSlashCommandAdapters(interaction, commandText, sessionKey, conversationId) {
342
+ const isDM = !interaction.inGuild();
343
+ const userId = interaction.user.id;
344
+ const userName = interaction.user.username;
345
+ const platform = this.getPlatformInfo();
346
+ const shouldUseEphemeral = !isDM;
347
+ const message = {
348
+ id: interaction.id,
349
+ sessionKey,
350
+ conversationKind: isDM ? "direct" : "shared",
351
+ userId,
352
+ userName,
353
+ text: commandText,
354
+ attachments: [],
355
+ };
356
+ const respondPrivately = async (text, replace = false) => {
357
+ if (interaction.replied || interaction.deferred) {
358
+ if (replace) {
359
+ await interaction.editReply({ content: text });
360
+ }
361
+ else {
362
+ await interaction.followUp({ content: text, ephemeral: shouldUseEphemeral });
363
+ }
364
+ return;
365
+ }
366
+ await interaction.reply({ content: text, ephemeral: shouldUseEphemeral });
367
+ };
368
+ const responseCtx = {
369
+ respond: async (text) => {
370
+ await respondPrivately(text);
371
+ },
372
+ replaceResponse: async (text) => {
373
+ await respondPrivately(text, true);
374
+ },
375
+ respondDiagnostic: async (text) => {
376
+ await respondPrivately(text);
377
+ },
378
+ respondToolResult: async (result) => {
379
+ const duration = (result.durationMs / 1000).toFixed(1);
380
+ const formatted = `${result.isError ? "Error" : "Done"} ${result.toolName} (${duration}s)\n${result.result}`;
381
+ await respondPrivately(formatted);
382
+ },
383
+ setTyping: async () => { },
384
+ setWorking: async () => { },
385
+ uploadFile: async (filePath, title) => {
386
+ await this.uploadFile(conversationId, filePath, title);
387
+ },
388
+ deleteResponse: async () => { },
389
+ };
390
+ return { message, responseCtx, platform };
391
+ }
392
+ setupEventHandlers() {
393
+ this.client.on(Events.InteractionCreate, async (interaction) => {
394
+ if (!interaction.isChatInputCommand())
395
+ return;
396
+ if (interaction.commandName !== "login" &&
397
+ interaction.commandName !== "session" &&
398
+ interaction.commandName !== "new" &&
399
+ interaction.commandName !== "stop" &&
400
+ interaction.commandName !== "model" &&
401
+ interaction.commandName !== "sandbox") {
402
+ return;
403
+ }
404
+ const isDM = !interaction.inGuild();
405
+ const { conversationId, threadTs } = this.resolveConversationContext({
406
+ channelId: interaction.channelId,
407
+ inGuild: interaction.inGuild(),
408
+ isThread: interaction.channel?.isThread() ?? false,
409
+ parentChannelId: interaction.channel && "parentId" in interaction.channel
410
+ ? interaction.channel.parentId
411
+ : null,
412
+ });
413
+ const sessionKey = resolveChatSessionKey({
414
+ conversationId,
415
+ conversationKind: isDM ? "direct" : "shared",
416
+ messageId: interaction.id,
417
+ persistentTopLevel: true,
418
+ threadTs,
419
+ });
420
+ const modelOption = interaction.commandName === "model"
421
+ ? interaction.options.getString("model")?.trim()
422
+ : undefined;
423
+ const sandboxAction = interaction.commandName === "sandbox"
424
+ ? interaction.options.getString("action")?.trim()
425
+ : undefined;
426
+ const commandArg = modelOption ?? sandboxAction;
427
+ const commandText = commandArg
428
+ ? `/${interaction.commandName} ${commandArg}`
429
+ : `/${interaction.commandName}`;
430
+ this.logToFile(conversationId, {
431
+ date: new Date(interaction.createdTimestamp).toISOString(),
432
+ ts: interaction.id,
433
+ ...(threadTs ? { threadTs } : {}),
434
+ user: interaction.user.id,
435
+ userName: interaction.user.username,
436
+ text: commandText,
437
+ attachments: [],
438
+ isBot: false,
439
+ });
440
+ const adapters = this.createSlashCommandAdapters(interaction, commandText, sessionKey, conversationId);
441
+ try {
442
+ if (interaction.commandName === "new") {
443
+ await this.handler.handleNewCommand(sessionKey, conversationId, this);
444
+ return;
445
+ }
446
+ if (interaction.commandName === "stop") {
447
+ const stopTarget = this.resolveStopTarget(conversationId, sessionKey);
448
+ if (stopTarget) {
449
+ await this.handler.handleStop(stopTarget, conversationId, this);
450
+ }
451
+ else {
452
+ await adapters.responseCtx.respond(formatNothingRunning("discord"));
453
+ }
454
+ return;
455
+ }
456
+ const event = {
457
+ type: "dm",
458
+ conversationId,
459
+ conversationKind: isDM ? "direct" : "shared",
460
+ ts: interaction.id,
461
+ thread_ts: threadTs,
462
+ sessionKey,
463
+ user: interaction.user.id,
464
+ text: commandText,
465
+ attachments: [],
466
+ };
467
+ await this.handler.handleEvent(event, this, adapters);
468
+ }
469
+ catch (err) {
470
+ log.logWarning("Discord slash command error", err instanceof Error ? err.message : String(err));
471
+ if (!interaction.replied && !interaction.deferred) {
472
+ await interaction.reply({
473
+ content: `${interaction.commandName} command failed. Please try again later.`,
474
+ ephemeral: !isDM,
475
+ });
476
+ }
477
+ }
478
+ });
479
+ this.client.on(Events.MessageCreate, async (msg) => {
480
+ // Skip messages from before startup
481
+ if (msg.createdTimestamp < this.startupTime)
482
+ return;
483
+ // Skip bot messages
484
+ if (msg.author.bot)
485
+ return;
486
+ const isDM = msg.channel.type === 1; // ChannelType.DM = 1
487
+ const isInThread = msg.channel.isThread();
488
+ const referencedMsgId = msg.reference?.messageId;
489
+ const isThreadReply = isInThread || !!referencedMsgId;
490
+ const isMentioned = msg.mentions.users.has(this.botUserId ?? "");
491
+ const isAutoReplyCandidate = !isDM && !isMentioned && !isThreadReply;
492
+ const { conversationId, threadTs } = this.resolveConversationContext({
493
+ channelId: msg.channelId,
494
+ inGuild: !isDM,
495
+ isThread: isInThread,
496
+ parentChannelId: "parentId" in msg.channel ? msg.channel.parentId : null,
497
+ referencedMsgId,
498
+ });
499
+ const userId = msg.author.id;
500
+ const userName = msg.author.username;
501
+ const msgId = msg.id;
502
+ // Track user
503
+ this.users.set(userId, {
504
+ id: userId,
505
+ userName,
506
+ displayName: msg.member?.displayName ?? userName,
507
+ });
508
+ // Track channel
509
+ if (!this.channels.has(conversationId) && "name" in msg.channel) {
510
+ const ch = msg.channel;
511
+ this.channels.set(conversationId, { id: conversationId, name: ch.name });
512
+ }
513
+ const conversationKind = isDM ? "direct" : "shared";
514
+ const sessionKey = resolveChatSessionKey({
515
+ conversationId,
516
+ conversationKind,
517
+ messageId: msgId,
518
+ persistentTopLevel: true,
519
+ threadTs,
520
+ });
521
+ const cleanedText = this.stripBotMention(msg.content);
522
+ const eventBase = {
523
+ type: isDM ? "dm" : "mention",
524
+ conversationId,
525
+ conversationKind,
526
+ ts: msgId,
527
+ thread_ts: threadTs,
528
+ sessionKey,
529
+ user: userId,
530
+ userName,
531
+ text: cleanedText,
532
+ };
533
+ // Handle stop before trigger gate — "stop" should never be auto-reply judged.
534
+ if (cleanedText.toLowerCase() === "stop" || cleanedText.toLowerCase() === "/stop") {
535
+ const stopTarget = this.resolveStopTarget(conversationId, sessionKey);
536
+ if (stopTarget) {
537
+ this.handler.handleStop(stopTarget, conversationId, this);
538
+ }
539
+ else if (!isAutoReplyCandidate) {
540
+ await this.postMessage(conversationId, formatNothingRunning("discord"));
541
+ }
542
+ return;
543
+ }
544
+ const triggerResult = isAutoReplyCandidate
545
+ ? await evaluateAutoReplyPolicy({ event: eventBase, workingDir: this.workingDir })
546
+ : { trigger: true, reason: "addressed" };
547
+ const logEntryBase = {
548
+ date: msg.createdAt.toISOString(),
549
+ ts: msgId,
550
+ ...(!isDM && threadTs ? { threadTs } : {}),
551
+ user: userId,
552
+ userName,
553
+ text: cleanedText,
554
+ isBot: false,
555
+ };
556
+ if (!triggerResult.trigger) {
557
+ this.logToFile(conversationId, { ...logEntryBase, attachments: [] });
558
+ return;
559
+ }
560
+ const processedAttachments = await this.processAttachments(conversationId, msg.attachments, msgId);
561
+ const event = { ...eventBase, attachments: processedAttachments };
562
+ this.logToFile(conversationId, { ...logEntryBase, attachments: processedAttachments });
563
+ this.getQueue(sessionKey).enqueue(() => {
564
+ const adapters = createDiscordAdapters(event, this);
565
+ return this.handler.handleEvent(event, this, adapters);
566
+ });
567
+ });
568
+ }
569
+ async fetchTextChannel(channelId) {
570
+ const ch = await this.client.channels.fetch(channelId);
571
+ if (!ch || !ch.isTextBased()) {
572
+ throw new Error(`Channel ${channelId} is not a text channel`);
573
+ }
574
+ return ch;
575
+ }
576
+ }
577
+ //# sourceMappingURL=bot.js.map