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,779 @@
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 { mkdtemp, readFile, readdir, writeFile, rm } from "node:fs/promises";
56
+ import { homedir, tmpdir } from "node:os";
57
+ import { isAbsolute, join, parse, resolve } from "node:path";
58
+ const COOPERATIVE_YIELD_EVERY_LINES = 20;
59
+ const COOPERATIVE_YIELD_EVERY_TWEETS = 20;
60
+ const ITEM_PROGRESS_NOTIFY_EVERY_TWEETS = 4;
61
+ async function cooperativeYield() {
62
+ await new Promise((resolve) => {
63
+ setImmediate(resolve);
64
+ });
65
+ }
66
+ function isRecord(value) {
67
+ return typeof value === "object" && value !== null && !Array.isArray(value);
68
+ }
69
+ function readString(value) {
70
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
71
+ }
72
+ function readNestedString(record, path) {
73
+ let current = record;
74
+ for (const key of path) {
75
+ if (!isRecord(current) || !(key in current)) {
76
+ return null;
77
+ }
78
+ current = current[key];
79
+ }
80
+ return readString(current);
81
+ }
82
+ function readNestedRecord(record, path) {
83
+ let current = record;
84
+ for (const key of path) {
85
+ if (!isRecord(current) || !(key in current)) {
86
+ return null;
87
+ }
88
+ current = current[key];
89
+ }
90
+ return isRecord(current) ? current : null;
91
+ }
92
+ function readNestedArray(record, path) {
93
+ let current = record;
94
+ for (const key of path) {
95
+ if (!isRecord(current) || !(key in current)) {
96
+ return null;
97
+ }
98
+ current = current[key];
99
+ }
100
+ return Array.isArray(current) ? current : null;
101
+ }
102
+ function firstNonEmptyString(candidates) {
103
+ for (const candidate of candidates) {
104
+ if (typeof candidate === "string" && candidate.trim().length > 0) {
105
+ return candidate;
106
+ }
107
+ }
108
+ return null;
109
+ }
110
+ function expandAndResolvePath(rawPath) {
111
+ const trimmed = rawPath.trim();
112
+ if (!trimmed) {
113
+ return trimmed;
114
+ }
115
+ let expanded = trimmed;
116
+ if (expanded === "~") {
117
+ expanded = homedir();
118
+ }
119
+ else if (expanded.startsWith("~/")) {
120
+ expanded = join(homedir(), expanded.slice(2));
121
+ }
122
+ return isAbsolute(expanded) ? expanded : resolve(expanded);
123
+ }
124
+ function buildDailyArchivePath(baseOutputPath, dateStamp) {
125
+ const parsed = parse(baseOutputPath);
126
+ const ext = parsed.ext || ".jsonl";
127
+ const name = parsed.name || parsed.base || "home";
128
+ return join(parsed.dir, `${name}.${dateStamp}${ext}`);
129
+ }
130
+ function escapeRegExp(text) {
131
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
132
+ }
133
+ async function listArchiveFiles(baseOutputPath) {
134
+ const parsed = parse(baseOutputPath);
135
+ const ext = parsed.ext || ".jsonl";
136
+ const name = parsed.name || parsed.base || "home";
137
+ const pattern = new RegExp(`^${escapeRegExp(name)}\\.(\\d{4}-\\d{2}-\\d{2})${escapeRegExp(ext)}$`);
138
+ let entries;
139
+ try {
140
+ entries = await readdir(parsed.dir);
141
+ }
142
+ catch {
143
+ return [];
144
+ }
145
+ return entries
146
+ .filter((entry) => pattern.test(entry))
147
+ .map((entry) => join(parsed.dir, entry))
148
+ .sort((a, b) => a.localeCompare(b));
149
+ }
150
+ function buildBirdGlobalArgs(config) {
151
+ const args = [];
152
+ if (config.chromeProfile) {
153
+ args.push("--chrome-profile", config.chromeProfile);
154
+ }
155
+ if (config.chromeProfileDir) {
156
+ args.push("--chrome-profile-dir", config.chromeProfileDir);
157
+ }
158
+ if (config.firefoxProfile) {
159
+ args.push("--firefox-profile", config.firefoxProfile);
160
+ }
161
+ for (const source of config.cookieSource) {
162
+ args.push("--cookie-source", source);
163
+ }
164
+ if (config.cookieTimeoutMs) {
165
+ args.push("--cookie-timeout", String(config.cookieTimeoutMs));
166
+ }
167
+ if (config.requestTimeoutMs) {
168
+ args.push("--timeout", String(config.requestTimeoutMs));
169
+ }
170
+ return args;
171
+ }
172
+ function runBirdCommand(args, timeoutMs) {
173
+ return new Promise((resolve, reject) => {
174
+ const child = spawn("bird", args, {
175
+ stdio: ["ignore", "pipe", "pipe"],
176
+ });
177
+ let stdout = "";
178
+ let stderr = "";
179
+ let timedOut = false;
180
+ const timer = setTimeout(() => {
181
+ timedOut = true;
182
+ child.kill("SIGKILL");
183
+ }, timeoutMs);
184
+ timer.unref();
185
+ child.stdout.on("data", (chunk) => {
186
+ stdout += String(chunk);
187
+ });
188
+ child.stderr.on("data", (chunk) => {
189
+ stderr += String(chunk);
190
+ });
191
+ child.on("error", (error) => {
192
+ clearTimeout(timer);
193
+ reject(error);
194
+ });
195
+ child.on("close", (code, signal) => {
196
+ clearTimeout(timer);
197
+ resolve({
198
+ code,
199
+ signal,
200
+ stdout,
201
+ stderr,
202
+ timedOut,
203
+ });
204
+ });
205
+ });
206
+ }
207
+ function runBirdCommandToFile(args, timeoutMs, outputPath) {
208
+ return new Promise((resolve, reject) => {
209
+ let stderr = "";
210
+ let timedOut = false;
211
+ let stdoutFd;
212
+ try {
213
+ stdoutFd = openSync(outputPath, "w");
214
+ }
215
+ catch (error) {
216
+ reject(error);
217
+ return;
218
+ }
219
+ const child = spawn("bird", args, {
220
+ stdio: ["ignore", stdoutFd, "pipe"],
221
+ });
222
+ const timer = setTimeout(() => {
223
+ timedOut = true;
224
+ child.kill("SIGKILL");
225
+ }, timeoutMs);
226
+ timer.unref();
227
+ const closeStdoutFd = () => {
228
+ try {
229
+ closeSync(stdoutFd);
230
+ }
231
+ catch {
232
+ // no-op
233
+ }
234
+ };
235
+ if (!child.stderr) {
236
+ clearTimeout(timer);
237
+ closeStdoutFd();
238
+ reject(new Error("bird stderr stream is unavailable"));
239
+ return;
240
+ }
241
+ child.stderr.on("data", (chunk) => {
242
+ stderr += String(chunk);
243
+ });
244
+ child.on("error", (error) => {
245
+ clearTimeout(timer);
246
+ closeStdoutFd();
247
+ reject(error);
248
+ });
249
+ child.on("close", (code, signal) => {
250
+ clearTimeout(timer);
251
+ closeStdoutFd();
252
+ resolve({
253
+ code,
254
+ signal,
255
+ stderr,
256
+ timedOut,
257
+ });
258
+ });
259
+ });
260
+ }
261
+ function formatBirdFailure(prefix, result) {
262
+ const stderr = result.stderr.trim();
263
+ const stdout = result.stdout.trim();
264
+ const detail = stderr || stdout || `exit=${result.code ?? "null"} signal=${result.signal ?? "none"}`;
265
+ if (result.timedOut) {
266
+ return `${prefix}: timed out (${detail})`;
267
+ }
268
+ return `${prefix}: ${detail}`;
269
+ }
270
+ function extractTweetId(record) {
271
+ return (readString(record.id) ??
272
+ readString(record.id_str) ??
273
+ readString(record.rest_id) ??
274
+ readNestedString(record, ["legacy", "id_str"]));
275
+ }
276
+ function extractStatusIdFromUrl(url) {
277
+ const normalized = url.trim();
278
+ if (!normalized) {
279
+ return null;
280
+ }
281
+ const matchedUserStatus = normalized.match(/^https?:\/\/(?:www\.)?(?:x|twitter)\.com\/[A-Za-z0-9_]+\/status\/(\d+)/i);
282
+ if (matchedUserStatus?.[1]) {
283
+ return matchedUserStatus[1];
284
+ }
285
+ const matchedWebStatus = normalized.match(/^https?:\/\/(?:www\.)?(?:x|twitter)\.com\/i\/web\/status\/(\d+)/i);
286
+ if (matchedWebStatus?.[1]) {
287
+ return matchedWebStatus[1];
288
+ }
289
+ return null;
290
+ }
291
+ function extractTcoUrlsFromText(text) {
292
+ if (!text) {
293
+ return [];
294
+ }
295
+ const deduped = new Set();
296
+ const pattern = /https?:\/\/t\.co\/[A-Za-z0-9]+/gi;
297
+ pattern.lastIndex = 0;
298
+ let matched;
299
+ while ((matched = pattern.exec(text)) !== null) {
300
+ const url = matched[0]?.trim();
301
+ if (url) {
302
+ deduped.add(url);
303
+ }
304
+ }
305
+ return [...deduped];
306
+ }
307
+ function extractBestFullText(record) {
308
+ const raw = isRecord(record._raw) ? record._raw : null;
309
+ const rawRetweeted = raw ? readNestedRecord(raw, ["legacy", "retweeted_status_result", "result"]) : null;
310
+ return firstNonEmptyString([
311
+ readNestedString(record, ["note_tweet", "note_tweet_results", "result", "text"]),
312
+ readNestedString(record, ["note_tweet_results", "result", "text"]),
313
+ rawRetweeted ? readNestedString(rawRetweeted, ["note_tweet", "note_tweet_results", "result", "text"]) : null,
314
+ rawRetweeted ? readNestedString(rawRetweeted, ["legacy", "full_text"]) : null,
315
+ rawRetweeted ? readString(rawRetweeted.text) : null,
316
+ raw ? readNestedString(raw, ["note_tweet", "note_tweet_results", "result", "text"]) : null,
317
+ raw ? readNestedString(raw, ["note_tweet_results", "result", "text"]) : null,
318
+ readNestedString(record, ["legacy", "full_text"]),
319
+ readString(record.full_text),
320
+ raw ? readNestedString(raw, ["legacy", "full_text"]) : null,
321
+ readString(record.text),
322
+ ]);
323
+ }
324
+ function extractBestText(record) {
325
+ return firstNonEmptyString([
326
+ readString(record.text),
327
+ readString(record.full_text),
328
+ readNestedString(record, ["legacy", "full_text"]),
329
+ readNestedString(record, ["legacy", "text"]),
330
+ ]);
331
+ }
332
+ function extractBestMedia(record) {
333
+ if (Array.isArray(record.media)) {
334
+ return record.media.map((item) => (isRecord(item) ? { ...item } : item));
335
+ }
336
+ const raw = isRecord(record._raw) ? record._raw : null;
337
+ const rawRetweeted = raw ? readNestedRecord(raw, ["legacy", "retweeted_status_result", "result"]) : null;
338
+ const candidates = [
339
+ raw ? readNestedArray(raw, ["legacy", "extended_entities", "media"]) : null,
340
+ rawRetweeted ? readNestedArray(rawRetweeted, ["media"]) : null,
341
+ rawRetweeted ? readNestedArray(rawRetweeted, ["legacy", "extended_entities", "media"]) : null,
342
+ ];
343
+ for (const candidate of candidates) {
344
+ if (Array.isArray(candidate) && candidate.length > 0) {
345
+ return candidate.map((item) => (isRecord(item) ? { ...item } : item));
346
+ }
347
+ }
348
+ return null;
349
+ }
350
+ function collectShortLinksForTweet(tweet, tweetFull) {
351
+ const links = new Set();
352
+ for (const shortUrl of extractTcoUrlsFromText(readString(tweet.text))) {
353
+ links.add(shortUrl);
354
+ }
355
+ if (tweetFull) {
356
+ for (const shortUrl of extractTcoUrlsFromText(readString(tweetFull.text))) {
357
+ links.add(shortUrl);
358
+ }
359
+ for (const shortUrl of extractTcoUrlsFromText(readString(tweetFull.fullText))) {
360
+ links.add(shortUrl);
361
+ }
362
+ }
363
+ return [...links];
364
+ }
365
+ async function createAsyncDisposableTempDir(prefix) {
366
+ const path = await mkdtemp(prefix);
367
+ let removed = false;
368
+ const remove = async () => {
369
+ if (removed) {
370
+ return;
371
+ }
372
+ removed = true;
373
+ await rm(path, { recursive: true, force: true });
374
+ };
375
+ return {
376
+ path,
377
+ remove,
378
+ [Symbol.asyncDispose]: remove,
379
+ };
380
+ }
381
+ function hasTweetFullLikeField(value) {
382
+ if (!isRecord(value)) {
383
+ return false;
384
+ }
385
+ return Boolean(readString(value.text) || readString(value.fullText) || Array.isArray(value.media));
386
+ }
387
+ async function verifyBirdAccess(config) {
388
+ const args = [...buildBirdGlobalArgs(config), "check"];
389
+ const result = await runBirdCommand(args, config.commandTimeoutMs);
390
+ if (result.code !== 0) {
391
+ throw new Error(formatBirdFailure("bird check failed", result));
392
+ }
393
+ }
394
+ async function fetchTweetById(tweetId, config, stats) {
395
+ stats.tweetReadAttempts += 1;
396
+ const args = [...buildBirdGlobalArgs(config), "read", tweetId, "--json-full", "--plain"];
397
+ const tempPrefix = join(tmpdir(), "mono-pilot-digest-backfill-");
398
+ try {
399
+ const env_1 = { stack: [], error: void 0, hasError: false };
400
+ try {
401
+ const tempDir = __addDisposableResource(env_1, await createAsyncDisposableTempDir(tempPrefix), true);
402
+ const tempOutputPath = join(tempDir.path, `${tweetId}.json`);
403
+ const result = await runBirdCommandToFile(args, config.commandTimeoutMs, tempOutputPath);
404
+ if (result.code !== 0) {
405
+ const asFullResult = {
406
+ code: result.code,
407
+ signal: result.signal,
408
+ stdout: "",
409
+ stderr: result.stderr,
410
+ timedOut: result.timedOut,
411
+ };
412
+ throw new Error(formatBirdFailure(`bird read failed (${tweetId})`, asFullResult));
413
+ }
414
+ const stdout = (await readFile(tempOutputPath, "utf-8")).trim();
415
+ if (!stdout) {
416
+ throw new Error(`bird read returned empty output (${tweetId})`);
417
+ }
418
+ const payload = JSON.parse(stdout);
419
+ if (!isRecord(payload)) {
420
+ throw new Error(`bird read output missing tweet object (${tweetId})`);
421
+ }
422
+ const normalized = {};
423
+ const text = extractBestText(payload);
424
+ const fullText = extractBestFullText(payload);
425
+ const media = extractBestMedia(payload);
426
+ if (text) {
427
+ normalized.text = text;
428
+ }
429
+ if (fullText) {
430
+ normalized.fullText = fullText;
431
+ }
432
+ if (media) {
433
+ normalized.media = media;
434
+ }
435
+ stats.tweetReadSuccess += 1;
436
+ return normalized;
437
+ }
438
+ catch (e_1) {
439
+ env_1.error = e_1;
440
+ env_1.hasError = true;
441
+ }
442
+ finally {
443
+ const result_1 = __disposeResources(env_1);
444
+ if (result_1)
445
+ await result_1;
446
+ }
447
+ }
448
+ catch {
449
+ stats.tweetReadFailed += 1;
450
+ return null;
451
+ }
452
+ }
453
+ async function resolveShortLinkMapping(shortUrl, config, stats) {
454
+ stats.shortLinkResolveAttempts += 1;
455
+ let currentUrl = shortUrl;
456
+ let resolvedUrl = shortUrl;
457
+ const maxRedirects = 8;
458
+ for (let hop = 0; hop < maxRedirects; hop += 1) {
459
+ const directStatusId = extractStatusIdFromUrl(currentUrl);
460
+ if (directStatusId) {
461
+ return { shortUrl, resolvedUrl: currentUrl, statusId: directStatusId };
462
+ }
463
+ let response;
464
+ try {
465
+ const timeoutMs = Math.max(500, config.requestTimeoutMs ?? config.commandTimeoutMs);
466
+ response = await fetch(currentUrl, {
467
+ method: "GET",
468
+ redirect: "manual",
469
+ signal: AbortSignal.timeout(timeoutMs),
470
+ });
471
+ }
472
+ catch {
473
+ stats.shortLinkResolveFailed += 1;
474
+ return { shortUrl, resolvedUrl, statusId: null };
475
+ }
476
+ if (response.url) {
477
+ resolvedUrl = response.url;
478
+ }
479
+ const responseStatusId = extractStatusIdFromUrl(response.url);
480
+ if (responseStatusId) {
481
+ void response.body?.cancel().catch(() => {
482
+ // no-op
483
+ });
484
+ return { shortUrl, resolvedUrl: response.url, statusId: responseStatusId };
485
+ }
486
+ const location = response.headers.get("location");
487
+ const isRedirect = response.status >= 300 && response.status < 400;
488
+ void response.body?.cancel().catch(() => {
489
+ // no-op
490
+ });
491
+ if (!isRedirect || !location) {
492
+ return { shortUrl, resolvedUrl, statusId: null };
493
+ }
494
+ try {
495
+ currentUrl = new URL(location, currentUrl).toString();
496
+ resolvedUrl = currentUrl;
497
+ }
498
+ catch {
499
+ stats.shortLinkResolveFailed += 1;
500
+ return { shortUrl, resolvedUrl, statusId: null };
501
+ }
502
+ }
503
+ return { shortUrl, resolvedUrl, statusId: null };
504
+ }
505
+ function readExistingShortLinkMappings(tweet) {
506
+ if (!Array.isArray(tweet.shortLinkMappings)) {
507
+ return [];
508
+ }
509
+ return tweet.shortLinkMappings.filter((item) => isRecord(item));
510
+ }
511
+ async function fillMissingTweetField(tweetId, cache, config, stats) {
512
+ if (!tweetId) {
513
+ return null;
514
+ }
515
+ if (cache.has(tweetId)) {
516
+ return cache.get(tweetId) ?? null;
517
+ }
518
+ const fetched = await fetchTweetById(tweetId, config, stats);
519
+ cache.set(tweetId, fetched ?? null);
520
+ return fetched;
521
+ }
522
+ async function enrichShortLinkMappings(tweet, tweetFull, selfTweetId, cache, config, stats) {
523
+ const existingMappings = readExistingShortLinkMappings(tweet);
524
+ const byShortUrl = new Map();
525
+ const order = [];
526
+ for (const mapping of existingMappings) {
527
+ const shortUrl = readString(mapping.shortUrl);
528
+ if (!shortUrl) {
529
+ continue;
530
+ }
531
+ if (!byShortUrl.has(shortUrl)) {
532
+ byShortUrl.set(shortUrl, { ...mapping, shortUrl });
533
+ order.push(shortUrl);
534
+ }
535
+ }
536
+ const detectedShortLinks = collectShortLinksForTweet(tweet, tweetFull);
537
+ for (const shortUrl of detectedShortLinks) {
538
+ if (!byShortUrl.has(shortUrl)) {
539
+ byShortUrl.set(shortUrl, { shortUrl, resolvedUrl: shortUrl, tweetId: null });
540
+ order.push(shortUrl);
541
+ }
542
+ }
543
+ let changed = false;
544
+ let addedMappings = 0;
545
+ let addedTweetFull = 0;
546
+ for (const shortUrl of order) {
547
+ const mapping = byShortUrl.get(shortUrl);
548
+ if (!mapping) {
549
+ continue;
550
+ }
551
+ const existingResolvedUrl = readString(mapping.resolvedUrl) ?? shortUrl;
552
+ let tweetId = readString(mapping.tweetId);
553
+ if (!tweetId) {
554
+ tweetId = extractStatusIdFromUrl(existingResolvedUrl);
555
+ if (tweetId) {
556
+ mapping.tweetId = tweetId;
557
+ changed = true;
558
+ }
559
+ }
560
+ if (!tweetId && (!readString(mapping.resolvedUrl) || !readString(mapping.tweetId))) {
561
+ const resolved = await resolveShortLinkMapping(shortUrl, config, stats);
562
+ if ((resolved.resolvedUrl ?? null) !== (readString(mapping.resolvedUrl) ?? null)) {
563
+ mapping.resolvedUrl = resolved.resolvedUrl;
564
+ changed = true;
565
+ }
566
+ if ((resolved.statusId ?? null) !== (readString(mapping.tweetId) ?? null)) {
567
+ mapping.tweetId = resolved.statusId;
568
+ changed = true;
569
+ }
570
+ tweetId = resolved.statusId;
571
+ }
572
+ if (tweetId && tweetId !== selfTweetId && !hasTweetFullLikeField(mapping.tweetFull)) {
573
+ const fetched = await fillMissingTweetField(tweetId, cache, config, stats);
574
+ if (fetched) {
575
+ mapping.tweetFull = { ...fetched };
576
+ changed = true;
577
+ addedTweetFull += 1;
578
+ }
579
+ }
580
+ }
581
+ if (existingMappings.length === 0 && order.length > 0) {
582
+ addedMappings += order.length;
583
+ changed = true;
584
+ }
585
+ else {
586
+ addedMappings += Math.max(0, order.length - existingMappings.length);
587
+ }
588
+ const mappings = order.map((shortUrl) => byShortUrl.get(shortUrl));
589
+ return { mappings, changed, addedMappings, addedTweetFull };
590
+ }
591
+ async function backfillTweet(tweet, cache, config, stats) {
592
+ let changed = false;
593
+ let tweetFullAdded = false;
594
+ let quotedTweetFullAdded = false;
595
+ const tweetId = extractTweetId(tweet);
596
+ let tweetFull = isRecord(tweet.tweetFull) ? tweet.tweetFull : null;
597
+ if (!hasTweetFullLikeField(tweetFull) && tweetId) {
598
+ const fetched = await fillMissingTweetField(tweetId, cache, config, stats);
599
+ if (fetched) {
600
+ tweet.tweetFull = { ...fetched };
601
+ tweetFull = fetched;
602
+ changed = true;
603
+ tweetFullAdded = true;
604
+ }
605
+ }
606
+ const quoted = isRecord(tweet.quotedTweet) ? tweet.quotedTweet : null;
607
+ let quotedFull = isRecord(tweet.quotedTweetFull) ? tweet.quotedTweetFull : null;
608
+ if (quoted && !hasTweetFullLikeField(quotedFull)) {
609
+ const quotedId = extractTweetId(quoted);
610
+ const fetchedQuoted = await fillMissingTweetField(quotedId, cache, config, stats);
611
+ if (fetchedQuoted) {
612
+ tweet.quotedTweetFull = { ...fetchedQuoted };
613
+ quotedFull = fetchedQuoted;
614
+ changed = true;
615
+ quotedTweetFullAdded = true;
616
+ }
617
+ }
618
+ const mappingsResult = await enrichShortLinkMappings(tweet, tweetFull, tweetId, cache, config, stats);
619
+ if (mappingsResult.mappings.length > 0 && mappingsResult.changed) {
620
+ tweet.shortLinkMappings = mappingsResult.mappings;
621
+ changed = true;
622
+ }
623
+ return {
624
+ changed,
625
+ tweetFullAdded,
626
+ quotedTweetFullAdded,
627
+ mappingsAdded: mappingsResult.addedMappings,
628
+ shortLinkTweetFullAdded: mappingsResult.addedTweetFull,
629
+ };
630
+ }
631
+ async function processArchiveFile(filePath, cache, config, stats, onTweetScanned) {
632
+ const raw = await readFile(filePath, "utf-8");
633
+ const hasTrailingNewline = raw.endsWith("\n");
634
+ const lines = raw.split(/\r?\n/);
635
+ if (hasTrailingNewline && lines[lines.length - 1] === "") {
636
+ lines.pop();
637
+ }
638
+ let changed = false;
639
+ let tweetsScanned = 0;
640
+ let tweetsUpdated = 0;
641
+ let tweetFullAdded = 0;
642
+ let quotedTweetFullAdded = 0;
643
+ let shortLinkMappingsAdded = 0;
644
+ let shortLinkTweetFullAdded = 0;
645
+ let processedLinesSinceYield = 0;
646
+ let processedTweetsSinceYield = 0;
647
+ let fileTweetCount = 0;
648
+ for (let index = 0; index < lines.length; index += 1) {
649
+ const line = lines[index];
650
+ if (!line || line.trim().length === 0) {
651
+ continue;
652
+ }
653
+ processedLinesSinceYield += 1;
654
+ if (processedLinesSinceYield >= COOPERATIVE_YIELD_EVERY_LINES) {
655
+ processedLinesSinceYield = 0;
656
+ await cooperativeYield();
657
+ }
658
+ let parsed;
659
+ try {
660
+ parsed = JSON.parse(line);
661
+ }
662
+ catch {
663
+ continue;
664
+ }
665
+ if (!isRecord(parsed) || !Array.isArray(parsed.tweets)) {
666
+ continue;
667
+ }
668
+ let lineChanged = false;
669
+ for (let tweetIndex = 0; tweetIndex < parsed.tweets.length; tweetIndex += 1) {
670
+ const rawTweet = parsed.tweets[tweetIndex];
671
+ if (!isRecord(rawTweet)) {
672
+ continue;
673
+ }
674
+ processedTweetsSinceYield += 1;
675
+ if (processedTweetsSinceYield >= COOPERATIVE_YIELD_EVERY_TWEETS) {
676
+ processedTweetsSinceYield = 0;
677
+ await cooperativeYield();
678
+ }
679
+ tweetsScanned += 1;
680
+ fileTweetCount += 1;
681
+ onTweetScanned?.(fileTweetCount);
682
+ const result = await backfillTweet(rawTweet, cache, config, stats);
683
+ if (result.changed) {
684
+ lineChanged = true;
685
+ tweetsUpdated += 1;
686
+ }
687
+ if (result.tweetFullAdded) {
688
+ tweetFullAdded += 1;
689
+ }
690
+ if (result.quotedTweetFullAdded) {
691
+ quotedTweetFullAdded += 1;
692
+ }
693
+ shortLinkMappingsAdded += result.mappingsAdded;
694
+ shortLinkTweetFullAdded += result.shortLinkTweetFullAdded;
695
+ }
696
+ if (lineChanged) {
697
+ lines[index] = JSON.stringify(parsed);
698
+ changed = true;
699
+ }
700
+ }
701
+ if (changed) {
702
+ const nextRaw = lines.join("\n");
703
+ await writeFile(filePath, hasTrailingNewline ? `${nextRaw}\n` : nextRaw, "utf-8");
704
+ }
705
+ return {
706
+ changed,
707
+ tweetsScanned,
708
+ tweetsUpdated,
709
+ tweetFullAdded,
710
+ quotedTweetFullAdded,
711
+ shortLinkMappingsAdded,
712
+ shortLinkTweetFullAdded,
713
+ };
714
+ }
715
+ export async function runDigestBackfill(options) {
716
+ const { twitterConfig, date, file, notify } = options;
717
+ const stats = {
718
+ filesScanned: 0,
719
+ filesUpdated: 0,
720
+ tweetsScanned: 0,
721
+ tweetsUpdated: 0,
722
+ tweetFullAdded: 0,
723
+ quotedTweetFullAdded: 0,
724
+ shortLinkMappingsAdded: 0,
725
+ shortLinkTweetFullAdded: 0,
726
+ tweetReadAttempts: 0,
727
+ tweetReadSuccess: 0,
728
+ tweetReadFailed: 0,
729
+ shortLinkResolveAttempts: 0,
730
+ shortLinkResolveFailed: 0,
731
+ };
732
+ const targets = file
733
+ ? [expandAndResolvePath(file)]
734
+ : date
735
+ ? [buildDailyArchivePath(twitterConfig.outputPath, date)]
736
+ : await listArchiveFiles(twitterConfig.outputPath);
737
+ if (targets.length === 0) {
738
+ notify("Digest backfill: no archive files found.", "warning");
739
+ return;
740
+ }
741
+ notify(`Digest backfill start: files=${targets.length} (serial mode).`, "info");
742
+ try {
743
+ await verifyBirdAccess(twitterConfig);
744
+ }
745
+ catch (error) {
746
+ notify(`Digest backfill aborted: ${error instanceof Error ? error.message : String(error)}`, "error");
747
+ return;
748
+ }
749
+ const tweetFullCache = new Map();
750
+ let totalTweetScanned = 0;
751
+ for (let index = 0; index < targets.length; index += 1) {
752
+ const filePath = targets[index];
753
+ stats.filesScanned += 1;
754
+ notify(`Digest backfill progress: ${index + 1}/${targets.length} file=${filePath}`, "info");
755
+ let result;
756
+ try {
757
+ result = await processArchiveFile(filePath, tweetFullCache, twitterConfig, stats, (fileTweetCount) => {
758
+ totalTweetScanned += 1;
759
+ if (totalTweetScanned % ITEM_PROGRESS_NOTIFY_EVERY_TWEETS === 0) {
760
+ notify(`Digest backfill item progress: totalTweets=${totalTweetScanned}, currentFile=${index + 1}/${targets.length}, fileTweets=${fileTweetCount}, file=${parse(filePath).base}`, "info");
761
+ }
762
+ });
763
+ }
764
+ catch (error) {
765
+ notify(`Digest backfill file failed: ${filePath} (${error instanceof Error ? error.message : String(error)})`, "warning");
766
+ continue;
767
+ }
768
+ stats.tweetsScanned += result.tweetsScanned;
769
+ stats.tweetsUpdated += result.tweetsUpdated;
770
+ stats.tweetFullAdded += result.tweetFullAdded;
771
+ stats.quotedTweetFullAdded += result.quotedTweetFullAdded;
772
+ stats.shortLinkMappingsAdded += result.shortLinkMappingsAdded;
773
+ stats.shortLinkTweetFullAdded += result.shortLinkTweetFullAdded;
774
+ if (result.changed) {
775
+ stats.filesUpdated += 1;
776
+ }
777
+ }
778
+ notify(`Digest backfill done: files=${stats.filesScanned}, updatedFiles=${stats.filesUpdated}, tweets=${stats.tweetsScanned}, updatedTweets=${stats.tweetsUpdated}, +tweetFull=${stats.tweetFullAdded}, +quotedTweetFull=${stats.quotedTweetFullAdded}, +shortLinkMappings=${stats.shortLinkMappingsAdded}, +shortLinkTweetFull=${stats.shortLinkTweetFullAdded}, birdRead=${stats.tweetReadAttempts}/${stats.tweetReadSuccess} ok, shortLinkResolve=${stats.shortLinkResolveAttempts} (failed=${stats.shortLinkResolveFailed}).`, "info");
779
+ }