@finchagentic/mcp 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +345 -0
  3. package/dist/_http-cache.js +96 -0
  4. package/dist/agent-loop.js +231 -0
  5. package/dist/annotations.js +113 -0
  6. package/dist/cli.js +1195 -0
  7. package/dist/clink-input.js +15 -0
  8. package/dist/config.js +132 -0
  9. package/dist/convex.js +151 -0
  10. package/dist/dex-pair.js +54 -0
  11. package/dist/enrichment-router.js +315 -0
  12. package/dist/index.js +256 -0
  13. package/dist/llm.js +323 -0
  14. package/dist/local-memory.js +102 -0
  15. package/dist/local-vault.js +454 -0
  16. package/dist/output-schemas.js +551 -0
  17. package/dist/prompts.js +111 -0
  18. package/dist/public-url.js +107 -0
  19. package/dist/resources.js +116 -0
  20. package/dist/server.js +300 -0
  21. package/dist/signal-gate.js +57 -0
  22. package/dist/token-decimals.js +26 -0
  23. package/dist/token-gate.js +88 -0
  24. package/dist/tool-filter.js +44 -0
  25. package/dist/tools/_solidity-scan.js +313 -0
  26. package/dist/tools/agents.js +729 -0
  27. package/dist/tools/automation.js +314 -0
  28. package/dist/tools/base-mcp.js +478 -0
  29. package/dist/tools/base.js +269 -0
  30. package/dist/tools/chronicle.js +268 -0
  31. package/dist/tools/coder.js +94 -0
  32. package/dist/tools/deep-research.js +1416 -0
  33. package/dist/tools/defi.js +291 -0
  34. package/dist/tools/equity.js +364 -0
  35. package/dist/tools/events.js +182 -0
  36. package/dist/tools/framework.js +150 -0
  37. package/dist/tools/github.js +514 -0
  38. package/dist/tools/insider.js +264 -0
  39. package/dist/tools/insight.js +634 -0
  40. package/dist/tools/market.js +555 -0
  41. package/dist/tools/memory.js +1046 -0
  42. package/dist/tools/miroshark.js +343 -0
  43. package/dist/tools/monitor.js +319 -0
  44. package/dist/tools/os.js +226 -0
  45. package/dist/tools/packets.js +296 -0
  46. package/dist/tools/research-chain.js +226 -0
  47. package/dist/tools/research-compare.js +280 -0
  48. package/dist/tools/research.js +188 -0
  49. package/dist/tools/rh-bridge.js +148 -0
  50. package/dist/tools/rh-mcp.js +1411 -0
  51. package/dist/tools/rh-orders.js +471 -0
  52. package/dist/tools/scanner.js +534 -0
  53. package/dist/tools/vault.js +764 -0
  54. package/dist/tools/wallet.js +200 -0
  55. package/dist/types.js +2 -0
  56. package/dist/wallet.js +184 -0
  57. package/package.json +87 -0
@@ -0,0 +1,343 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MIROSHARK_TOOLS = void 0;
4
+ exports.handleMirosharkTool = handleMirosharkTool;
5
+ const CONVEX_SITE = process.env.FINCH_CONVEX_URL ?? "https://befitting-porcupine-276.convex.site";
6
+ exports.MIROSHARK_TOOLS = [
7
+ {
8
+ name: "miroshark_simulate",
9
+ description: "Simulate any scenario using MiroShark multi-agent AI. Use this when the user asks to simulate, model, or explore 'what happens if' - market crashes, regulations, social events, macro changes. " +
10
+ "AI agents act as traders, analysts, journalists, and social actors. Returns a simulation_id to track progress.",
11
+ inputSchema: {
12
+ type: "object",
13
+ properties: {
14
+ scenario: {
15
+ type: "string",
16
+ description: "Plain-English description of the scenario to simulate. E.g. 'What happens if ETH drops 20% and whale wallets start selling?'",
17
+ },
18
+ },
19
+ required: ["scenario"],
20
+ },
21
+ },
22
+ {
23
+ name: "miroshark_status",
24
+ description: "Poll the status of a MiroShark simulation. Returns preparation progress, running progress, or - when finished - the full agent action feed with a per-type breakdown for YOU to read the run from. Automatically starts the simulation when agent preparation completes. No API key needed.",
25
+ inputSchema: {
26
+ type: "object",
27
+ properties: {
28
+ simulation_id: {
29
+ type: "string",
30
+ description: "Simulation ID returned by miroshark_simulate",
31
+ },
32
+ },
33
+ required: ["simulation_id"],
34
+ },
35
+ },
36
+ {
37
+ name: "miroshark_stop",
38
+ description: "Stop a running MiroShark simulation.",
39
+ inputSchema: {
40
+ type: "object",
41
+ properties: {
42
+ simulation_id: {
43
+ type: "string",
44
+ description: "Simulation ID to stop",
45
+ },
46
+ },
47
+ required: ["simulation_id"],
48
+ },
49
+ },
50
+ ];
51
+ // โ”€โ”€ HTTP helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
52
+ function authHeaders() {
53
+ const key = process.env.FINCH_API_KEY ?? process.env.FINCH_SESSION_TOKEN;
54
+ return key ? { Authorization: `Bearer ${key}` } : {};
55
+ }
56
+ async function miroJson(path, method, body, timeoutMs = 90000) {
57
+ const res = await fetch(`${CONVEX_SITE}${path}`, {
58
+ method,
59
+ headers: { ...authHeaders(), "Content-Type": "application/json" },
60
+ body: body !== undefined ? JSON.stringify(body) : undefined,
61
+ signal: AbortSignal.timeout(timeoutMs),
62
+ });
63
+ const text = await res.text();
64
+ if (!res.ok)
65
+ throw new Error(`MiroShark ${method} ${path} [${res.status}]: ${text.slice(0, 300)}`);
66
+ let json;
67
+ try {
68
+ json = JSON.parse(text);
69
+ }
70
+ catch {
71
+ throw new Error(`MiroShark non-JSON response: ${text.slice(0, 200)}`);
72
+ }
73
+ if (json.success === false)
74
+ throw new Error(`MiroShark error: ${json.error ?? JSON.stringify(json).slice(0, 300)}`);
75
+ return json.data ?? json;
76
+ }
77
+ async function miroForm(path, form, timeoutMs = 120000) {
78
+ // No Content-Type header - browser/fetch sets multipart boundary automatically
79
+ const res = await fetch(`${CONVEX_SITE}${path}`, {
80
+ method: "POST",
81
+ headers: authHeaders(),
82
+ body: form,
83
+ signal: AbortSignal.timeout(timeoutMs),
84
+ });
85
+ const text = await res.text();
86
+ if (!res.ok)
87
+ throw new Error(`MiroShark POST ${path} [${res.status}]: ${text.slice(0, 300)}`);
88
+ let json;
89
+ try {
90
+ json = JSON.parse(text);
91
+ }
92
+ catch {
93
+ throw new Error(`MiroShark non-JSON response: ${text.slice(0, 200)}`);
94
+ }
95
+ if (json.success === false)
96
+ throw new Error(`MiroShark error: ${json.error ?? JSON.stringify(json).slice(0, 300)}`);
97
+ return json.data ?? json;
98
+ }
99
+ async function pollUntilDone(taskPath, pollIntervalMs = 8000, maxWaitMs = 180000) {
100
+ const deadline = Date.now() + maxWaitMs;
101
+ while (Date.now() < deadline) {
102
+ await new Promise(r => setTimeout(r, pollIntervalMs));
103
+ const task = await miroJson(taskPath, "GET");
104
+ const s = (task.status ?? "").toLowerCase();
105
+ if (s === "completed" || s === "success")
106
+ return task;
107
+ if (s === "failed" || s === "error") {
108
+ throw new Error(`Task failed: ${task.error ?? task.message ?? s}`);
109
+ }
110
+ }
111
+ throw new Error("Task timed out after 3 minutes");
112
+ }
113
+ // โ”€โ”€ Tool handler โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
114
+ async function handleMirosharkTool(name, args) {
115
+ const a = (args ?? {});
116
+ // โ”€โ”€ miroshark_simulate โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
117
+ if (name === "miroshark_simulate") {
118
+ if (!a.scenario?.trim()) {
119
+ return { content: [{ type: "text", text: "scenario is required" }], isError: true };
120
+ }
121
+ try {
122
+ // Step 1: convert plain-English question into a structured seed document
123
+ let asked;
124
+ try {
125
+ asked = await miroJson("/miroshark/api/simulation/ask", "POST", { question: a.scenario });
126
+ }
127
+ catch (err) {
128
+ const msg = err.message ?? "";
129
+ if (msg.includes("[404]") || msg.includes("[503]") || msg.includes("Upstream unreachable") || msg.includes("[502]")) {
130
+ return {
131
+ content: [{
132
+ type: "text",
133
+ text: "MiroShark simulation service is currently unavailable. The backend may be redeploying โ€” try again in a few minutes. If this persists, the MIROSHARK_URL Cloudflare secret may need to be updated (`wrangler secret put MIROSHARK_URL`).",
134
+ }],
135
+ isError: true,
136
+ };
137
+ }
138
+ throw err;
139
+ }
140
+ const { title, seed_document, simulation_requirement } = asked;
141
+ // Step 2: generate knowledge-graph ontology from the seed document
142
+ const form = new FormData();
143
+ form.append("simulation_requirement", simulation_requirement ?? a.scenario);
144
+ form.append("project_name", (title ?? a.scenario).slice(0, 100));
145
+ form.append("url_docs", JSON.stringify([{
146
+ title: title ?? "Simulation Context",
147
+ url: "",
148
+ text: seed_document ?? a.scenario,
149
+ }]));
150
+ const ontology = await miroForm("/miroshark/api/graph/ontology/generate", form);
151
+ const projectId = ontology.project_id;
152
+ if (!projectId)
153
+ throw new Error("No project_id in ontology response");
154
+ // Step 3: kick off the async graph build
155
+ const built = await miroJson("/miroshark/api/graph/build", "POST", { project_id: projectId });
156
+ const graphTaskId = built.task_id;
157
+ if (!graphTaskId)
158
+ throw new Error("No task_id in graph build response");
159
+ // Step 4: wait for graph to finish (up to 3 min)
160
+ await pollUntilDone(`/miroshark/api/graph/task/${graphTaskId}`);
161
+ // Step 5: create simulation from the built graph
162
+ const created = await miroJson("/miroshark/api/simulation/create", "POST", { project_id: projectId });
163
+ const simId = created.simulation_id ?? created.id;
164
+ if (!simId)
165
+ throw new Error("No simulation_id in create response");
166
+ // Step 6: kick off agent preparation (async - don't block)
167
+ const prepared = await miroJson("/miroshark/api/simulation/prepare", "POST", { simulation_id: simId, parallel_profile_count: 10 });
168
+ const prepTaskId = prepared.task_id;
169
+ return {
170
+ content: [{
171
+ type: "text",
172
+ text: [
173
+ `**MiroShark simulation queued** โœ“`,
174
+ ``,
175
+ `Scenario: ${a.scenario}`,
176
+ `Project: \`${projectId}\``,
177
+ `Simulation ID: \`${simId}\``,
178
+ `Status: preparing agents${prepTaskId ? ` (task: ${prepTaskId})` : ""}`,
179
+ ``,
180
+ `Agent preparation runs in the background. Poll progress with:`,
181
+ `\`miroshark_status simulation_id="${simId}"\``,
182
+ ].join("\n"),
183
+ }],
184
+ };
185
+ }
186
+ catch (err) {
187
+ return { content: [{ type: "text", text: `MiroShark error: ${err.message}` }], isError: true };
188
+ }
189
+ }
190
+ // โ”€โ”€ miroshark_status โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
191
+ if (name === "miroshark_status") {
192
+ if (!a.simulation_id?.trim()) {
193
+ return { content: [{ type: "text", text: "simulation_id is required" }], isError: true };
194
+ }
195
+ const simId = a.simulation_id.trim();
196
+ if (!/^[a-zA-Z0-9_-]{5,100}$/.test(simId)) {
197
+ return { content: [{ type: "text", text: "Invalid simulation_id format." }], isError: true };
198
+ }
199
+ try {
200
+ // Check run status first
201
+ const runStatus = await miroJson(`/miroshark/api/simulation/${simId}/run-status`, "GET").catch(() => ({ runner_status: "idle" }));
202
+ const runnerStatus = (runStatus?.runner_status ?? "idle").toLowerCase();
203
+ // If not yet running, check whether agents are prepared by probing /config
204
+ // (config only exists after /prepare completes)
205
+ if (runnerStatus === "idle") {
206
+ const config = await miroJson(`/miroshark/api/simulation/${simId}/config`, "GET").catch(() => null);
207
+ if (!config) {
208
+ // Preparation still in progress - check profiles for real-time progress
209
+ const profiles = await miroJson(`/miroshark/api/simulation/${simId}/profiles/realtime`, "GET").catch(() => null);
210
+ const total = profiles?.total_expected ?? "?";
211
+ const ready = profiles?.profiles_ready ?? 0;
212
+ return {
213
+ content: [{
214
+ type: "text",
215
+ text: [
216
+ `**MiroShark \`${simId}\`** - preparing agents`,
217
+ total !== "?" ? `Profiles: ${ready} / ${total} ready` : `Profiles generating...`,
218
+ ``,
219
+ `Poll again in ~10 seconds.`,
220
+ ].join("\n"),
221
+ }],
222
+ };
223
+ }
224
+ // Config exists โ†’ agents prepared โ†’ auto-start
225
+ await miroJson("/miroshark/api/simulation/start", "POST", {
226
+ simulation_id: simId,
227
+ platform: "parallel",
228
+ });
229
+ return {
230
+ content: [{
231
+ type: "text",
232
+ text: [
233
+ `**MiroShark \`${simId}\`** - simulation started`,
234
+ ``,
235
+ `Agents are now active. Poll again in ~15 seconds for progress.`,
236
+ `\`miroshark_status simulation_id="${simId}"\``,
237
+ ].join("\n"),
238
+ }],
239
+ };
240
+ }
241
+ if (runnerStatus === "running") {
242
+ const round = runStatus.current_round ?? 0;
243
+ const total = runStatus.total_rounds ?? "?";
244
+ const pct = runStatus.progress_percent?.toFixed(1) ?? "0";
245
+ const twitterActs = runStatus.twitter_actions_count ?? 0;
246
+ const redditActs = runStatus.reddit_actions_count ?? 0;
247
+ return {
248
+ content: [{
249
+ type: "text",
250
+ text: [
251
+ `**MiroShark \`${simId}\`** - running`,
252
+ `Round: ${round} / ${total} (${pct}%)`,
253
+ `Actions: ${twitterActs} Twitter ยท ${redditActs} Reddit`,
254
+ ``,
255
+ `Simulation in progress - poll again in ~15 seconds.`,
256
+ ].join("\n"),
257
+ }],
258
+ };
259
+ }
260
+ if (runnerStatus === "completed" || runnerStatus === "stopped") {
261
+ const actionsData = await miroJson(`/miroshark/api/simulation/${simId}/actions?limit=100`, "GET").catch(() => ({ actions: [] }));
262
+ const actions = actionsData?.actions ?? [];
263
+ const totalActions = runStatus.total_actions_count ?? actions.length;
264
+ const rounds = runStatus.current_round ?? "?";
265
+ const ACTION_EMOJI = {
266
+ tweet: "๐Ÿฆ", post: "๐Ÿ“", sell: "๐Ÿ“‰", buy: "๐Ÿ“ˆ",
267
+ article: "๐Ÿ“ฐ", comment: "๐Ÿ’ฌ", alert: "๐Ÿšจ", analyze: "๐Ÿ”",
268
+ };
269
+ // The tool's job is polling the external simulation and returning what
270
+ // the agents actually did. Reading sentiment off that log is model work
271
+ // - and the caller can weigh it against the user's actual question,
272
+ // which a fixed server-side summariser prompt never could. The feed is
273
+ // returned in full (was: 20 of 50 shown, the rest silently dropped).
274
+ const feed = actions.map((act) => {
275
+ const who = act.agent_name ?? act.agent_id ?? "agent";
276
+ const what = (act.action_type ?? act.type ?? "action").toLowerCase();
277
+ const emoji = ACTION_EMOJI[what] ?? "โ€ข";
278
+ const content = act.content ?? act.text ?? "";
279
+ return `${emoji} **${who}** [${what}]${content ? `: ${String(content).slice(0, 200)}` : ""}`;
280
+ });
281
+ // Counts per action type - mechanical, and it tells the caller at a
282
+ // glance whether the run was trade-heavy or chatter-heavy.
283
+ const byType = new Map();
284
+ for (const act of actions) {
285
+ const what = String(act.action_type ?? act.type ?? "action").toLowerCase();
286
+ byType.set(what, (byType.get(what) ?? 0) + 1);
287
+ }
288
+ const typeLine = [...byType.entries()]
289
+ .sort((x, y) => y[1] - x[1])
290
+ .map(([t, n]) => `${t} ร—${n}`)
291
+ .join(" ยท ");
292
+ const lines = [
293
+ `**MiroShark \`${simId}\`** - ${runnerStatus}`,
294
+ `Rounds: ${rounds} ยท Actions: ${totalActions}`,
295
+ ...(typeLine ? [`Breakdown: ${typeLine}`] : []),
296
+ "",
297
+ ];
298
+ if (feed.length > 0) {
299
+ lines.push(`**Agent Feed** (${feed.length}${totalActions > feed.length ? ` of ${totalActions} โ€” API caps the page at 100` : ""}):`);
300
+ lines.push(...feed);
301
+ lines.push("", "---", "", "## Read the run from this", "", "- **Market sentiment** โ€” net direction across the agents, not the loudest one.", "- **Dominant narrative** โ€” what most agents converged on, and who dissented.", "- **Top 3 agent behaviours** โ€” name the agents and quote what they did.", "- **Outlook** โ€” what the run implies, and what it can't tell you.", "", "This is simulated agent behaviour, not market data. Say so if you carry any of it into a " +
302
+ "real position. Worth keeping? Save it with `vault_save` " +
303
+ `(type: research, key: \`miroshark-${simId.slice(0, 8)}\`).`);
304
+ }
305
+ if (feed.length === 0)
306
+ lines.push("_No agent actions recorded for this run._");
307
+ return { content: [{ type: "text", text: lines.join("\n") }] };
308
+ }
309
+ // Fallback: unknown state
310
+ return {
311
+ content: [{
312
+ type: "text",
313
+ text: [
314
+ `**MiroShark \`${simId}\`** - status: ${runnerStatus || "unknown"}`,
315
+ ``,
316
+ `If agents are still preparing, poll again shortly.`,
317
+ ].filter(Boolean).join("\n"),
318
+ }],
319
+ };
320
+ }
321
+ catch (err) {
322
+ return { content: [{ type: "text", text: `MiroShark error: ${err.message}` }], isError: true };
323
+ }
324
+ }
325
+ // โ”€โ”€ miroshark_stop โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
326
+ if (name === "miroshark_stop") {
327
+ if (!a.simulation_id?.trim()) {
328
+ return { content: [{ type: "text", text: "simulation_id is required" }], isError: true };
329
+ }
330
+ const simId = a.simulation_id.trim();
331
+ if (!/^[a-zA-Z0-9_-]{5,100}$/.test(simId)) {
332
+ return { content: [{ type: "text", text: "Invalid simulation_id format." }], isError: true };
333
+ }
334
+ try {
335
+ await miroJson(`/miroshark/api/simulation/${simId}/stop`, "POST", {});
336
+ return { content: [{ type: "text", text: `โน๏ธ Simulation \`${simId}\` stopped.` }] };
337
+ }
338
+ catch (err) {
339
+ return { content: [{ type: "text", text: `MiroShark error: ${err.message}` }], isError: true };
340
+ }
341
+ }
342
+ return null;
343
+ }
@@ -0,0 +1,319 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MONITOR_TOOLS = void 0;
4
+ exports.buildMonitorList = buildMonitorList;
5
+ exports.handleMonitorTool = handleMonitorTool;
6
+ const zod_1 = require("zod");
7
+ const convex_js_1 = require("../convex.js");
8
+ const TRIGGER_BASE = "https://api.trigger.dev/api/v1";
9
+ const MONITOR_TASK_ID = "finch-monitor";
10
+ const MONITOR_INPUT_SCHEMA = {
11
+ type: "object",
12
+ properties: {
13
+ topic: { type: "string", description: "What to research - topic, keyword, or question" },
14
+ schedule: {
15
+ type: "string",
16
+ description: "Cron expression or preset. Presets: 'daily-8am', 'daily-6pm', 'weekly-monday', 'hourly'. Or raw cron: '0 8 * * *'",
17
+ },
18
+ label: { type: "string", description: "Short label to identify this schedule (e.g. 'morning brief', 'competitor watch')" },
19
+ },
20
+ required: ["topic", "schedule"],
21
+ };
22
+ exports.MONITOR_TOOLS = [
23
+ {
24
+ name: "schedule_research",
25
+ description: "Schedule recurring autonomous research on any topic - runs on a cron schedule, saves findings to vault, " +
26
+ "and sends a Telegram notification. The agent runs completely on its own with no prompting needed. " +
27
+ "Requires TRIGGER_SECRET_KEY env var (trigger.dev). " +
28
+ "Examples: daily morning briefing, weekly competitor analysis, hourly price alerts, monthly industry report.",
29
+ inputSchema: MONITOR_INPUT_SCHEMA,
30
+ },
31
+ {
32
+ name: "list_monitors",
33
+ description: "List all active scheduled research monitors - shows topic, schedule, next run, and monitor ID.",
34
+ inputSchema: { type: "object", properties: {}, required: [] },
35
+ },
36
+ {
37
+ name: "cancel_monitor",
38
+ description: "PERMANENT. Cancel and delete a scheduled research monitor โ€” the schedule is removed and " +
39
+ "cannot be restored. Requires confirm: true. Run list_monitors first and show the user which " +
40
+ "monitor (id + topic) you are about to delete.",
41
+ inputSchema: {
42
+ type: "object",
43
+ properties: {
44
+ id: { type: "string", description: "Monitor ID (from list_monitors)" },
45
+ confirm: { type: "boolean", description: "Must be true to delete. Guards against removing the wrong monitor." },
46
+ },
47
+ required: ["id", "confirm"],
48
+ },
49
+ },
50
+ ];
51
+ const CRON_PRESETS = {
52
+ "daily-8am": "0 8 * * *",
53
+ "daily-6pm": "0 18 * * *",
54
+ "weekly-monday": "0 8 * * 1",
55
+ "hourly": "0 * * * *",
56
+ };
57
+ const CreateSchema = zod_1.z.object({
58
+ topic: zod_1.z.string().min(1).max(200),
59
+ schedule: zod_1.z.string().min(1),
60
+ label: zod_1.z.string().max(80).optional(),
61
+ });
62
+ const CancelSchema = zod_1.z.object({ id: zod_1.z.string().min(1) });
63
+ function getKey() {
64
+ return process.env.TRIGGER_SECRET_KEY ?? null;
65
+ }
66
+ function noKeyMsg() {
67
+ return {
68
+ content: [{
69
+ type: "text",
70
+ text: [
71
+ `โš ๏ธ **TRIGGER_SECRET_KEY not set.**`,
72
+ ``,
73
+ `To enable autonomous monitors:`,
74
+ `1. Sign up at trigger.dev (free tier available)`,
75
+ `2. Create a project, go to API Keys โ†’ copy your Secret Key`,
76
+ `3. Add to your MCP config env block: \`"TRIGGER_SECRET_KEY": "tr_prod_..."\``,
77
+ `4. In the finch worker directory, run: \`npx trigger.dev@latest deploy\``,
78
+ ``,
79
+ `Then monitors will run autonomously - no chat needed.`,
80
+ ].join("\n"),
81
+ }],
82
+ isError: true,
83
+ };
84
+ }
85
+ function resolveCron(input) {
86
+ return CRON_PRESETS[input] ?? input;
87
+ }
88
+ // Structured output builder for list_monitors (schema in output-schemas.ts).
89
+ function buildMonitorList(schedules, configs = {}) {
90
+ return {
91
+ count: schedules.length,
92
+ monitors: schedules.map((s) => {
93
+ const cfg = s.externalId ? configs[s.externalId] : undefined;
94
+ return {
95
+ id: s.id ?? null,
96
+ externalId: s.externalId ?? null,
97
+ label: cfg?.label ?? s.externalId ?? s.id ?? null,
98
+ topic: cfg?.topic ?? null,
99
+ cron: s.cron ?? null,
100
+ nextRun: s.nextRun ?? null,
101
+ };
102
+ }),
103
+ };
104
+ }
105
+ async function handleMonitorTool(name, args) {
106
+ if (name === "schedule_research" || name === "create_monitor") {
107
+ const parsed = CreateSchema.safeParse(args);
108
+ if (!parsed.success)
109
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
110
+ const key = getKey();
111
+ if (!key)
112
+ return noKeyMsg();
113
+ const { topic, schedule, label } = parsed.data;
114
+ const cron = resolveCron(schedule);
115
+ // Dedup gate - reject if the user already has a monitor with the exact
116
+ // same topic + schedule combo. Stops "track AI news daily" from being
117
+ // created 4ร— by an over-eager agent loop, which is what produced the
118
+ // duplicate Telegram briefings users complained about.
119
+ try {
120
+ const existingMonitors = await (0, convex_js_1.callConvex)("/vault/list", "POST", { type: "workflow", tags: ["monitor-config"] }, "list_monitors_for_dedup");
121
+ const duplicates = (existingMonitors?.entries ?? []).filter((e) => {
122
+ try {
123
+ const cfg = JSON.parse(e.content ?? "{}");
124
+ const sameTopic = (cfg.topic ?? "").toLowerCase().trim() === topic.toLowerCase().trim();
125
+ const sameCron = (cfg.cron ?? "") === cron;
126
+ return sameTopic && sameCron;
127
+ }
128
+ catch {
129
+ return false;
130
+ }
131
+ });
132
+ if (duplicates.length > 0) {
133
+ const existingIds = duplicates
134
+ .map((d) => {
135
+ try {
136
+ return JSON.parse(d.content ?? "{}").scheduleId;
137
+ }
138
+ catch {
139
+ return null;
140
+ }
141
+ })
142
+ .filter(Boolean);
143
+ return {
144
+ content: [{
145
+ type: "text",
146
+ text: [
147
+ `โš ๏ธ **Duplicate monitor blocked**`,
148
+ ``,
149
+ `You already have ${duplicates.length} active monitor${duplicates.length > 1 ? "s" : ""} for the exact topic + schedule:`,
150
+ ` โ€ข Topic: ${topic}`,
151
+ ` โ€ข Schedule: ${schedule}`,
152
+ ``,
153
+ existingIds.length > 0
154
+ ? `Existing schedule ID${existingIds.length > 1 ? "s" : ""}: ${existingIds.map((id) => `\`${id}\``).join(", ")}`
155
+ : "",
156
+ ``,
157
+ `Cancel the old one first if you want to recreate:`,
158
+ ` \`cancel_monitor id: "<schedule_id>"\``,
159
+ ``,
160
+ `Or use a more specific topic to make this a distinct monitor.`,
161
+ ].filter(Boolean).join("\n"),
162
+ }],
163
+ };
164
+ }
165
+ }
166
+ catch {
167
+ // Dedup check failed (auth or network) - continue, better to allow than
168
+ // block on infrastructure errors.
169
+ }
170
+ // Unique stable ID - worker reads config from vault using this as key
171
+ const externalId = `monitor-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
172
+ try {
173
+ const res = await fetch(`${TRIGGER_BASE}/schedules`, {
174
+ method: "POST",
175
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${key}` },
176
+ body: JSON.stringify({ task: MONITOR_TASK_ID, cron, externalId, deduplicationKey: externalId }),
177
+ signal: AbortSignal.timeout(10000),
178
+ });
179
+ if (!res.ok) {
180
+ const err = await res.text();
181
+ return { content: [{ type: "text", text: `Failed to create monitor: ${err}` }], isError: true };
182
+ }
183
+ const data = await res.json();
184
+ const scheduleId = data.id ?? externalId;
185
+ // Save config to vault so the worker can retrieve topic + metadata
186
+ let configSaved = true;
187
+ try {
188
+ await (0, convex_js_1.callConvex)("/vault/save", "POST", {
189
+ type: "workflow",
190
+ title: `Monitor: ${label ?? topic}`,
191
+ content: JSON.stringify({ topic, label: label ?? topic, cron, scheduleId, externalId }),
192
+ key: `monitor-config/${externalId}`,
193
+ agentId: "os",
194
+ tags: ["monitor-config"],
195
+ commitMsg: "create_monitor config",
196
+ }, "vault_save");
197
+ }
198
+ catch {
199
+ configSaved = false;
200
+ }
201
+ return {
202
+ content: [{
203
+ type: "text",
204
+ text: [
205
+ `โœ… **Monitor created**`,
206
+ ``,
207
+ `๐Ÿ“Œ Topic: ${topic}`,
208
+ `๐Ÿ• Schedule: ${cron}${CRON_PRESETS[schedule] ? ` (${schedule})` : ""}`,
209
+ `๐Ÿ†” ID: ${scheduleId}`,
210
+ data.nextRun ? `โญ๏ธ Next run: ${new Date(data.nextRun).toUTCString()}` : "",
211
+ ``,
212
+ configSaved
213
+ ? `The agent will research "${topic}" on schedule, save findings to vault, and send a Telegram notification if configured.`
214
+ : `โš ๏ธ Monitor schedule created but config save failed - the agent may use a default topic on first run. Try \`cancel_monitor\` and recreate.`,
215
+ `Use \`list_monitors\` to see all active monitors.`,
216
+ ].filter(Boolean).join("\n"),
217
+ }],
218
+ };
219
+ }
220
+ catch (err) {
221
+ return { content: [{ type: "text", text: `Monitor error: ${err.message}` }], isError: true };
222
+ }
223
+ }
224
+ if (name === "list_monitors") {
225
+ const key = getKey();
226
+ if (!key)
227
+ return noKeyMsg();
228
+ try {
229
+ const res = await fetch(`${TRIGGER_BASE}/schedules`, {
230
+ headers: { "Authorization": `Bearer ${key}` },
231
+ signal: AbortSignal.timeout(10000),
232
+ });
233
+ if (!res.ok) {
234
+ const err = await res.text();
235
+ return { content: [{ type: "text", text: `Failed to list monitors: ${err}` }], isError: true };
236
+ }
237
+ const data = await res.json();
238
+ const schedules = (data.data ?? []).filter((s) => (s.task ?? s.taskIdentifier) === MONITOR_TASK_ID);
239
+ if (!schedules.length) {
240
+ return {
241
+ content: [{
242
+ type: "text",
243
+ text: `No active monitors.\n\nUse \`create_monitor\` to set up an autonomous agent that runs on a schedule.`,
244
+ }],
245
+ structuredContent: buildMonitorList([]),
246
+ };
247
+ }
248
+ // Load topic labels from vault for each schedule
249
+ const configMap = new Map();
250
+ await Promise.allSettled(schedules
251
+ .filter(s => s.externalId)
252
+ .map(async (s) => {
253
+ try {
254
+ // `/vault/entry`, not `/vault/read` โ€” the latter 404s, which this
255
+ // catch used to hide, leaving every monitor listed by raw id.
256
+ const cfg = await (0, convex_js_1.callConvex)(`/vault/entry?key=monitor-config/${s.externalId}`, "GET", undefined, "vault_read");
257
+ if (cfg?.content) {
258
+ const parsed = JSON.parse(cfg.content);
259
+ configMap.set(s.externalId, { topic: parsed.topic, label: parsed.label ?? parsed.topic });
260
+ }
261
+ }
262
+ catch { /* skip - show raw externalId */ }
263
+ }));
264
+ const lines = [`๐Ÿ“‹ **Active Monitors** - ${schedules.length} running\n`];
265
+ for (const s of schedules) {
266
+ const cfg = s.externalId ? configMap.get(s.externalId) : undefined;
267
+ const label = cfg?.label ?? s.externalId ?? s.id;
268
+ const topic = cfg?.topic ? ` - ${cfg.topic}` : "";
269
+ const next = s.nextRun ? new Date(s.nextRun).toUTCString() : "unknown";
270
+ lines.push(`**${label}**${topic}`);
271
+ lines.push(` ID: \`${s.id}\` ยท Cron: \`${s.cron}\` ยท Next: ${next}`);
272
+ lines.push("");
273
+ }
274
+ lines.push(`Use \`cancel_monitor id: "<id>"\` to stop a monitor.`);
275
+ return {
276
+ content: [{ type: "text", text: lines.join("\n") }],
277
+ structuredContent: buildMonitorList(schedules, Object.fromEntries(configMap)),
278
+ };
279
+ }
280
+ catch (err) {
281
+ return { content: [{ type: "text", text: `List error: ${err.message}` }], isError: true };
282
+ }
283
+ }
284
+ if (name === "cancel_monitor") {
285
+ const parsed = CancelSchema.safeParse(args);
286
+ if (!parsed.success)
287
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
288
+ if (args?.confirm !== true) {
289
+ return {
290
+ content: [{
291
+ type: "text",
292
+ text: "Refusing to cancel: this deletes the monitor's schedule permanently. Show the user " +
293
+ "which monitor it is (run `list_monitors`), then pass `confirm: true`.",
294
+ }],
295
+ isError: true,
296
+ };
297
+ }
298
+ const key = getKey();
299
+ if (!key)
300
+ return noKeyMsg();
301
+ const { id } = parsed.data;
302
+ try {
303
+ const res = await fetch(`${TRIGGER_BASE}/schedules/${id}`, {
304
+ method: "DELETE",
305
+ headers: { "Authorization": `Bearer ${key}` },
306
+ signal: AbortSignal.timeout(10000),
307
+ });
308
+ if (!res.ok) {
309
+ const err = await res.text();
310
+ return { content: [{ type: "text", text: `Failed to cancel monitor: ${err}` }], isError: true };
311
+ }
312
+ return { content: [{ type: "text", text: `โœ… Monitor \`${id}\` cancelled.` }] };
313
+ }
314
+ catch (err) {
315
+ return { content: [{ type: "text", text: `Cancel error: ${err.message}` }], isError: true };
316
+ }
317
+ }
318
+ return null;
319
+ }