mono-pilot 0.2.9 → 0.2.12

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 (158) hide show
  1. package/README.md +270 -7
  2. package/dist/src/agents-paths.js +36 -0
  3. package/dist/src/brief/blocks.js +83 -0
  4. package/dist/src/brief/defaults.js +60 -0
  5. package/dist/src/brief/frontmatter.js +53 -0
  6. package/dist/src/brief/paths.js +10 -0
  7. package/dist/src/brief/reflection.js +27 -0
  8. package/dist/src/cli.js +62 -5
  9. package/dist/src/cluster/bus.js +102 -0
  10. package/dist/src/cluster/follower.js +137 -0
  11. package/dist/src/cluster/init.js +182 -0
  12. package/dist/src/cluster/leader.js +97 -0
  13. package/dist/src/cluster/log.js +49 -0
  14. package/dist/src/cluster/protocol.js +34 -0
  15. package/dist/src/cluster/services/bus.js +243 -0
  16. package/dist/src/cluster/services/embedding.js +12 -0
  17. package/dist/src/cluster/socket.js +86 -0
  18. package/dist/src/cluster/test-bus.js +175 -0
  19. package/dist/src/cluster_v2/connection-lifecycle.js +31 -0
  20. package/dist/src/cluster_v2/connection-lifecycle.test.js +24 -0
  21. package/dist/src/cluster_v2/connection.js +159 -0
  22. package/dist/src/cluster_v2/connection.test.js +55 -0
  23. package/dist/src/cluster_v2/events.js +102 -0
  24. package/dist/src/cluster_v2/index.js +2 -0
  25. package/dist/src/cluster_v2/observability.js +99 -0
  26. package/dist/src/cluster_v2/observability.test.js +46 -0
  27. package/dist/src/cluster_v2/rpc.js +389 -0
  28. package/dist/src/cluster_v2/rpc.test.js +110 -0
  29. package/dist/src/cluster_v2/runtime.failover.integration.test.js +156 -0
  30. package/dist/src/cluster_v2/runtime.js +531 -0
  31. package/dist/src/cluster_v2/runtime.lease-compromise.integration.test.js +91 -0
  32. package/dist/src/cluster_v2/runtime.lifecycle.integration.test.js +225 -0
  33. package/dist/src/cluster_v2/services/bus.integration.test.js +140 -0
  34. package/dist/src/cluster_v2/services/bus.js +450 -0
  35. package/dist/src/cluster_v2/services/discord/auth-store.js +82 -0
  36. package/dist/src/cluster_v2/services/discord/collector.js +569 -0
  37. package/dist/src/cluster_v2/services/discord/index.js +1 -0
  38. package/dist/src/cluster_v2/services/discord/oauth.js +87 -0
  39. package/dist/src/cluster_v2/services/discord/rpc-client.js +325 -0
  40. package/dist/src/cluster_v2/services/embedding.js +66 -0
  41. package/dist/src/cluster_v2/services/registry-cache.js +107 -0
  42. package/dist/src/cluster_v2/services/registry-cache.test.js +66 -0
  43. package/dist/src/cluster_v2/services/registry.js +36 -0
  44. package/dist/src/cluster_v2/services/twitter/collector.js +1055 -0
  45. package/dist/src/cluster_v2/services/twitter/index.js +1 -0
  46. package/dist/src/config/digest.js +78 -0
  47. package/dist/src/config/discord.js +143 -0
  48. package/dist/src/config/image-gen.js +48 -0
  49. package/dist/src/config/mono-pilot.js +31 -0
  50. package/dist/src/config/twitter.js +100 -0
  51. package/dist/src/extensions/cluster.js +311 -0
  52. package/dist/src/extensions/commands/build-memory.js +76 -0
  53. package/dist/src/extensions/commands/digest/backfill.js +779 -0
  54. package/dist/src/extensions/commands/digest/index.js +1133 -0
  55. package/dist/src/extensions/commands/image-model.js +214 -0
  56. package/dist/src/extensions/game/bus-injection.js +47 -0
  57. package/dist/src/extensions/game/identity.js +83 -0
  58. package/dist/src/extensions/game/mailbox.js +61 -0
  59. package/dist/src/extensions/game/system-prompt.js +134 -0
  60. package/dist/src/extensions/game/tools.js +28 -0
  61. package/dist/src/extensions/lifecycle.js +337 -0
  62. package/dist/src/extensions/mode-runtime.js +26 -2
  63. package/dist/src/extensions/mono-game.js +66 -0
  64. package/dist/src/extensions/mono-pilot.js +100 -18
  65. package/dist/src/extensions/nvim.js +47 -0
  66. package/dist/src/extensions/session-hints.js +60 -35
  67. package/dist/src/extensions/sftp.js +897 -0
  68. package/dist/src/extensions/status.js +676 -0
  69. package/dist/src/extensions/system-events.js +478 -0
  70. package/dist/src/extensions/system-prompt.js +24 -14
  71. package/dist/src/extensions/user-message.js +94 -50
  72. package/dist/src/lsp/client.js +235 -0
  73. package/dist/src/lsp/index.js +165 -0
  74. package/dist/src/lsp/runtime.js +67 -0
  75. package/dist/src/lsp/server.js +242 -0
  76. package/dist/src/mcp/config.js +112 -0
  77. package/dist/src/{utils/mcp-client.js → mcp/protocol.js} +1 -100
  78. package/dist/src/mcp/servers.js +90 -0
  79. package/dist/src/memory/build-memory.js +103 -0
  80. package/dist/src/memory/config/defaults.js +55 -0
  81. package/dist/src/memory/config/loader.js +29 -0
  82. package/dist/src/memory/config/paths.js +9 -0
  83. package/dist/src/memory/config/resolve.js +90 -0
  84. package/dist/src/memory/config/types.js +1 -0
  85. package/dist/src/memory/embeddings/batch-runner.js +39 -0
  86. package/dist/src/memory/embeddings/cache.js +47 -0
  87. package/dist/src/memory/embeddings/chunk-limits.js +26 -0
  88. package/dist/src/memory/embeddings/input-limits.js +48 -0
  89. package/dist/src/memory/embeddings/local.js +108 -0
  90. package/dist/src/memory/embeddings/types.js +1 -0
  91. package/dist/src/memory/index-manager.js +552 -0
  92. package/dist/src/memory/indexing/embeddings.js +67 -0
  93. package/dist/src/memory/indexing/files.js +180 -0
  94. package/dist/src/memory/indexing/index-file.js +105 -0
  95. package/dist/src/memory/log.js +38 -0
  96. package/dist/src/memory/paths.js +15 -0
  97. package/dist/src/memory/runtime/index.js +299 -0
  98. package/dist/src/memory/runtime/thread.js +116 -0
  99. package/dist/src/memory/search/fts.js +57 -0
  100. package/dist/src/memory/search/hybrid.js +50 -0
  101. package/dist/src/memory/search/text.js +30 -0
  102. package/dist/src/memory/search/vector.js +43 -0
  103. package/dist/src/memory/session/content-hash.js +7 -0
  104. package/dist/src/memory/session/entry.js +33 -0
  105. package/dist/src/memory/session/flush-policy.js +34 -0
  106. package/dist/src/memory/session/hook.js +191 -0
  107. package/dist/src/memory/session/paths.js +15 -0
  108. package/dist/src/memory/session/session-reader.js +88 -0
  109. package/dist/src/memory/session/transcript/content-hash.js +7 -0
  110. package/dist/src/memory/session/transcript/entry.js +28 -0
  111. package/dist/src/memory/session/transcript/flush.js +56 -0
  112. package/dist/src/memory/session/transcript/paths.js +28 -0
  113. package/dist/src/memory/session/transcript/reader.js +112 -0
  114. package/dist/src/memory/session/transcript/state.js +31 -0
  115. package/dist/src/memory/store/schema.js +89 -0
  116. package/dist/src/memory/store/sqlite.js +89 -0
  117. package/dist/src/memory/types.js +1 -0
  118. package/dist/src/memory/warm.js +25 -0
  119. package/dist/src/rules/discovery.js +41 -0
  120. package/dist/{tools → src/tools}/README.md +29 -3
  121. package/dist/{tools → src/tools}/apply-patch-description.md +8 -2
  122. package/dist/{tools → src/tools}/apply-patch.js +174 -104
  123. package/dist/{tools → src/tools}/apply-patch.test.js +52 -1
  124. package/dist/{tools/ask-question.js → src/tools/ask-user-question.js} +3 -3
  125. package/dist/src/tools/ast-grep.js +357 -0
  126. package/dist/src/tools/brief-write.js +122 -0
  127. package/dist/src/tools/bus-send.js +100 -0
  128. package/dist/{tools → src/tools}/call-mcp-tool.js +40 -124
  129. package/dist/src/tools/codex-apply-patch-description.md +52 -0
  130. package/dist/src/tools/codex-apply-patch.js +540 -0
  131. package/dist/{tools → src/tools}/delete.js +24 -0
  132. package/dist/src/tools/exit-plan-mode.js +83 -0
  133. package/dist/{tools → src/tools}/fetch-mcp-resource.js +56 -100
  134. package/dist/src/tools/generate-image.js +567 -0
  135. package/dist/{tools → src/tools}/glob.js +55 -1
  136. package/dist/{tools → src/tools}/list-mcp-resources.js +46 -57
  137. package/dist/{tools → src/tools}/list-mcp-tools.js +52 -63
  138. package/dist/src/tools/ls.js +48 -0
  139. package/dist/src/tools/lsp-diagnostics.js +67 -0
  140. package/dist/src/tools/lsp-symbols.js +54 -0
  141. package/dist/src/tools/mailbox.js +85 -0
  142. package/dist/src/tools/memory-get.js +90 -0
  143. package/dist/src/tools/memory-search.js +180 -0
  144. package/dist/{tools → src/tools}/plan-mode-reminder.md +3 -4
  145. package/dist/{tools → src/tools}/read-file.js +8 -19
  146. package/dist/{tools → src/tools}/rg.js +10 -20
  147. package/dist/{tools → src/tools}/shell.js +19 -42
  148. package/dist/{tools → src/tools}/subagent.js +255 -6
  149. package/dist/{tools → src/tools}/switch-mode.js +37 -6
  150. package/dist/{tools → src/tools}/web-fetch.js +105 -7
  151. package/dist/{tools → src/tools}/web-search.js +29 -1
  152. package/package.json +21 -9
  153. /package/dist/{tools → src/tools}/ask-mode-reminder.md +0 -0
  154. /package/dist/{tools → src/tools}/rg.test.js +0 -0
  155. /package/dist/{tools → src/tools}/semantic-search-description.md +0 -0
  156. /package/dist/{tools → src/tools}/semantic-search.js +0 -0
  157. /package/dist/{tools → src/tools}/shell-description.md +0 -0
  158. /package/dist/{tools → src/tools}/subagent-description.md +0 -0
@@ -0,0 +1,569 @@
1
+ import { appendFile, mkdir } from "node:fs/promises";
2
+ import { dirname, join, parse } from "node:path";
3
+ import { extractDiscordCollectorConfig, } from "../../../config/discord.js";
4
+ import { loadMonoPilotConfigObject } from "../../../config/mono-pilot.js";
5
+ import { emitClusterV2DiscordChannelBatch } from "../../events.js";
6
+ import { logClusterEvent } from "../../observability.js";
7
+ import { getAuthStorePath, readDiscordAuthToken, writeDiscordAuthToken, } from "./auth-store.js";
8
+ import { exchangeDiscordAuthorizeCode, tryRefreshDiscordToken } from "./oauth.js";
9
+ import { DiscordRpcClient } from "./rpc-client.js";
10
+ const INITIAL_RECONNECT_DELAY_MS = 1_000;
11
+ const TOKEN_EXPIRY_SKEW_MS = 60_000;
12
+ function isTerminalAuthErrorMessage(message) {
13
+ return (message.includes("invalid_client") ||
14
+ message.includes("access_denied") ||
15
+ message.includes("invalid_grant"));
16
+ }
17
+ function isRecord(value) {
18
+ return typeof value === "object" && value !== null && !Array.isArray(value);
19
+ }
20
+ function readString(value) {
21
+ return typeof value === "string" && value.length > 0 ? value : null;
22
+ }
23
+ function readAttachmentUrls(value) {
24
+ if (!Array.isArray(value)) {
25
+ return [];
26
+ }
27
+ const urls = [];
28
+ for (const item of value) {
29
+ if (!isRecord(item)) {
30
+ continue;
31
+ }
32
+ const url = readString(item.url) ?? readString(item.proxy_url);
33
+ if (url) {
34
+ urls.push(url);
35
+ }
36
+ }
37
+ return urls;
38
+ }
39
+ function readEmbedUrls(value) {
40
+ if (!Array.isArray(value)) {
41
+ return [];
42
+ }
43
+ const urls = [];
44
+ for (const item of value) {
45
+ if (!isRecord(item)) {
46
+ continue;
47
+ }
48
+ const candidates = [
49
+ readString(item.url),
50
+ isRecord(item.image) ? readString(item.image.url) : null,
51
+ isRecord(item.thumbnail) ? readString(item.thumbnail.url) : null,
52
+ isRecord(item.video) ? readString(item.video.url) : null,
53
+ ];
54
+ for (const candidate of candidates) {
55
+ if (candidate) {
56
+ urls.push(candidate);
57
+ }
58
+ }
59
+ }
60
+ return urls;
61
+ }
62
+ function sleep(ms) {
63
+ return new Promise((resolve) => setTimeout(resolve, ms));
64
+ }
65
+ function isTokenLikelyExpired(expiresAt) {
66
+ if (!expiresAt) {
67
+ return false;
68
+ }
69
+ const expiresTime = Date.parse(expiresAt);
70
+ if (!Number.isFinite(expiresTime)) {
71
+ return false;
72
+ }
73
+ return expiresTime - Date.now() <= TOKEN_EXPIRY_SKEW_MS;
74
+ }
75
+ function formatLocalDateStamp(date) {
76
+ const year = String(date.getFullYear());
77
+ const month = String(date.getMonth() + 1).padStart(2, "0");
78
+ const day = String(date.getDate()).padStart(2, "0");
79
+ return `${year}-${month}-${day}`;
80
+ }
81
+ class JsonlWriter {
82
+ outputPath;
83
+ logContext;
84
+ queue = Promise.resolve();
85
+ dirReady = false;
86
+ outputDir;
87
+ outputBaseName;
88
+ outputExtension;
89
+ currentDateStamp = null;
90
+ currentOutputPath = null;
91
+ constructor(outputPath, logContext) {
92
+ this.outputPath = outputPath;
93
+ this.logContext = logContext;
94
+ const parsed = parse(outputPath);
95
+ this.outputDir = parsed.dir;
96
+ this.outputBaseName = parsed.name || parsed.base || "messages";
97
+ this.outputExtension = parsed.ext || ".jsonl";
98
+ }
99
+ append(record) {
100
+ this.queue = this.queue
101
+ .then(async () => {
102
+ if (!this.dirReady) {
103
+ await mkdir(this.outputDir || dirname(this.outputPath), { recursive: true });
104
+ this.dirReady = true;
105
+ }
106
+ const currentPath = this.resolveCurrentOutputPath();
107
+ await appendFile(currentPath, `${JSON.stringify(record)}\n`, "utf-8");
108
+ })
109
+ .catch((error) => {
110
+ logClusterEvent("warn", "discord_collector_persist_failed", this.logContext, {
111
+ error: error instanceof Error ? error.message : String(error),
112
+ outputPath: this.outputPath,
113
+ currentOutputPath: this.currentOutputPath,
114
+ });
115
+ });
116
+ }
117
+ resolveCurrentOutputPath(now = new Date()) {
118
+ const dateStamp = formatLocalDateStamp(now);
119
+ if (this.currentDateStamp === dateStamp && this.currentOutputPath) {
120
+ return this.currentOutputPath;
121
+ }
122
+ this.currentDateStamp = dateStamp;
123
+ this.currentOutputPath = join(this.outputDir, `${this.outputBaseName}.${dateStamp}${this.outputExtension}`);
124
+ return this.currentOutputPath;
125
+ }
126
+ async flush() {
127
+ await this.queue;
128
+ }
129
+ }
130
+ class DiscordCollector {
131
+ config;
132
+ writer;
133
+ lifecycleContext;
134
+ loop = null;
135
+ closed = false;
136
+ client = null;
137
+ seq = 0;
138
+ reconnectDelayMs = INITIAL_RECONNECT_DELAY_MS;
139
+ authToken = null;
140
+ warnedOauthConfig = false;
141
+ channelContextCache = new Map();
142
+ channelContextInFlight = new Map();
143
+ guildNameCache = new Map();
144
+ guildNameInFlight = new Map();
145
+ channelAliasById = new Map();
146
+ channelMessageCounts = new Map();
147
+ channelBatchSequences = new Map();
148
+ descriptor = {
149
+ name: "discord_intel",
150
+ version: "v1",
151
+ capabilities: {
152
+ mode: "leader_local",
153
+ },
154
+ };
155
+ constructor(config, context) {
156
+ this.config = config;
157
+ this.lifecycleContext = {
158
+ ...context,
159
+ role: context.role ? `${context.role}:discord_intel` : "discord_intel",
160
+ };
161
+ for (const channel of this.config.channels) {
162
+ if (channel.alias) {
163
+ this.channelAliasById.set(channel.id, channel.alias);
164
+ }
165
+ }
166
+ this.writer = new JsonlWriter(this.config.outputPath, this.lifecycleContext);
167
+ this.descriptor = {
168
+ name: "discord_intel",
169
+ version: "v1",
170
+ capabilities: {
171
+ mode: "leader_local",
172
+ events: this.config.events,
173
+ channelCount: this.config.channels.length,
174
+ outputPath: this.config.outputPath,
175
+ },
176
+ };
177
+ }
178
+ start() {
179
+ if (this.loop) {
180
+ return;
181
+ }
182
+ this.loop = this.runLoop();
183
+ }
184
+ async close() {
185
+ if (this.closed) {
186
+ return;
187
+ }
188
+ this.closed = true;
189
+ this.client?.close();
190
+ if (this.loop) {
191
+ await this.loop;
192
+ }
193
+ await this.writer.flush();
194
+ logClusterEvent("info", "discord_collector_stopped", this.lifecycleContext);
195
+ }
196
+ async runLoop() {
197
+ logClusterEvent("info", "discord_collector_started", this.lifecycleContext, {
198
+ outputPath: this.config.outputPath,
199
+ events: this.config.events,
200
+ channelCount: this.config.channels.length,
201
+ });
202
+ while (!this.closed) {
203
+ try {
204
+ await this.runOnce();
205
+ this.reconnectDelayMs = INITIAL_RECONNECT_DELAY_MS;
206
+ }
207
+ catch (error) {
208
+ if (this.closed) {
209
+ break;
210
+ }
211
+ const message = error instanceof Error ? error.message : String(error);
212
+ if (isTerminalAuthErrorMessage(message)) {
213
+ logClusterEvent("error", "discord_collector_terminal_auth_error", this.lifecycleContext, {
214
+ error: message,
215
+ action: "stop_retries_until_restart",
216
+ authStorePath: getAuthStorePath(),
217
+ });
218
+ break;
219
+ }
220
+ logClusterEvent("warn", "discord_collector_cycle_failed", this.lifecycleContext, {
221
+ error: message,
222
+ retryInMs: this.reconnectDelayMs,
223
+ });
224
+ await sleep(this.reconnectDelayMs);
225
+ this.reconnectDelayMs = Math.min(this.reconnectDelayMs * 2, this.config.maxReconnectDelayMs);
226
+ }
227
+ }
228
+ }
229
+ async runOnce() {
230
+ if (this.closed) {
231
+ return;
232
+ }
233
+ const client = new DiscordRpcClient({
234
+ clientId: this.config.clientId ?? "",
235
+ });
236
+ this.client = client;
237
+ const unsubs = this.config.events.map((eventName) => client.onEvent(eventName, (payload) => {
238
+ void this.persistEvent(client, eventName, payload);
239
+ }));
240
+ try {
241
+ await client.connect();
242
+ let accessToken = await this.resolveAccessToken(client);
243
+ try {
244
+ await client.authenticate(accessToken);
245
+ }
246
+ catch (error) {
247
+ if (this.config.accessToken) {
248
+ throw error;
249
+ }
250
+ logClusterEvent("warn", "discord_collector_auth_token_rejected", this.lifecycleContext, {
251
+ error: error instanceof Error ? error.message : String(error),
252
+ });
253
+ const refreshed = await this.tryRefreshToken();
254
+ if (refreshed) {
255
+ accessToken = refreshed.accessToken;
256
+ await client.authenticate(accessToken);
257
+ }
258
+ else {
259
+ const interactive = await this.authorizeAndPersist(client);
260
+ accessToken = interactive.accessToken;
261
+ await client.authenticate(accessToken);
262
+ }
263
+ }
264
+ for (const channel of this.config.channels) {
265
+ for (const eventName of this.config.events) {
266
+ await client.subscribe(eventName, { channel_id: channel.id });
267
+ }
268
+ }
269
+ logClusterEvent("info", "discord_collector_connected", this.lifecycleContext, {
270
+ channels: this.config.channels.length,
271
+ events: this.config.events,
272
+ });
273
+ await client.waitForDisconnect();
274
+ if (!this.closed) {
275
+ throw new Error("discord ipc disconnected");
276
+ }
277
+ }
278
+ finally {
279
+ for (const unsub of unsubs) {
280
+ unsub();
281
+ }
282
+ client.close();
283
+ if (this.client === client) {
284
+ this.client = null;
285
+ }
286
+ }
287
+ }
288
+ async resolveAccessToken(client) {
289
+ if (this.config.accessToken) {
290
+ return this.config.accessToken;
291
+ }
292
+ await this.loadCachedAuthToken();
293
+ if (this.authToken && isTokenLikelyExpired(this.authToken.expiresAt)) {
294
+ const refreshed = await this.tryRefreshToken();
295
+ if (refreshed) {
296
+ return refreshed.accessToken;
297
+ }
298
+ }
299
+ if (this.authToken?.accessToken) {
300
+ return this.authToken.accessToken;
301
+ }
302
+ const interactive = await this.authorizeAndPersist(client);
303
+ return interactive.accessToken;
304
+ }
305
+ async loadCachedAuthToken() {
306
+ if (this.authToken || this.config.accessToken || !this.config.clientId) {
307
+ return;
308
+ }
309
+ try {
310
+ this.authToken = await readDiscordAuthToken(this.config.clientId);
311
+ if (this.authToken) {
312
+ logClusterEvent("info", "discord_collector_auth_cache_hit", this.lifecycleContext, {
313
+ authStorePath: getAuthStorePath(),
314
+ });
315
+ }
316
+ else {
317
+ logClusterEvent("info", "discord_collector_auth_cache_miss", this.lifecycleContext, {
318
+ authStorePath: getAuthStorePath(),
319
+ });
320
+ }
321
+ }
322
+ catch (error) {
323
+ logClusterEvent("warn", "discord_collector_auth_cache_load_failed", this.lifecycleContext, {
324
+ error: error instanceof Error ? error.message : String(error),
325
+ authStorePath: getAuthStorePath(),
326
+ });
327
+ }
328
+ }
329
+ async tryRefreshToken() {
330
+ if (!this.config.clientId || !this.authToken?.refreshToken) {
331
+ return null;
332
+ }
333
+ const refreshed = await tryRefreshDiscordToken({
334
+ clientId: this.config.clientId,
335
+ clientSecret: this.config.clientSecret,
336
+ redirectUri: this.config.redirectUri,
337
+ scopes: this.config.scopes,
338
+ refreshToken: this.authToken.refreshToken,
339
+ });
340
+ if (!refreshed) {
341
+ return null;
342
+ }
343
+ const persisted = await this.persistAuthToken(refreshed);
344
+ logClusterEvent("info", "discord_collector_auth_token_refreshed", this.lifecycleContext, {
345
+ authStorePath: getAuthStorePath(),
346
+ });
347
+ return persisted;
348
+ }
349
+ async authorizeAndPersist(client) {
350
+ if (!this.config.clientId) {
351
+ throw new Error("discord collector requires clientId");
352
+ }
353
+ if (!this.warnedOauthConfig && !this.config.clientSecret && !this.config.accessToken) {
354
+ this.warnedOauthConfig = true;
355
+ logClusterEvent("warn", "discord_collector_missing_client_secret", this.lifecycleContext, {
356
+ hint: "oauth2/token authorization_code usually requires clientSecret (and sometimes redirectUri)",
357
+ });
358
+ }
359
+ logClusterEvent("info", "discord_collector_authorize_start", this.lifecycleContext, {
360
+ authStorePath: getAuthStorePath(),
361
+ scopes: this.config.scopes,
362
+ });
363
+ const { code } = await client.authorize(this.config.scopes);
364
+ const exchanged = await exchangeDiscordAuthorizeCode({
365
+ clientId: this.config.clientId,
366
+ clientSecret: this.config.clientSecret,
367
+ redirectUri: this.config.redirectUri,
368
+ scopes: this.config.scopes,
369
+ code,
370
+ });
371
+ const persisted = await this.persistAuthToken(exchanged);
372
+ logClusterEvent("info", "discord_collector_authorize_success", this.lifecycleContext, {
373
+ authStorePath: getAuthStorePath(),
374
+ });
375
+ return persisted;
376
+ }
377
+ async persistAuthToken(token) {
378
+ if (!this.config.clientId) {
379
+ throw new Error("discord collector requires clientId");
380
+ }
381
+ try {
382
+ const persisted = await writeDiscordAuthToken(this.config.clientId, token);
383
+ this.authToken = persisted;
384
+ return persisted;
385
+ }
386
+ catch (error) {
387
+ logClusterEvent("warn", "discord_collector_auth_cache_write_failed", this.lifecycleContext, {
388
+ error: error instanceof Error ? error.message : String(error),
389
+ authStorePath: getAuthStorePath(),
390
+ });
391
+ const fallback = {
392
+ ...token,
393
+ updatedAt: new Date().toISOString(),
394
+ };
395
+ this.authToken = fallback;
396
+ return fallback;
397
+ }
398
+ }
399
+ async persistEvent(client, eventName, payload) {
400
+ const payloadRecord = isRecord(payload) ? payload : null;
401
+ const messageRecord = payloadRecord && isRecord(payloadRecord.message) ? payloadRecord.message : payloadRecord;
402
+ const authorRecord = messageRecord && isRecord(messageRecord.author) ? messageRecord.author : null;
403
+ const channelId = readString(payloadRecord?.channel_id) ?? readString(messageRecord?.channel_id) ?? null;
404
+ const channelAlias = channelId ? (this.channelAliasById.get(channelId) ?? null) : null;
405
+ let guildId = readString(payloadRecord?.guild_id) ?? readString(messageRecord?.guild_id) ?? null;
406
+ let channelName = null;
407
+ let guildName = null;
408
+ if (channelId) {
409
+ try {
410
+ const context = await this.resolveChannelContext(client, channelId, guildId);
411
+ channelName = context.channelName;
412
+ guildId = guildId ?? context.guildId;
413
+ guildName = context.guildName;
414
+ }
415
+ catch (error) {
416
+ logClusterEvent("warn", "discord_collector_channel_context_failed", this.lifecycleContext, {
417
+ channelId,
418
+ error: error instanceof Error ? error.message : String(error),
419
+ });
420
+ }
421
+ }
422
+ if (!guildName && guildId) {
423
+ try {
424
+ guildName = await this.resolveGuildName(client, guildId);
425
+ }
426
+ catch (error) {
427
+ logClusterEvent("warn", "discord_collector_guild_context_failed", this.lifecycleContext, {
428
+ guildId,
429
+ error: error instanceof Error ? error.message : String(error),
430
+ });
431
+ }
432
+ }
433
+ const record = {
434
+ seq: ++this.seq,
435
+ receivedAt: new Date().toISOString(),
436
+ event: eventName,
437
+ channelId,
438
+ channelAlias,
439
+ channelName,
440
+ guildId,
441
+ guildName,
442
+ messageId: readString(messageRecord?.id),
443
+ authorId: readString(authorRecord?.id),
444
+ authorUsername: readString(authorRecord?.username),
445
+ content: readString(messageRecord?.content),
446
+ attachments: readAttachmentUrls(messageRecord?.attachments),
447
+ embeds: readEmbedUrls(messageRecord?.embeds),
448
+ };
449
+ if (this.config.includeRawPayload) {
450
+ record.raw = payload;
451
+ }
452
+ this.writer.append(record);
453
+ this.maybeEmitChannelBatchEvent(eventName, record);
454
+ }
455
+ maybeEmitChannelBatchEvent(eventName, record) {
456
+ if (eventName !== "MESSAGE_CREATE") {
457
+ return;
458
+ }
459
+ if (!record.channelId) {
460
+ return;
461
+ }
462
+ const channelId = record.channelId;
463
+ const nextCount = (this.channelMessageCounts.get(channelId) ?? 0) + 1;
464
+ if (nextCount < this.config.systemEventBatchSize) {
465
+ this.channelMessageCounts.set(channelId, nextCount);
466
+ return;
467
+ }
468
+ this.channelMessageCounts.set(channelId, 0);
469
+ const sequence = (this.channelBatchSequences.get(channelId) ?? 0) + 1;
470
+ this.channelBatchSequences.set(channelId, sequence);
471
+ emitClusterV2DiscordChannelBatch({
472
+ scope: this.lifecycleContext.scope ?? "default",
473
+ channelId,
474
+ channelAlias: record.channelAlias ?? undefined,
475
+ channelName: record.channelName ?? undefined,
476
+ guildName: record.guildName ?? undefined,
477
+ count: this.config.systemEventBatchSize,
478
+ sequence,
479
+ });
480
+ }
481
+ async resolveChannelContext(client, channelId, fallbackGuildId) {
482
+ const cached = this.channelContextCache.get(channelId);
483
+ if (cached) {
484
+ return cached;
485
+ }
486
+ const inFlight = this.channelContextInFlight.get(channelId);
487
+ if (inFlight) {
488
+ return inFlight;
489
+ }
490
+ const promise = (async () => {
491
+ const channel = await client.getChannel(channelId);
492
+ const guildId = channel.guildId ?? fallbackGuildId;
493
+ const guildName = guildId ? await this.resolveGuildName(client, guildId) : null;
494
+ const context = {
495
+ channelName: channel.name,
496
+ guildId,
497
+ guildName,
498
+ };
499
+ this.channelContextCache.set(channelId, context);
500
+ return context;
501
+ })()
502
+ .finally(() => {
503
+ this.channelContextInFlight.delete(channelId);
504
+ });
505
+ this.channelContextInFlight.set(channelId, promise);
506
+ return promise;
507
+ }
508
+ async resolveGuildName(client, guildId) {
509
+ if (this.guildNameCache.has(guildId)) {
510
+ return this.guildNameCache.get(guildId) ?? null;
511
+ }
512
+ const inFlight = this.guildNameInFlight.get(guildId);
513
+ if (inFlight) {
514
+ return inFlight;
515
+ }
516
+ const promise = (async () => {
517
+ const guild = await client.getGuild(guildId);
518
+ const name = guild.name;
519
+ this.guildNameCache.set(guildId, name);
520
+ return name;
521
+ })()
522
+ .catch((error) => {
523
+ logClusterEvent("warn", "discord_collector_get_guild_failed", this.lifecycleContext, {
524
+ guildId,
525
+ error: error instanceof Error ? error.message : String(error),
526
+ });
527
+ this.guildNameCache.set(guildId, null);
528
+ return null;
529
+ })
530
+ .finally(() => {
531
+ this.guildNameInFlight.delete(guildId);
532
+ });
533
+ this.guildNameInFlight.set(guildId, promise);
534
+ return promise;
535
+ }
536
+ }
537
+ function validateCollectorConfig(config, context) {
538
+ if (!config.enabled) {
539
+ return false;
540
+ }
541
+ if (!config.clientId) {
542
+ logClusterEvent("warn", "discord_collector_disabled_missing_client_id", context);
543
+ return false;
544
+ }
545
+ if (config.channels.length === 0) {
546
+ logClusterEvent("warn", "discord_collector_disabled_missing_channels", context);
547
+ return false;
548
+ }
549
+ return true;
550
+ }
551
+ export async function maybeStartDiscordCollector(context) {
552
+ let configObject;
553
+ try {
554
+ configObject = await loadMonoPilotConfigObject();
555
+ }
556
+ catch (error) {
557
+ logClusterEvent("warn", "discord_collector_config_load_failed", context, {
558
+ error: error instanceof Error ? error.message : String(error),
559
+ });
560
+ return null;
561
+ }
562
+ const config = extractDiscordCollectorConfig(configObject);
563
+ if (!validateCollectorConfig(config, context)) {
564
+ return null;
565
+ }
566
+ const collector = new DiscordCollector(config, context);
567
+ collector.start();
568
+ return collector;
569
+ }
@@ -0,0 +1 @@
1
+ export { maybeStartDiscordCollector } from "./collector.js";
@@ -0,0 +1,87 @@
1
+ const DISCORD_OAUTH_TOKEN_URL = "https://discord.com/api/oauth2/token";
2
+ function readString(value) {
3
+ if (typeof value !== "string") {
4
+ return undefined;
5
+ }
6
+ const trimmed = value.trim();
7
+ return trimmed.length > 0 ? trimmed : undefined;
8
+ }
9
+ function toTokenRecord(payload) {
10
+ const accessToken = readString(payload.access_token);
11
+ if (!accessToken) {
12
+ return null;
13
+ }
14
+ let expiresAt;
15
+ if (typeof payload.expires_in === "number" && Number.isFinite(payload.expires_in)) {
16
+ expiresAt = new Date(Date.now() + Math.max(0, payload.expires_in) * 1000).toISOString();
17
+ }
18
+ return {
19
+ accessToken,
20
+ refreshToken: readString(payload.refresh_token),
21
+ tokenType: readString(payload.token_type),
22
+ scope: readString(payload.scope),
23
+ expiresAt,
24
+ };
25
+ }
26
+ function buildRequestBody(grantType, params) {
27
+ const body = new URLSearchParams();
28
+ body.set("client_id", params.clientId);
29
+ body.set("grant_type", grantType);
30
+ if (params.clientSecret) {
31
+ body.set("client_secret", params.clientSecret);
32
+ }
33
+ if (params.redirectUri) {
34
+ body.set("redirect_uri", params.redirectUri);
35
+ }
36
+ if (params.scopes && params.scopes.length > 0) {
37
+ body.set("scope", params.scopes.join(" "));
38
+ }
39
+ if (grantType === "authorization_code") {
40
+ body.set("code", params.code);
41
+ }
42
+ else {
43
+ body.set("refresh_token", params.refreshToken);
44
+ }
45
+ return body;
46
+ }
47
+ async function exchangeToken(grantType, params, strict) {
48
+ const response = await fetch(DISCORD_OAUTH_TOKEN_URL, {
49
+ method: "POST",
50
+ headers: {
51
+ "Content-Type": "application/x-www-form-urlencoded",
52
+ },
53
+ body: buildRequestBody(grantType, params),
54
+ });
55
+ let payload = {};
56
+ try {
57
+ payload = (await response.json());
58
+ }
59
+ catch {
60
+ if (strict) {
61
+ throw new Error(`discord oauth token exchange failed: HTTP ${response.status}`);
62
+ }
63
+ return null;
64
+ }
65
+ const token = toTokenRecord(payload);
66
+ if (response.ok && token) {
67
+ return token;
68
+ }
69
+ const message = readString(payload.error_description) ??
70
+ readString(payload.message) ??
71
+ readString(payload.error) ??
72
+ `HTTP ${response.status}`;
73
+ if (strict) {
74
+ throw new Error(`discord oauth token exchange failed: ${message}`);
75
+ }
76
+ return null;
77
+ }
78
+ export async function exchangeDiscordAuthorizeCode(params) {
79
+ const token = await exchangeToken("authorization_code", params, true);
80
+ if (!token) {
81
+ throw new Error("discord oauth authorization_code exchange returned no token");
82
+ }
83
+ return token;
84
+ }
85
+ export async function tryRefreshDiscordToken(params) {
86
+ return exchangeToken("refresh_token", params, false);
87
+ }