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,1055 @@
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
53
+ import { spawn } from "node:child_process";
54
+ import { closeSync, openSync } from "node:fs";
55
+ import { appendFile, mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
56
+ import { tmpdir } from "node:os";
57
+ import { dirname, join, parse } from "node:path";
58
+ import { extractTwitterCollectorConfig } from "../../../config/twitter.js";
59
+ import { loadMonoPilotConfigObject } from "../../../config/mono-pilot.js";
60
+ import { emitClusterV2TwitterCollectorStartupFailed, emitClusterV2TwitterPullBatch, emitClusterV2TwitterPullFailed, } from "../../events.js";
61
+ import { logClusterEvent } from "../../observability.js";
62
+ function oppositeTimelineFeed(feed) {
63
+ return feed === "for_you" ? "following" : "for_you";
64
+ }
65
+ function isRecord(value) {
66
+ return typeof value === "object" && value !== null && !Array.isArray(value);
67
+ }
68
+ function readString(value) {
69
+ return typeof value === "string" && value.length > 0 ? value : null;
70
+ }
71
+ function readNestedString(record, path) {
72
+ let current = record;
73
+ for (const key of path) {
74
+ if (!isRecord(current) || !(key in current)) {
75
+ return null;
76
+ }
77
+ current = current[key];
78
+ }
79
+ return readString(current);
80
+ }
81
+ function readNestedRecord(record, path) {
82
+ let current = record;
83
+ for (const key of path) {
84
+ if (!isRecord(current) || !(key in current)) {
85
+ return null;
86
+ }
87
+ current = current[key];
88
+ }
89
+ return isRecord(current) ? current : null;
90
+ }
91
+ function readNestedArray(record, path) {
92
+ let current = record;
93
+ for (const key of path) {
94
+ if (!isRecord(current) || !(key in current)) {
95
+ return null;
96
+ }
97
+ current = current[key];
98
+ }
99
+ return Array.isArray(current) ? current : null;
100
+ }
101
+ function firstNonEmptyString(candidates) {
102
+ for (const candidate of candidates) {
103
+ if (typeof candidate === "string" && candidate.trim().length > 0) {
104
+ return candidate;
105
+ }
106
+ }
107
+ return null;
108
+ }
109
+ function formatLocalDateStamp(date) {
110
+ const year = String(date.getFullYear());
111
+ const month = String(date.getMonth() + 1).padStart(2, "0");
112
+ const day = String(date.getDate()).padStart(2, "0");
113
+ return `${year}-${month}-${day}`;
114
+ }
115
+ function shiftDate(date, dayOffset) {
116
+ const shifted = new Date(date);
117
+ shifted.setDate(shifted.getDate() + dayOffset);
118
+ return shifted;
119
+ }
120
+ function listRecentDateStamps(now, dayCount) {
121
+ const result = [];
122
+ for (let index = 0; index < dayCount; index += 1) {
123
+ result.push(formatLocalDateStamp(shiftDate(now, -index)));
124
+ }
125
+ return result;
126
+ }
127
+ function resolveDailyArchivePath(outputPath, dateStamp) {
128
+ const parsed = parse(outputPath);
129
+ const dir = parsed.dir;
130
+ const baseName = parsed.name || parsed.base || "home";
131
+ const extension = parsed.ext || ".jsonl";
132
+ return join(dir, `${baseName}.${dateStamp}${extension}`);
133
+ }
134
+ function hasOwnField(record, key) {
135
+ return Object.prototype.hasOwnProperty.call(record, key);
136
+ }
137
+ function hasTweetFullField(record) {
138
+ return hasOwnField(record, "tweetFull");
139
+ }
140
+ function toLineRecord(line) {
141
+ const trimmed = line.trim();
142
+ if (!trimmed) {
143
+ return null;
144
+ }
145
+ try {
146
+ const parsed = JSON.parse(trimmed);
147
+ return isRecord(parsed) ? parsed : null;
148
+ }
149
+ catch {
150
+ return null;
151
+ }
152
+ }
153
+ async function createAsyncDisposableTempDir(prefix) {
154
+ const path = await mkdtemp(prefix);
155
+ let removed = false;
156
+ const remove = async () => {
157
+ if (removed) {
158
+ return;
159
+ }
160
+ removed = true;
161
+ await rm(path, { recursive: true, force: true });
162
+ };
163
+ return {
164
+ path,
165
+ remove,
166
+ [Symbol.asyncDispose]: remove,
167
+ };
168
+ }
169
+ class JsonlWriter {
170
+ outputPath;
171
+ logContext;
172
+ queue = Promise.resolve();
173
+ dirReady = false;
174
+ outputDir;
175
+ outputBaseName;
176
+ outputExtension;
177
+ currentDateStamp = null;
178
+ currentOutputPath = null;
179
+ constructor(outputPath, logContext) {
180
+ this.outputPath = outputPath;
181
+ this.logContext = logContext;
182
+ const parsed = parse(outputPath);
183
+ this.outputDir = parsed.dir;
184
+ this.outputBaseName = parsed.name || parsed.base || "home";
185
+ this.outputExtension = parsed.ext || ".jsonl";
186
+ }
187
+ append(record) {
188
+ this.queue = this.queue
189
+ .then(async () => {
190
+ if (!this.dirReady) {
191
+ await mkdir(this.outputDir || dirname(this.outputPath), { recursive: true });
192
+ this.dirReady = true;
193
+ }
194
+ const currentPath = this.resolveCurrentOutputPath();
195
+ await appendFile(currentPath, `${JSON.stringify(record)}\n`, "utf-8");
196
+ })
197
+ .catch((error) => {
198
+ logClusterEvent("warn", "twitter_collector_persist_failed", this.logContext, {
199
+ error: error instanceof Error ? error.message : String(error),
200
+ outputPath: this.outputPath,
201
+ currentOutputPath: this.currentOutputPath,
202
+ });
203
+ });
204
+ }
205
+ resolveCurrentOutputPath(now = new Date()) {
206
+ const dateStamp = formatLocalDateStamp(now);
207
+ if (this.currentDateStamp === dateStamp && this.currentOutputPath) {
208
+ return this.currentOutputPath;
209
+ }
210
+ this.currentDateStamp = dateStamp;
211
+ this.currentOutputPath = join(this.outputDir, `${this.outputBaseName}.${dateStamp}${this.outputExtension}`);
212
+ return this.currentOutputPath;
213
+ }
214
+ async flush() {
215
+ await this.queue;
216
+ }
217
+ }
218
+ function buildBirdGlobalArgs(config) {
219
+ const args = [];
220
+ if (config.chromeProfile) {
221
+ args.push("--chrome-profile", config.chromeProfile);
222
+ }
223
+ if (config.chromeProfileDir) {
224
+ args.push("--chrome-profile-dir", config.chromeProfileDir);
225
+ }
226
+ if (config.firefoxProfile) {
227
+ args.push("--firefox-profile", config.firefoxProfile);
228
+ }
229
+ for (const source of config.cookieSource) {
230
+ args.push("--cookie-source", source);
231
+ }
232
+ if (config.cookieTimeoutMs) {
233
+ args.push("--cookie-timeout", String(config.cookieTimeoutMs));
234
+ }
235
+ if (config.requestTimeoutMs) {
236
+ args.push("--timeout", String(config.requestTimeoutMs));
237
+ }
238
+ return args;
239
+ }
240
+ function runBirdCommand(args, timeoutMs) {
241
+ return new Promise((resolve, reject) => {
242
+ const child = spawn("bird", args, {
243
+ stdio: ["ignore", "pipe", "pipe"],
244
+ });
245
+ let stdout = "";
246
+ let stderr = "";
247
+ let timedOut = false;
248
+ const timer = setTimeout(() => {
249
+ timedOut = true;
250
+ child.kill("SIGKILL");
251
+ }, timeoutMs);
252
+ timer.unref();
253
+ child.stdout.on("data", (chunk) => {
254
+ stdout += String(chunk);
255
+ });
256
+ child.stderr.on("data", (chunk) => {
257
+ stderr += String(chunk);
258
+ });
259
+ child.on("error", (error) => {
260
+ clearTimeout(timer);
261
+ reject(error);
262
+ });
263
+ child.on("close", (code, signal) => {
264
+ clearTimeout(timer);
265
+ resolve({
266
+ code,
267
+ signal,
268
+ stdout,
269
+ stderr,
270
+ timedOut,
271
+ });
272
+ });
273
+ });
274
+ }
275
+ function runBirdCommandToFile(args, timeoutMs, outputPath) {
276
+ return new Promise((resolve, reject) => {
277
+ let stderr = "";
278
+ let timedOut = false;
279
+ let stdoutFd;
280
+ try {
281
+ stdoutFd = openSync(outputPath, "w");
282
+ }
283
+ catch (error) {
284
+ reject(error);
285
+ return;
286
+ }
287
+ const child = spawn("bird", args, {
288
+ stdio: ["ignore", stdoutFd, "pipe"],
289
+ });
290
+ const timer = setTimeout(() => {
291
+ timedOut = true;
292
+ child.kill("SIGKILL");
293
+ }, timeoutMs);
294
+ timer.unref();
295
+ const closeStdoutFd = () => {
296
+ try {
297
+ closeSync(stdoutFd);
298
+ }
299
+ catch {
300
+ // no-op; best-effort cleanup for temporary stdout fd
301
+ }
302
+ };
303
+ if (!child.stderr) {
304
+ clearTimeout(timer);
305
+ closeStdoutFd();
306
+ reject(new Error("bird stderr stream is unavailable"));
307
+ return;
308
+ }
309
+ child.stderr.on("data", (chunk) => {
310
+ stderr += String(chunk);
311
+ });
312
+ child.on("error", (error) => {
313
+ clearTimeout(timer);
314
+ closeStdoutFd();
315
+ reject(error);
316
+ });
317
+ child.on("close", (code, signal) => {
318
+ clearTimeout(timer);
319
+ closeStdoutFd();
320
+ resolve({
321
+ code,
322
+ signal,
323
+ stderr,
324
+ timedOut,
325
+ });
326
+ });
327
+ });
328
+ }
329
+ function formatBirdFailure(prefix, result) {
330
+ const stderr = result.stderr.trim();
331
+ const stdout = result.stdout.trim();
332
+ const detail = stderr || stdout || `exit=${result.code ?? "null"} signal=${result.signal ?? "none"}`;
333
+ if (result.timedOut) {
334
+ return `${prefix}: timed out (${detail})`;
335
+ }
336
+ return `${prefix}: ${detail}`;
337
+ }
338
+ function looksLikeTweetRecord(record) {
339
+ const tweetId = extractTweetId(record);
340
+ const text = readString(record.text) ??
341
+ readString(record.full_text) ??
342
+ readNestedString(record, ["legacy", "full_text"]) ??
343
+ readNestedString(record, ["legacy", "text"]);
344
+ return Boolean(tweetId || text);
345
+ }
346
+ function extractTweetId(record) {
347
+ return (readString(record.id) ??
348
+ readString(record.id_str) ??
349
+ readString(record.rest_id) ??
350
+ readNestedString(record, ["legacy", "id_str"]));
351
+ }
352
+ function readStringArray(value) {
353
+ if (!Array.isArray(value)) {
354
+ return [];
355
+ }
356
+ const deduped = new Set();
357
+ for (const item of value) {
358
+ const text = readString(item);
359
+ if (text) {
360
+ deduped.add(text);
361
+ }
362
+ }
363
+ return [...deduped];
364
+ }
365
+ function extractStatusIdFromUrl(url) {
366
+ const normalized = url.trim();
367
+ if (!normalized) {
368
+ return null;
369
+ }
370
+ const matchedUserStatus = normalized.match(/^https?:\/\/(?:www\.)?(?:x|twitter)\.com\/[A-Za-z0-9_]+\/status\/(\d+)/i);
371
+ if (matchedUserStatus?.[1]) {
372
+ return matchedUserStatus[1];
373
+ }
374
+ const matchedWebStatus = normalized.match(/^https?:\/\/(?:www\.)?(?:x|twitter)\.com\/i\/web\/status\/(\d+)/i);
375
+ if (matchedWebStatus?.[1]) {
376
+ return matchedWebStatus[1];
377
+ }
378
+ return null;
379
+ }
380
+ function extractTcoUrlsFromText(text) {
381
+ if (!text) {
382
+ return [];
383
+ }
384
+ const deduped = new Set();
385
+ const pattern = /https?:\/\/t\.co\/[A-Za-z0-9]+/gi;
386
+ pattern.lastIndex = 0;
387
+ let matched;
388
+ while ((matched = pattern.exec(text)) !== null) {
389
+ const url = matched[0]?.trim();
390
+ if (url) {
391
+ deduped.add(url);
392
+ }
393
+ }
394
+ return [...deduped];
395
+ }
396
+ function extractBestFullText(record) {
397
+ const raw = isRecord(record._raw) ? record._raw : null;
398
+ const rawRetweeted = raw
399
+ ? readNestedRecord(raw, ["legacy", "retweeted_status_result", "result"])
400
+ : null;
401
+ return firstNonEmptyString([
402
+ readNestedString(record, ["note_tweet", "note_tweet_results", "result", "text"]),
403
+ readNestedString(record, ["note_tweet_results", "result", "text"]),
404
+ rawRetweeted ? readNestedString(rawRetweeted, ["note_tweet", "note_tweet_results", "result", "text"]) : null,
405
+ rawRetweeted ? readNestedString(rawRetweeted, ["legacy", "full_text"]) : null,
406
+ rawRetweeted ? readString(rawRetweeted.text) : null,
407
+ raw ? readNestedString(raw, ["note_tweet", "note_tweet_results", "result", "text"]) : null,
408
+ raw ? readNestedString(raw, ["note_tweet_results", "result", "text"]) : null,
409
+ readNestedString(record, ["legacy", "full_text"]),
410
+ readString(record.full_text),
411
+ raw ? readNestedString(raw, ["legacy", "full_text"]) : null,
412
+ readString(record.text),
413
+ ]);
414
+ }
415
+ function extractBestText(record) {
416
+ return firstNonEmptyString([
417
+ readString(record.text),
418
+ readString(record.full_text),
419
+ readNestedString(record, ["legacy", "full_text"]),
420
+ readNestedString(record, ["legacy", "text"]),
421
+ ]);
422
+ }
423
+ function extractBestMedia(record) {
424
+ if (Array.isArray(record.media)) {
425
+ return record.media.map((item) => (isRecord(item) ? { ...item } : item));
426
+ }
427
+ const raw = isRecord(record._raw) ? record._raw : null;
428
+ const rawRetweeted = raw
429
+ ? readNestedRecord(raw, ["legacy", "retweeted_status_result", "result"])
430
+ : null;
431
+ const candidates = [
432
+ raw ? readNestedArray(raw, ["legacy", "extended_entities", "media"]) : null,
433
+ rawRetweeted ? readNestedArray(rawRetweeted, ["media"]) : null,
434
+ rawRetweeted ? readNestedArray(rawRetweeted, ["legacy", "extended_entities", "media"]) : null,
435
+ ];
436
+ for (const candidate of candidates) {
437
+ if (Array.isArray(candidate) && candidate.length > 0) {
438
+ return candidate.map((item) => (isRecord(item) ? { ...item } : item));
439
+ }
440
+ }
441
+ return null;
442
+ }
443
+ function parseTweetsFromPayload(payload, limit) {
444
+ const selected = [];
445
+ const seen = new Set();
446
+ const appendFromArray = (items) => {
447
+ for (const item of items) {
448
+ if (!isRecord(item) || !looksLikeTweetRecord(item)) {
449
+ continue;
450
+ }
451
+ const dedupeKey = extractTweetId(item) ?? "";
452
+ if (dedupeKey && seen.has(dedupeKey)) {
453
+ continue;
454
+ }
455
+ if (dedupeKey) {
456
+ seen.add(dedupeKey);
457
+ }
458
+ selected.push({ ...item });
459
+ if (selected.length >= limit) {
460
+ return;
461
+ }
462
+ }
463
+ };
464
+ if (Array.isArray(payload)) {
465
+ appendFromArray(payload);
466
+ return selected;
467
+ }
468
+ if (!isRecord(payload)) {
469
+ return selected;
470
+ }
471
+ const timelineArrayCandidates = [];
472
+ const directTweets = payload.tweets;
473
+ if (Array.isArray(directTweets)) {
474
+ timelineArrayCandidates.push(directTweets);
475
+ }
476
+ const directData = payload.data;
477
+ if (Array.isArray(directData)) {
478
+ timelineArrayCandidates.push(directData);
479
+ }
480
+ for (const candidate of timelineArrayCandidates) {
481
+ appendFromArray(candidate);
482
+ if (selected.length >= limit) {
483
+ break;
484
+ }
485
+ }
486
+ return selected;
487
+ }
488
+ class TwitterCollector {
489
+ config;
490
+ writer;
491
+ lifecycleContext;
492
+ birdGlobalArgs;
493
+ intervalTimer = null;
494
+ seq = 0;
495
+ inFlight = false;
496
+ closed = false;
497
+ archiveWindowDays = 2;
498
+ tweetFullCache = new Map();
499
+ preferredTimelineFeed = "for_you";
500
+ descriptor;
501
+ constructor(config, context) {
502
+ this.config = config;
503
+ this.lifecycleContext = {
504
+ ...context,
505
+ role: context.role ? `${context.role}:twitter_intel` : "twitter_intel",
506
+ };
507
+ this.writer = new JsonlWriter(config.outputPath, this.lifecycleContext);
508
+ this.birdGlobalArgs = buildBirdGlobalArgs(config);
509
+ this.descriptor = {
510
+ name: "twitter_intel",
511
+ version: "v1",
512
+ capabilities: {
513
+ mode: "leader_local",
514
+ feed: "for_you",
515
+ fallbackFeed: "following",
516
+ pullCount: this.config.pullCount,
517
+ pullIntervalMinutes: this.config.pullIntervalMinutes,
518
+ outputPath: this.config.outputPath,
519
+ },
520
+ };
521
+ }
522
+ async start() {
523
+ await this.ensureBirdProfileReady();
524
+ if (this.closed) {
525
+ return;
526
+ }
527
+ logClusterEvent("info", "twitter_collector_started", this.lifecycleContext, {
528
+ outputPath: this.config.outputPath,
529
+ pullCount: this.config.pullCount,
530
+ pullIntervalMinutes: this.config.pullIntervalMinutes,
531
+ });
532
+ void this.pullAndPersist("startup");
533
+ const intervalMs = Math.max(1, Math.floor(this.config.pullIntervalMinutes * 60_000));
534
+ this.intervalTimer = setInterval(() => {
535
+ void this.pullAndPersist("interval");
536
+ }, intervalMs);
537
+ this.intervalTimer.unref();
538
+ }
539
+ async close() {
540
+ if (this.closed) {
541
+ return;
542
+ }
543
+ this.closed = true;
544
+ if (this.intervalTimer) {
545
+ clearInterval(this.intervalTimer);
546
+ this.intervalTimer = null;
547
+ }
548
+ await this.writer.flush();
549
+ logClusterEvent("info", "twitter_collector_stopped", this.lifecycleContext);
550
+ }
551
+ async ensureBirdProfileReady() {
552
+ const args = [...this.birdGlobalArgs, "check", "--plain"];
553
+ let result;
554
+ try {
555
+ result = await runBirdCommand(args, this.config.commandTimeoutMs);
556
+ }
557
+ catch (error) {
558
+ throw new Error(`failed to start bird check: ${error instanceof Error ? error.message : String(error)}`);
559
+ }
560
+ if (result.code !== 0) {
561
+ throw new Error(formatBirdFailure("bird profile check failed", result));
562
+ }
563
+ logClusterEvent("info", "twitter_collector_profile_check_ok", this.lifecycleContext);
564
+ }
565
+ async pullAndPersist(trigger) {
566
+ if (this.closed || this.inFlight) {
567
+ return;
568
+ }
569
+ this.inFlight = true;
570
+ const startedAt = Date.now();
571
+ try {
572
+ const archiveIndex = await this.loadRecentArchiveTweetIndex();
573
+ const timeline = await this.fetchTimelineWithFallback();
574
+ const payload = timeline.payload;
575
+ const fetchedTweets = timeline.fetchedTweets;
576
+ const { tweets, skippedArchivedCount, keptForBackfillCount } = this.filterTweetsByArchiveIndex(fetchedTweets, archiveIndex);
577
+ await this.enrichTweetsDepthOne(tweets);
578
+ if (tweets.length === 0) {
579
+ emitClusterV2TwitterPullBatch({
580
+ scope: this.lifecycleContext.scope ?? "default",
581
+ count: 0,
582
+ requestedCount: this.config.pullCount,
583
+ sequence: this.seq,
584
+ });
585
+ logClusterEvent("info", "twitter_collector_pull_dedup_skipped_all", this.lifecycleContext, {
586
+ trigger,
587
+ feed: timeline.feed,
588
+ requestedCount: this.config.pullCount,
589
+ fetchedCount: fetchedTweets.length,
590
+ skippedArchivedCount,
591
+ keptForBackfillCount,
592
+ archiveWindowDays: this.archiveWindowDays,
593
+ durationMs: Date.now() - startedAt,
594
+ });
595
+ return;
596
+ }
597
+ const fetchedAt = new Date().toISOString();
598
+ const seq = ++this.seq;
599
+ const record = {
600
+ seq,
601
+ fetchedAt,
602
+ snapshotId: `${fetchedAt}-${seq}`,
603
+ feed: timeline.feed,
604
+ requestedCount: this.config.pullCount,
605
+ receivedCount: tweets.length,
606
+ tweets,
607
+ };
608
+ if (this.config.includeRawPayload) {
609
+ record.raw = payload;
610
+ }
611
+ this.writer.append(record);
612
+ emitClusterV2TwitterPullBatch({
613
+ scope: this.lifecycleContext.scope ?? "default",
614
+ count: record.receivedCount,
615
+ requestedCount: record.requestedCount,
616
+ sequence: record.seq,
617
+ });
618
+ logClusterEvent("info", "twitter_collector_pull_success", this.lifecycleContext, {
619
+ trigger,
620
+ feed: timeline.feed,
621
+ requestedCount: this.config.pullCount,
622
+ fetchedCount: fetchedTweets.length,
623
+ receivedCount: tweets.length,
624
+ skippedArchivedCount,
625
+ keptForBackfillCount,
626
+ archiveWindowDays: this.archiveWindowDays,
627
+ durationMs: Date.now() - startedAt,
628
+ });
629
+ }
630
+ catch (error) {
631
+ const message = error instanceof Error ? error.message : String(error);
632
+ emitClusterV2TwitterPullFailed({
633
+ scope: this.lifecycleContext.scope ?? "default",
634
+ trigger,
635
+ error: message,
636
+ });
637
+ logClusterEvent("warn", "twitter_collector_pull_failed", this.lifecycleContext, {
638
+ trigger,
639
+ error: message,
640
+ });
641
+ }
642
+ finally {
643
+ this.inFlight = false;
644
+ }
645
+ }
646
+ filterTweetsByArchiveIndex(tweets, archiveIndex) {
647
+ const selected = [];
648
+ let skippedArchivedCount = 0;
649
+ let keptForBackfillCount = 0;
650
+ for (const tweet of tweets) {
651
+ if (!isRecord(tweet)) {
652
+ selected.push(tweet);
653
+ continue;
654
+ }
655
+ const tweetId = extractTweetId(tweet);
656
+ if (!tweetId) {
657
+ selected.push(tweet);
658
+ continue;
659
+ }
660
+ const snapshot = archiveIndex.get(tweetId);
661
+ if (!snapshot) {
662
+ selected.push(tweet);
663
+ continue;
664
+ }
665
+ if (snapshot.hasTweetFullField) {
666
+ skippedArchivedCount += 1;
667
+ continue;
668
+ }
669
+ keptForBackfillCount += 1;
670
+ selected.push(tweet);
671
+ }
672
+ return {
673
+ tweets: selected,
674
+ skippedArchivedCount,
675
+ keptForBackfillCount,
676
+ };
677
+ }
678
+ async loadRecentArchiveTweetIndex(now = new Date()) {
679
+ const index = new Map();
680
+ const dateStamps = listRecentDateStamps(now, this.archiveWindowDays);
681
+ for (const dateStamp of dateStamps) {
682
+ const archivePath = resolveDailyArchivePath(this.config.outputPath, dateStamp);
683
+ let content;
684
+ try {
685
+ content = await readFile(archivePath, "utf-8");
686
+ }
687
+ catch {
688
+ continue;
689
+ }
690
+ const lines = content.split("\n");
691
+ for (const line of lines) {
692
+ const record = toLineRecord(line);
693
+ if (!record) {
694
+ continue;
695
+ }
696
+ const tweets = record.tweets;
697
+ if (!Array.isArray(tweets)) {
698
+ continue;
699
+ }
700
+ for (const tweet of tweets) {
701
+ if (!isRecord(tweet)) {
702
+ continue;
703
+ }
704
+ const tweetId = extractTweetId(tweet);
705
+ if (!tweetId) {
706
+ continue;
707
+ }
708
+ const previous = index.get(tweetId);
709
+ index.set(tweetId, {
710
+ hasTweetFullField: Boolean(previous?.hasTweetFullField || hasTweetFullField(tweet)),
711
+ });
712
+ }
713
+ }
714
+ }
715
+ return index;
716
+ }
717
+ async enrichTweetsDepthOne(tweets) {
718
+ for (const tweet of tweets) {
719
+ if (!isRecord(tweet)) {
720
+ continue;
721
+ }
722
+ const mainId = extractTweetId(tweet);
723
+ let fullMain = null;
724
+ if (mainId) {
725
+ fullMain = await this.loadTweetFull(mainId, "tweetFull");
726
+ if (fullMain) {
727
+ tweet.tweetFull = { ...fullMain };
728
+ }
729
+ }
730
+ let quotedId = null;
731
+ const quoted = tweet.quotedTweet;
732
+ if (isRecord(quoted)) {
733
+ quotedId = extractTweetId(quoted);
734
+ if (quotedId) {
735
+ const fullQuoted = await this.loadTweetFull(quotedId, "quotedTweetFull");
736
+ if (fullQuoted) {
737
+ // Keep timeline snippet in quotedTweet; attach fetched full tweet separately.
738
+ tweet.quotedTweetFull = { ...fullQuoted };
739
+ }
740
+ }
741
+ }
742
+ const shortLinks = this.collectShortLinksForTweet(tweet, fullMain);
743
+ if (shortLinks.length === 0) {
744
+ continue;
745
+ }
746
+ const shortLinkMappings = await this.resolveShortLinkMappings(shortLinks);
747
+ if (shortLinkMappings.length > 0) {
748
+ const tweetFullByShortLinkTweetId = new Map();
749
+ const enrichedShortLinkMappings = [];
750
+ for (const mapping of shortLinkMappings) {
751
+ const enriched = {
752
+ shortUrl: mapping.shortUrl,
753
+ resolvedUrl: mapping.resolvedUrl,
754
+ tweetId: mapping.statusId,
755
+ };
756
+ const statusId = mapping.statusId;
757
+ if (statusId && statusId !== mainId) {
758
+ let shortLinkTweetFull = tweetFullByShortLinkTweetId.get(statusId);
759
+ if (shortLinkTweetFull === undefined) {
760
+ shortLinkTweetFull = await this.loadTweetFull(statusId, "shortLinkTweetFull");
761
+ tweetFullByShortLinkTweetId.set(statusId, shortLinkTweetFull ?? null);
762
+ }
763
+ if (shortLinkTweetFull) {
764
+ enriched.tweetFull = { ...shortLinkTweetFull };
765
+ }
766
+ }
767
+ enrichedShortLinkMappings.push(enriched);
768
+ }
769
+ tweet.shortLinkMappings = enrichedShortLinkMappings;
770
+ }
771
+ }
772
+ }
773
+ collectShortLinksForTweet(tweet, tweetFull) {
774
+ const links = new Set();
775
+ for (const shortUrl of extractTcoUrlsFromText(readString(tweet.text))) {
776
+ links.add(shortUrl);
777
+ }
778
+ if (tweetFull) {
779
+ for (const shortUrl of extractTcoUrlsFromText(readString(tweetFull.text))) {
780
+ links.add(shortUrl);
781
+ }
782
+ for (const shortUrl of extractTcoUrlsFromText(readString(tweetFull.fullText))) {
783
+ links.add(shortUrl);
784
+ }
785
+ }
786
+ return [...links];
787
+ }
788
+ async resolveShortLinkMappings(shortLinks) {
789
+ const mappings = [];
790
+ for (const shortUrl of shortLinks) {
791
+ const mapping = await this.resolveShortLinkMapping(shortUrl);
792
+ mappings.push(mapping);
793
+ }
794
+ return mappings;
795
+ }
796
+ async resolveShortLinkMapping(shortUrl) {
797
+ let currentUrl = shortUrl;
798
+ let resolvedUrl = shortUrl;
799
+ const maxRedirects = 8;
800
+ for (let hop = 0; hop < maxRedirects; hop += 1) {
801
+ const directStatusId = extractStatusIdFromUrl(currentUrl);
802
+ if (directStatusId) {
803
+ return { shortUrl, resolvedUrl: currentUrl, statusId: directStatusId };
804
+ }
805
+ let response;
806
+ try {
807
+ const timeoutMs = Math.max(500, this.config.requestTimeoutMs ?? this.config.commandTimeoutMs);
808
+ response = await fetch(currentUrl, {
809
+ method: "GET",
810
+ redirect: "manual",
811
+ signal: AbortSignal.timeout(timeoutMs),
812
+ });
813
+ }
814
+ catch {
815
+ return { shortUrl, resolvedUrl, statusId: null };
816
+ }
817
+ if (response.url) {
818
+ resolvedUrl = response.url;
819
+ }
820
+ const responseStatusId = extractStatusIdFromUrl(response.url);
821
+ if (responseStatusId) {
822
+ void response.body?.cancel().catch(() => {
823
+ // no-op
824
+ });
825
+ return { shortUrl, resolvedUrl: response.url, statusId: responseStatusId };
826
+ }
827
+ const location = response.headers.get("location");
828
+ const isRedirect = response.status >= 300 && response.status < 400;
829
+ void response.body?.cancel().catch(() => {
830
+ // no-op
831
+ });
832
+ if (!isRedirect || !location) {
833
+ return { shortUrl, resolvedUrl, statusId: null };
834
+ }
835
+ try {
836
+ currentUrl = new URL(location, currentUrl).toString();
837
+ resolvedUrl = currentUrl;
838
+ }
839
+ catch {
840
+ return { shortUrl, resolvedUrl, statusId: null };
841
+ }
842
+ }
843
+ return { shortUrl, resolvedUrl, statusId: null };
844
+ }
845
+ async loadTweetFull(tweetId, targetField) {
846
+ const cached = this.tweetFullCache.get(tweetId);
847
+ if (cached) {
848
+ return cached;
849
+ }
850
+ const maxAttempts = 2; // first try + one retry
851
+ let lastError;
852
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
853
+ try {
854
+ const fullTweet = await this.fetchTweetById(tweetId);
855
+ this.tweetFullCache.set(tweetId, fullTweet);
856
+ return fullTweet;
857
+ }
858
+ catch (error) {
859
+ lastError = error;
860
+ if (attempt < maxAttempts) {
861
+ logClusterEvent("info", "twitter_collector_tweet_read_retry", this.lifecycleContext, {
862
+ tweetId,
863
+ targetField,
864
+ attempt,
865
+ nextAttempt: attempt + 1,
866
+ error: error instanceof Error ? error.message : String(error),
867
+ });
868
+ await new Promise((resolve) => setTimeout(resolve, 200));
869
+ }
870
+ }
871
+ }
872
+ logClusterEvent("warn", "twitter_collector_tweet_read_failed", this.lifecycleContext, {
873
+ tweetId,
874
+ targetField,
875
+ attempts: maxAttempts,
876
+ error: lastError instanceof Error ? lastError.message : String(lastError),
877
+ });
878
+ return null;
879
+ }
880
+ async fetchTimelineWithFallback() {
881
+ const currentFeed = this.preferredTimelineFeed;
882
+ const currentPayload = await this.fetchHomeTimeline(currentFeed === "following");
883
+ const currentTweets = parseTweetsFromPayload(currentPayload, this.config.pullCount);
884
+ if (currentTweets.length > 0) {
885
+ return {
886
+ feed: currentFeed,
887
+ payload: currentPayload,
888
+ fetchedTweets: currentTweets,
889
+ };
890
+ }
891
+ const nextFeed = oppositeTimelineFeed(currentFeed);
892
+ logClusterEvent("info", "twitter_collector_feed_empty_switch", this.lifecycleContext, {
893
+ fromFeed: currentFeed,
894
+ toFeed: nextFeed,
895
+ requestedCount: this.config.pullCount,
896
+ fetchedCount: currentTweets.length,
897
+ });
898
+ try {
899
+ const nextPayload = await this.fetchHomeTimeline(nextFeed === "following");
900
+ const nextTweets = parseTweetsFromPayload(nextPayload, this.config.pullCount);
901
+ this.preferredTimelineFeed = nextFeed;
902
+ return {
903
+ feed: nextFeed,
904
+ payload: nextPayload,
905
+ fetchedTweets: nextTweets,
906
+ };
907
+ }
908
+ catch (error) {
909
+ logClusterEvent("warn", "twitter_collector_feed_switch_failed", this.lifecycleContext, {
910
+ fromFeed: currentFeed,
911
+ toFeed: nextFeed,
912
+ error: error instanceof Error ? error.message : String(error),
913
+ requestedCount: this.config.pullCount,
914
+ action: "keep_current_feed_empty_batch",
915
+ });
916
+ return {
917
+ feed: currentFeed,
918
+ payload: currentPayload,
919
+ fetchedTweets: currentTweets,
920
+ };
921
+ }
922
+ }
923
+ async fetchHomeTimeline(useFollowingFeed) {
924
+ const args = [
925
+ ...this.birdGlobalArgs,
926
+ "home",
927
+ ...(useFollowingFeed ? ["--following"] : []),
928
+ "--count",
929
+ String(this.config.pullCount),
930
+ "--json",
931
+ "--plain",
932
+ ];
933
+ const result = await runBirdCommand(args, this.config.commandTimeoutMs);
934
+ if (result.code !== 0) {
935
+ throw new Error(formatBirdFailure(`bird home${useFollowingFeed ? " --following" : ""} failed`, result));
936
+ }
937
+ const stdout = result.stdout.trim();
938
+ if (!stdout) {
939
+ throw new Error(`bird home${useFollowingFeed ? " --following" : ""} returned empty output`);
940
+ }
941
+ try {
942
+ return JSON.parse(stdout);
943
+ }
944
+ catch (error) {
945
+ throw new Error(`bird home${useFollowingFeed ? " --following" : ""} returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
946
+ }
947
+ }
948
+ async fetchTweetById(tweetId) {
949
+ const env_1 = { stack: [], error: void 0, hasError: false };
950
+ try {
951
+ const args = [
952
+ ...this.birdGlobalArgs,
953
+ "read",
954
+ tweetId,
955
+ "--json-full",
956
+ "--plain",
957
+ ];
958
+ const tempPrefix = join(tmpdir(), "mono-pilot-bird-read-");
959
+ const tempDir = __addDisposableResource(env_1, await createAsyncDisposableTempDir(tempPrefix), true);
960
+ const tempOutputPath = join(tempDir.path, `${tweetId}.json`);
961
+ const result = await runBirdCommandToFile(args, this.config.commandTimeoutMs, tempOutputPath);
962
+ if (result.code !== 0) {
963
+ throw new Error(formatBirdFailure(`bird read failed (${tweetId})`, { ...result, stdout: "" }));
964
+ }
965
+ const stdout = (await readFile(tempOutputPath, "utf-8")).trim();
966
+ if (!stdout) {
967
+ throw new Error(`bird read returned empty output (${tweetId})`);
968
+ }
969
+ let payload;
970
+ try {
971
+ payload = JSON.parse(stdout);
972
+ }
973
+ catch (error) {
974
+ throw new Error(`bird read returned invalid JSON (${tweetId}): ${error instanceof Error ? error.message : String(error)}`);
975
+ }
976
+ if (!isRecord(payload) || !looksLikeTweetRecord(payload)) {
977
+ throw new Error(`bird read output missing tweet object (${tweetId})`);
978
+ }
979
+ const normalized = {};
980
+ const text = extractBestText(payload);
981
+ const fullText = extractBestFullText(payload);
982
+ const media = extractBestMedia(payload);
983
+ if (text) {
984
+ normalized.text = text;
985
+ }
986
+ if (fullText) {
987
+ normalized.fullText = fullText;
988
+ }
989
+ if (media) {
990
+ normalized.media = media;
991
+ }
992
+ return normalized;
993
+ }
994
+ catch (e_1) {
995
+ env_1.error = e_1;
996
+ env_1.hasError = true;
997
+ }
998
+ finally {
999
+ const result_1 = __disposeResources(env_1);
1000
+ if (result_1)
1001
+ await result_1;
1002
+ }
1003
+ }
1004
+ }
1005
+ function validateCollectorConfig(config, context) {
1006
+ if (!config.enabled) {
1007
+ return false;
1008
+ }
1009
+ if (config.pullCount <= 0) {
1010
+ logClusterEvent("warn", "twitter_collector_disabled_invalid_pull_count", context, {
1011
+ pullCount: config.pullCount,
1012
+ });
1013
+ return false;
1014
+ }
1015
+ if (config.pullIntervalMinutes <= 0) {
1016
+ logClusterEvent("warn", "twitter_collector_disabled_invalid_interval", context, {
1017
+ pullIntervalMinutes: config.pullIntervalMinutes,
1018
+ });
1019
+ return false;
1020
+ }
1021
+ return true;
1022
+ }
1023
+ export async function maybeStartTwitterCollector(context) {
1024
+ let configObject;
1025
+ try {
1026
+ configObject = await loadMonoPilotConfigObject();
1027
+ }
1028
+ catch (error) {
1029
+ logClusterEvent("warn", "twitter_collector_config_load_failed", context, {
1030
+ error: error instanceof Error ? error.message : String(error),
1031
+ });
1032
+ return null;
1033
+ }
1034
+ const config = extractTwitterCollectorConfig(configObject);
1035
+ if (!validateCollectorConfig(config, context)) {
1036
+ return null;
1037
+ }
1038
+ const collector = new TwitterCollector(config, context);
1039
+ try {
1040
+ await collector.start();
1041
+ }
1042
+ catch (error) {
1043
+ const message = error instanceof Error ? error.message : String(error);
1044
+ emitClusterV2TwitterCollectorStartupFailed({
1045
+ scope: context.scope ?? "default",
1046
+ error: message,
1047
+ });
1048
+ logClusterEvent("warn", "twitter_collector_start_failed", context, {
1049
+ error: message,
1050
+ action: "skip_until_next_cluster_init",
1051
+ });
1052
+ return null;
1053
+ }
1054
+ return collector;
1055
+ }