mono-pilot 0.2.10 → 0.2.13

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 (155) hide show
  1. package/README.md +260 -2
  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 +1 -2
  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 +70 -1
  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/memory/build-memory.js +103 -0
  77. package/dist/src/memory/config/defaults.js +55 -0
  78. package/dist/src/memory/config/loader.js +29 -0
  79. package/dist/src/memory/config/paths.js +9 -0
  80. package/dist/src/memory/config/resolve.js +90 -0
  81. package/dist/src/memory/config/types.js +1 -0
  82. package/dist/src/memory/embeddings/batch-runner.js +39 -0
  83. package/dist/src/memory/embeddings/cache.js +47 -0
  84. package/dist/src/memory/embeddings/chunk-limits.js +26 -0
  85. package/dist/src/memory/embeddings/input-limits.js +48 -0
  86. package/dist/src/memory/embeddings/local.js +108 -0
  87. package/dist/src/memory/embeddings/types.js +1 -0
  88. package/dist/src/memory/index-manager.js +552 -0
  89. package/dist/src/memory/indexing/embeddings.js +67 -0
  90. package/dist/src/memory/indexing/files.js +180 -0
  91. package/dist/src/memory/indexing/index-file.js +105 -0
  92. package/dist/src/memory/log.js +38 -0
  93. package/dist/src/memory/paths.js +15 -0
  94. package/dist/src/memory/runtime/index.js +299 -0
  95. package/dist/src/memory/runtime/thread.js +116 -0
  96. package/dist/src/memory/search/fts.js +57 -0
  97. package/dist/src/memory/search/hybrid.js +50 -0
  98. package/dist/src/memory/search/text.js +30 -0
  99. package/dist/src/memory/search/vector.js +43 -0
  100. package/dist/src/memory/session/content-hash.js +7 -0
  101. package/dist/src/memory/session/entry.js +33 -0
  102. package/dist/src/memory/session/flush-policy.js +34 -0
  103. package/dist/src/memory/session/hook.js +191 -0
  104. package/dist/src/memory/session/paths.js +15 -0
  105. package/dist/src/memory/session/session-reader.js +88 -0
  106. package/dist/src/memory/session/transcript/content-hash.js +7 -0
  107. package/dist/src/memory/session/transcript/entry.js +28 -0
  108. package/dist/src/memory/session/transcript/flush.js +56 -0
  109. package/dist/src/memory/session/transcript/paths.js +28 -0
  110. package/dist/src/memory/session/transcript/reader.js +112 -0
  111. package/dist/src/memory/session/transcript/state.js +31 -0
  112. package/dist/src/memory/store/schema.js +89 -0
  113. package/dist/src/memory/store/sqlite.js +89 -0
  114. package/dist/src/memory/types.js +1 -0
  115. package/dist/src/memory/warm.js +25 -0
  116. package/dist/{tools → src/tools}/README.md +28 -2
  117. package/dist/{tools → src/tools}/apply-patch-description.md +8 -2
  118. package/dist/{tools → src/tools}/apply-patch.js +174 -104
  119. package/dist/{tools → src/tools}/apply-patch.test.js +52 -1
  120. package/dist/{tools/ask-question.js → src/tools/ask-user-question.js} +3 -3
  121. package/dist/src/tools/ast-grep.js +357 -0
  122. package/dist/src/tools/brief-write.js +122 -0
  123. package/dist/src/tools/bus-send.js +100 -0
  124. package/dist/{tools → src/tools}/call-mcp-tool.js +20 -24
  125. package/dist/src/tools/codex-apply-patch-description.md +52 -0
  126. package/dist/src/tools/codex-apply-patch.js +540 -0
  127. package/dist/{tools → src/tools}/delete.js +24 -0
  128. package/dist/src/tools/exit-plan-mode.js +83 -0
  129. package/dist/{tools → src/tools}/fetch-mcp-resource.js +31 -3
  130. package/dist/src/tools/generate-image.js +567 -0
  131. package/dist/{tools → src/tools}/glob.js +55 -1
  132. package/dist/{tools → src/tools}/list-mcp-resources.js +32 -3
  133. package/dist/{tools → src/tools}/list-mcp-tools.js +38 -3
  134. package/dist/src/tools/ls.js +48 -0
  135. package/dist/src/tools/lsp-diagnostics.js +67 -0
  136. package/dist/src/tools/lsp-symbols.js +54 -0
  137. package/dist/src/tools/mailbox.js +85 -0
  138. package/dist/src/tools/memory-get.js +90 -0
  139. package/dist/src/tools/memory-search.js +180 -0
  140. package/dist/{tools → src/tools}/plan-mode-reminder.md +3 -4
  141. package/dist/{tools → src/tools}/read-file.js +8 -19
  142. package/dist/{tools → src/tools}/rg.js +10 -20
  143. package/dist/{tools → src/tools}/shell.js +19 -42
  144. package/dist/{tools → src/tools}/subagent.js +255 -6
  145. package/dist/{tools → src/tools}/switch-mode.js +37 -6
  146. package/dist/{tools → src/tools}/web-fetch.js +105 -7
  147. package/dist/{tools → src/tools}/web-search.js +29 -1
  148. package/package.json +21 -9
  149. package/dist/src/utils/mcp-client.js +0 -282
  150. /package/dist/{tools → src/tools}/ask-mode-reminder.md +0 -0
  151. /package/dist/{tools → src/tools}/rg.test.js +0 -0
  152. /package/dist/{tools → src/tools}/semantic-search-description.md +0 -0
  153. /package/dist/{tools → src/tools}/semantic-search.js +0 -0
  154. /package/dist/{tools → src/tools}/shell-description.md +0 -0
  155. /package/dist/{tools → src/tools}/subagent-description.md +0 -0
@@ -0,0 +1 @@
1
+ export { maybeStartTwitterCollector } from "./collector.js";
@@ -0,0 +1,78 @@
1
+ const DEFAULT_TEMPERATURE = 0;
2
+ const DEFAULT_MAX_TOKENS = 300;
3
+ const DEFAULT_CONCURRENCY = 4;
4
+ function isRecord(value) {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
7
+ function readString(value) {
8
+ if (typeof value !== "string") {
9
+ return undefined;
10
+ }
11
+ const trimmed = value.trim();
12
+ return trimmed.length > 0 ? trimmed : undefined;
13
+ }
14
+ function normalizeNumber(value, fallback, { integer, min, max, } = {}) {
15
+ if (typeof value !== "number" || !Number.isFinite(value)) {
16
+ return fallback;
17
+ }
18
+ let next = integer ? Math.floor(value) : value;
19
+ if (typeof min === "number") {
20
+ next = Math.max(min, next);
21
+ }
22
+ if (typeof max === "number") {
23
+ next = Math.min(max, next);
24
+ }
25
+ return next;
26
+ }
27
+ function readClassifierRecord(value) {
28
+ if (!isRecord(value)) {
29
+ return {};
30
+ }
31
+ return isRecord(value.classifier) ? value.classifier : {};
32
+ }
33
+ function readTwitterDigestClassifier(config) {
34
+ if (!isRecord(config.twitter)) {
35
+ return {};
36
+ }
37
+ const twitter = config.twitter;
38
+ return readClassifierRecord(twitter.digest);
39
+ }
40
+ function readLegacyDigestClassifier(config) {
41
+ return readClassifierRecord(config.digest);
42
+ }
43
+ export function extractDigestConfig(config) {
44
+ if (!config) {
45
+ return {
46
+ classifier: {
47
+ provider: undefined,
48
+ model: undefined,
49
+ temperature: DEFAULT_TEMPERATURE,
50
+ maxTokens: DEFAULT_MAX_TOKENS,
51
+ concurrency: DEFAULT_CONCURRENCY,
52
+ },
53
+ };
54
+ }
55
+ const twitterClassifier = readTwitterDigestClassifier(config);
56
+ const legacyClassifier = readLegacyDigestClassifier(config);
57
+ const provider = readString(twitterClassifier.provider) ?? readString(legacyClassifier.provider);
58
+ const model = readString(twitterClassifier.model) ?? readString(legacyClassifier.model);
59
+ const temperatureRaw = twitterClassifier.temperature ?? legacyClassifier.temperature;
60
+ const maxTokensRaw = twitterClassifier.maxTokens ?? legacyClassifier.maxTokens;
61
+ const concurrencyRaw = twitterClassifier.concurrency ?? legacyClassifier.concurrency;
62
+ return {
63
+ classifier: {
64
+ provider,
65
+ model,
66
+ temperature: normalizeNumber(temperatureRaw, DEFAULT_TEMPERATURE, { min: 0, max: 2 }),
67
+ maxTokens: normalizeNumber(maxTokensRaw, DEFAULT_MAX_TOKENS, {
68
+ integer: true,
69
+ min: 1,
70
+ }),
71
+ concurrency: normalizeNumber(concurrencyRaw, DEFAULT_CONCURRENCY, {
72
+ integer: true,
73
+ min: 1,
74
+ max: 16,
75
+ }),
76
+ },
77
+ };
78
+ }
@@ -0,0 +1,143 @@
1
+ import { homedir } from "node:os";
2
+ import { isAbsolute, join, resolve } from "node:path";
3
+ export const DISCORD_SUBSCRIBE_EVENTS = [
4
+ "MESSAGE_CREATE",
5
+ "MESSAGE_UPDATE",
6
+ "MESSAGE_DELETE",
7
+ ];
8
+ const DEFAULT_OUTPUT_PATH = join(homedir(), ".mono-pilot", "discord", "messages.jsonl");
9
+ const DEFAULT_RECONNECT_DELAY_MS = 30_000;
10
+ const DEFAULT_SYSTEM_EVENT_BATCH_SIZE = 20;
11
+ const DEFAULT_SCOPES = ["rpc", "messages.read", "identify"];
12
+ function isRecord(value) {
13
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14
+ }
15
+ function readString(value) {
16
+ if (typeof value !== "string") {
17
+ return undefined;
18
+ }
19
+ const trimmed = value.trim();
20
+ return trimmed.length > 0 ? trimmed : undefined;
21
+ }
22
+ function readStringArray(value) {
23
+ if (typeof value === "string") {
24
+ const values = value
25
+ .split(/\s+/)
26
+ .map((item) => item.trim())
27
+ .filter((item) => item.length > 0);
28
+ return [...new Set(values)];
29
+ }
30
+ if (!Array.isArray(value)) {
31
+ return [];
32
+ }
33
+ const values = value
34
+ .filter((item) => typeof item === "string")
35
+ .map((item) => item.trim())
36
+ .filter((item) => item.length > 0);
37
+ return [...new Set(values)];
38
+ }
39
+ function normalizeChannels(value) {
40
+ if (!Array.isArray(value)) {
41
+ return [];
42
+ }
43
+ const seen = new Set();
44
+ const channels = [];
45
+ for (const item of value) {
46
+ if (!isRecord(item)) {
47
+ continue;
48
+ }
49
+ const id = readString(item.id);
50
+ if (!id || seen.has(id)) {
51
+ continue;
52
+ }
53
+ seen.add(id);
54
+ const alias = readString(item.alias);
55
+ channels.push(alias ? { id, alias } : { id });
56
+ }
57
+ return channels;
58
+ }
59
+ function normalizeScopes(value) {
60
+ const scopes = readStringArray(value);
61
+ if (scopes.length > 0) {
62
+ return scopes;
63
+ }
64
+ return [...DEFAULT_SCOPES];
65
+ }
66
+ function normalizeOutputPath(rawPath) {
67
+ if (!rawPath) {
68
+ return DEFAULT_OUTPUT_PATH;
69
+ }
70
+ let expanded = rawPath;
71
+ if (expanded === "~") {
72
+ expanded = homedir();
73
+ }
74
+ else if (expanded.startsWith("~/")) {
75
+ expanded = join(homedir(), expanded.slice(2));
76
+ }
77
+ if (isAbsolute(expanded)) {
78
+ return expanded;
79
+ }
80
+ return resolve(expanded);
81
+ }
82
+ function normalizeEvents(value) {
83
+ const allowed = new Set(DISCORD_SUBSCRIBE_EVENTS);
84
+ const list = readStringArray(value)
85
+ .map((item) => item.toUpperCase())
86
+ .filter((item) => allowed.has(item));
87
+ if (list.length === 0) {
88
+ return ["MESSAGE_CREATE"];
89
+ }
90
+ return [...new Set(list)];
91
+ }
92
+ function normalizeReconnectDelay(value) {
93
+ if (typeof value !== "number" || !Number.isFinite(value)) {
94
+ return DEFAULT_RECONNECT_DELAY_MS;
95
+ }
96
+ const normalized = Math.floor(value);
97
+ if (normalized <= 0) {
98
+ return DEFAULT_RECONNECT_DELAY_MS;
99
+ }
100
+ return normalized;
101
+ }
102
+ function normalizeSystemEventBatchSize(value) {
103
+ if (typeof value !== "number" || !Number.isFinite(value)) {
104
+ return DEFAULT_SYSTEM_EVENT_BATCH_SIZE;
105
+ }
106
+ const normalized = Math.floor(value);
107
+ if (normalized <= 0) {
108
+ return DEFAULT_SYSTEM_EVENT_BATCH_SIZE;
109
+ }
110
+ return normalized;
111
+ }
112
+ export function extractDiscordCollectorConfig(config) {
113
+ if (!config || !isRecord(config.discord)) {
114
+ return {
115
+ enabled: false,
116
+ scopes: [...DEFAULT_SCOPES],
117
+ channels: [],
118
+ events: ["MESSAGE_CREATE"],
119
+ outputPath: DEFAULT_OUTPUT_PATH,
120
+ includeRawPayload: false,
121
+ maxReconnectDelayMs: DEFAULT_RECONNECT_DELAY_MS,
122
+ systemEventBatchSize: DEFAULT_SYSTEM_EVENT_BATCH_SIZE,
123
+ };
124
+ }
125
+ const discord = config.discord;
126
+ const channels = normalizeChannels(discord.channels);
127
+ const fallbackChannelIds = readStringArray(discord.channelIds);
128
+ const normalizedChannels = channels.length > 0 ? channels : fallbackChannelIds.map((id) => ({ id }));
129
+ return {
130
+ enabled: discord.enabled === true,
131
+ clientId: readString(discord.clientId),
132
+ accessToken: readString(discord.accessToken),
133
+ clientSecret: readString(discord.clientSecret),
134
+ redirectUri: readString(discord.redirectUri),
135
+ scopes: normalizeScopes(discord.scopes),
136
+ channels: normalizedChannels,
137
+ events: normalizeEvents(discord.events),
138
+ outputPath: normalizeOutputPath(readString(discord.outputPath)),
139
+ includeRawPayload: discord.includeRawPayload === true,
140
+ maxReconnectDelayMs: normalizeReconnectDelay(discord.maxReconnectDelayMs),
141
+ systemEventBatchSize: normalizeSystemEventBatchSize(discord.systemEventBatchSize),
142
+ };
143
+ }
@@ -0,0 +1,48 @@
1
+ function isRecord(value) {
2
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3
+ }
4
+ function readString(value) {
5
+ return typeof value === "string" ? value : undefined;
6
+ }
7
+ export function extractImageGenConfig(config) {
8
+ const providers = {};
9
+ const selection = {};
10
+ if (!config) {
11
+ return { providers, selection };
12
+ }
13
+ if (isRecord(config.imageGen)) {
14
+ selection.provider = readString(config.imageGen.provider);
15
+ selection.model = readString(config.imageGen.model);
16
+ }
17
+ if (isRecord(config.imageGenProviders)) {
18
+ for (const [key, value] of Object.entries(config.imageGenProviders)) {
19
+ if (!isRecord(value))
20
+ continue;
21
+ const modelsRaw = Array.isArray(value.models) ? value.models : [];
22
+ const models = modelsRaw
23
+ .map((entry) => {
24
+ if (!isRecord(entry))
25
+ return undefined;
26
+ const id = readString(entry.id);
27
+ if (!id)
28
+ return undefined;
29
+ const name = readString(entry.name);
30
+ return name ? { id, name } : { id };
31
+ })
32
+ .filter((entry) => Boolean(entry));
33
+ providers[key] = {
34
+ baseUrl: readString(value.baseUrl),
35
+ apiKey: readString(value.apiKey),
36
+ authHeader: typeof value.authHeader === "boolean" ? value.authHeader : undefined,
37
+ models: models.length > 0 ? models : undefined,
38
+ };
39
+ }
40
+ }
41
+ return { providers, selection };
42
+ }
43
+ export function applyImageGenSelection(config, selection) {
44
+ config.imageGen = {
45
+ provider: selection.provider,
46
+ model: selection.model,
47
+ };
48
+ }
@@ -0,0 +1,31 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { getUserConfigDir, getUserConfigPath } from "../memory/config/paths.js";
4
+ function isRecord(value) {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
7
+ export async function loadMonoPilotConfigObject() {
8
+ const configPath = getUserConfigPath();
9
+ if (!existsSync(configPath))
10
+ return {};
11
+ const raw = await readFile(configPath, "utf-8");
12
+ let parsed;
13
+ try {
14
+ parsed = JSON.parse(raw);
15
+ }
16
+ catch (error) {
17
+ const message = error instanceof Error ? error.message : String(error);
18
+ throw new Error(`Invalid JSON in mono-pilot config: ${message}`);
19
+ }
20
+ if (!isRecord(parsed)) {
21
+ throw new Error("MonoPilot config root must be a JSON object.");
22
+ }
23
+ return parsed;
24
+ }
25
+ export async function saveMonoPilotConfigObject(config) {
26
+ const configDir = getUserConfigDir();
27
+ await mkdir(configDir, { recursive: true });
28
+ const configPath = getUserConfigPath();
29
+ const payload = JSON.stringify(config, null, 2);
30
+ await writeFile(configPath, `${payload}\n`, "utf-8");
31
+ }
@@ -0,0 +1,100 @@
1
+ import { homedir } from "node:os";
2
+ import { isAbsolute, join, resolve } from "node:path";
3
+ const DEFAULT_OUTPUT_PATH = join(homedir(), ".mono-pilot", "twitter", "home.jsonl");
4
+ const DEFAULT_PULL_INTERVAL_MINUTES = 10;
5
+ const DEFAULT_PULL_COUNT = 10;
6
+ const DEFAULT_COMMAND_TIMEOUT_MS = 30_000;
7
+ function isRecord(value) {
8
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9
+ }
10
+ function readString(value) {
11
+ if (typeof value !== "string") {
12
+ return undefined;
13
+ }
14
+ const trimmed = value.trim();
15
+ return trimmed.length > 0 ? trimmed : undefined;
16
+ }
17
+ function readStringArray(value) {
18
+ if (typeof value === "string") {
19
+ const values = value
20
+ .split(/\s+/)
21
+ .map((item) => item.trim())
22
+ .filter((item) => item.length > 0);
23
+ return [...new Set(values)];
24
+ }
25
+ if (!Array.isArray(value)) {
26
+ return [];
27
+ }
28
+ const values = value
29
+ .filter((item) => typeof item === "string")
30
+ .map((item) => item.trim())
31
+ .filter((item) => item.length > 0);
32
+ return [...new Set(values)];
33
+ }
34
+ function normalizeOutputPath(rawPath) {
35
+ if (!rawPath) {
36
+ return DEFAULT_OUTPUT_PATH;
37
+ }
38
+ let expanded = rawPath;
39
+ if (expanded === "~") {
40
+ expanded = homedir();
41
+ }
42
+ else if (expanded.startsWith("~/")) {
43
+ expanded = join(homedir(), expanded.slice(2));
44
+ }
45
+ if (isAbsolute(expanded)) {
46
+ return expanded;
47
+ }
48
+ return resolve(expanded);
49
+ }
50
+ function normalizePositiveInteger(value, fallback) {
51
+ if (typeof value !== "number" || !Number.isFinite(value)) {
52
+ return fallback;
53
+ }
54
+ const normalized = Math.floor(value);
55
+ if (normalized <= 0) {
56
+ return fallback;
57
+ }
58
+ return normalized;
59
+ }
60
+ function normalizeOptionalPositiveInteger(value) {
61
+ if (value === undefined || value === null) {
62
+ return undefined;
63
+ }
64
+ if (typeof value !== "number" || !Number.isFinite(value)) {
65
+ return undefined;
66
+ }
67
+ const normalized = Math.floor(value);
68
+ if (normalized <= 0) {
69
+ return undefined;
70
+ }
71
+ return normalized;
72
+ }
73
+ export function extractTwitterCollectorConfig(config) {
74
+ if (!config || !isRecord(config.twitter)) {
75
+ return {
76
+ enabled: false,
77
+ outputPath: DEFAULT_OUTPUT_PATH,
78
+ pullIntervalMinutes: DEFAULT_PULL_INTERVAL_MINUTES,
79
+ pullCount: DEFAULT_PULL_COUNT,
80
+ includeRawPayload: false,
81
+ commandTimeoutMs: DEFAULT_COMMAND_TIMEOUT_MS,
82
+ cookieSource: [],
83
+ };
84
+ }
85
+ const twitter = config.twitter;
86
+ return {
87
+ enabled: twitter.enabled === true,
88
+ outputPath: normalizeOutputPath(readString(twitter.outputPath)),
89
+ pullIntervalMinutes: normalizePositiveInteger(twitter.pullIntervalMinutes, DEFAULT_PULL_INTERVAL_MINUTES),
90
+ pullCount: normalizePositiveInteger(twitter.pullCount, DEFAULT_PULL_COUNT),
91
+ includeRawPayload: twitter.includeRawPayload === true,
92
+ commandTimeoutMs: normalizePositiveInteger(twitter.commandTimeoutMs, DEFAULT_COMMAND_TIMEOUT_MS),
93
+ requestTimeoutMs: normalizeOptionalPositiveInteger(twitter.requestTimeoutMs),
94
+ chromeProfile: readString(twitter.chromeProfile),
95
+ chromeProfileDir: readString(twitter.chromeProfileDir),
96
+ firefoxProfile: readString(twitter.firefoxProfile),
97
+ cookieSource: readStringArray(twitter.cookieSource),
98
+ cookieTimeoutMs: normalizeOptionalPositiveInteger(twitter.cookieTimeoutMs),
99
+ };
100
+ }