@finchagentic/mcp 4.6.1 โ†’ 4.6.2

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 (58) hide show
  1. package/package.json +5 -6
  2. package/dist/_http-cache.js +0 -96
  3. package/dist/_text-search.js +0 -39
  4. package/dist/agent-loop.js +0 -301
  5. package/dist/annotations.js +0 -122
  6. package/dist/cli.js +0 -1391
  7. package/dist/clink-input.js +0 -15
  8. package/dist/config.js +0 -132
  9. package/dist/convex.js +0 -175
  10. package/dist/dex-pair.js +0 -54
  11. package/dist/enrichment-router.js +0 -315
  12. package/dist/index.js +0 -258
  13. package/dist/llm.js +0 -298
  14. package/dist/local-memory-file.js +0 -150
  15. package/dist/local-memory.js +0 -135
  16. package/dist/local-vault.js +0 -456
  17. package/dist/output-schemas.js +0 -605
  18. package/dist/project.js +0 -36
  19. package/dist/prompts.js +0 -111
  20. package/dist/public-url.js +0 -107
  21. package/dist/resources.js +0 -111
  22. package/dist/server.js +0 -322
  23. package/dist/signal-gate.js +0 -57
  24. package/dist/token-decimals.js +0 -26
  25. package/dist/token-gate.js +0 -88
  26. package/dist/tool-filter.js +0 -53
  27. package/dist/tools/_solidity-scan.js +0 -313
  28. package/dist/tools/agents.js +0 -441
  29. package/dist/tools/automation.js +0 -354
  30. package/dist/tools/base-mcp.js +0 -466
  31. package/dist/tools/base.js +0 -283
  32. package/dist/tools/chronicle.js +0 -268
  33. package/dist/tools/coder.js +0 -94
  34. package/dist/tools/deep-research.js +0 -1421
  35. package/dist/tools/defi.js +0 -292
  36. package/dist/tools/equity.js +0 -372
  37. package/dist/tools/events.js +0 -182
  38. package/dist/tools/github.js +0 -564
  39. package/dist/tools/insider.js +0 -264
  40. package/dist/tools/insight.js +0 -630
  41. package/dist/tools/market.js +0 -555
  42. package/dist/tools/memory.js +0 -1059
  43. package/dist/tools/miroshark.js +0 -350
  44. package/dist/tools/monitor.js +0 -319
  45. package/dist/tools/os.js +0 -236
  46. package/dist/tools/packets.js +0 -296
  47. package/dist/tools/research-chain.js +0 -226
  48. package/dist/tools/research-compare.js +0 -280
  49. package/dist/tools/research.js +0 -188
  50. package/dist/tools/rh-bridge.js +0 -148
  51. package/dist/tools/rh-mcp.js +0 -1448
  52. package/dist/tools/rh-orders.js +0 -556
  53. package/dist/tools/scanner.js +0 -564
  54. package/dist/tools/stake.js +0 -369
  55. package/dist/tools/vault.js +0 -1020
  56. package/dist/tools/wallet.js +0 -200
  57. package/dist/types.js +0 -2
  58. package/dist/wallet.js +0 -372
@@ -1,350 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MIROSHARK_TOOLS = void 0;
4
- exports.handleMirosharkTool = handleMirosharkTool;
5
- const config_js_1 = require("../config.js");
6
- const CONVEX_SITE = process.env.FINCH_CONVEX_URL ?? "https://befitting-porcupine-276.convex.site";
7
- exports.MIROSHARK_TOOLS = [
8
- {
9
- name: "miroshark_simulate",
10
- 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. " +
11
- "AI agents act as traders, analysts, journalists, and social actors. Returns a simulation_id to track progress.",
12
- inputSchema: {
13
- type: "object",
14
- properties: {
15
- scenario: {
16
- type: "string",
17
- description: "Plain-English description of the scenario to simulate. E.g. 'What happens if ETH drops 20% and whale wallets start selling?'",
18
- },
19
- },
20
- required: ["scenario"],
21
- },
22
- },
23
- {
24
- name: "miroshark_status",
25
- 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.",
26
- inputSchema: {
27
- type: "object",
28
- properties: {
29
- simulation_id: {
30
- type: "string",
31
- description: "Simulation ID returned by miroshark_simulate",
32
- },
33
- },
34
- required: ["simulation_id"],
35
- },
36
- },
37
- {
38
- name: "miroshark_stop",
39
- description: "Stop a running MiroShark simulation.",
40
- inputSchema: {
41
- type: "object",
42
- properties: {
43
- simulation_id: {
44
- type: "string",
45
- description: "Simulation ID to stop",
46
- },
47
- },
48
- required: ["simulation_id"],
49
- },
50
- },
51
- ];
52
- // โ”€โ”€ HTTP helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
53
- function authHeaders() {
54
- // Bug fix: this used to read only process.env.FINCH_API_KEY/
55
- // FINCH_SESSION_TOKEN directly, bypassing getSavedToken()'s env-var-then-
56
- // saved-config fallback that every other tool goes through (via
57
- // callConvex/callConvexRaw in convex.ts). A user authenticated via
58
- // `finch login` (writes to ~/.finch/config.json, not an env var) got a
59
- // silent, unauthenticated request here while every other tool worked.
60
- const key = process.env.FINCH_API_KEY ?? (0, config_js_1.getSavedToken)();
61
- return key ? { Authorization: `Bearer ${key}` } : {};
62
- }
63
- async function miroJson(path, method, body, timeoutMs = 90000) {
64
- const res = await fetch(`${CONVEX_SITE}${path}`, {
65
- method,
66
- headers: { ...authHeaders(), "Content-Type": "application/json" },
67
- body: body !== undefined ? JSON.stringify(body) : undefined,
68
- signal: AbortSignal.timeout(timeoutMs),
69
- });
70
- const text = await res.text();
71
- if (!res.ok)
72
- throw new Error(`MiroShark ${method} ${path} [${res.status}]: ${text.slice(0, 300)}`);
73
- let json;
74
- try {
75
- json = JSON.parse(text);
76
- }
77
- catch {
78
- throw new Error(`MiroShark non-JSON response: ${text.slice(0, 200)}`);
79
- }
80
- if (json.success === false)
81
- throw new Error(`MiroShark error: ${json.error ?? JSON.stringify(json).slice(0, 300)}`);
82
- return json.data ?? json;
83
- }
84
- async function miroForm(path, form, timeoutMs = 120000) {
85
- // No Content-Type header - browser/fetch sets multipart boundary automatically
86
- const res = await fetch(`${CONVEX_SITE}${path}`, {
87
- method: "POST",
88
- headers: authHeaders(),
89
- body: form,
90
- signal: AbortSignal.timeout(timeoutMs),
91
- });
92
- const text = await res.text();
93
- if (!res.ok)
94
- throw new Error(`MiroShark POST ${path} [${res.status}]: ${text.slice(0, 300)}`);
95
- let json;
96
- try {
97
- json = JSON.parse(text);
98
- }
99
- catch {
100
- throw new Error(`MiroShark non-JSON response: ${text.slice(0, 200)}`);
101
- }
102
- if (json.success === false)
103
- throw new Error(`MiroShark error: ${json.error ?? JSON.stringify(json).slice(0, 300)}`);
104
- return json.data ?? json;
105
- }
106
- async function pollUntilDone(taskPath, pollIntervalMs = 8000, maxWaitMs = 180000) {
107
- const deadline = Date.now() + maxWaitMs;
108
- while (Date.now() < deadline) {
109
- await new Promise(r => setTimeout(r, pollIntervalMs));
110
- const task = await miroJson(taskPath, "GET");
111
- const s = (task.status ?? "").toLowerCase();
112
- if (s === "completed" || s === "success")
113
- return task;
114
- if (s === "failed" || s === "error") {
115
- throw new Error(`Task failed: ${task.error ?? task.message ?? s}`);
116
- }
117
- }
118
- throw new Error("Task timed out after 3 minutes");
119
- }
120
- // โ”€โ”€ Tool handler โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
121
- async function handleMirosharkTool(name, args) {
122
- const a = (args ?? {});
123
- // โ”€โ”€ miroshark_simulate โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
124
- if (name === "miroshark_simulate") {
125
- if (!a.scenario?.trim()) {
126
- return { content: [{ type: "text", text: "scenario is required" }], isError: true };
127
- }
128
- try {
129
- // Step 1: convert plain-English question into a structured seed document
130
- let asked;
131
- try {
132
- asked = await miroJson("/miroshark/api/simulation/ask", "POST", { question: a.scenario });
133
- }
134
- catch (err) {
135
- const msg = err.message ?? "";
136
- if (msg.includes("[404]") || msg.includes("[503]") || msg.includes("Upstream unreachable") || msg.includes("[502]")) {
137
- return {
138
- content: [{
139
- type: "text",
140
- 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`).",
141
- }],
142
- isError: true,
143
- };
144
- }
145
- throw err;
146
- }
147
- const { title, seed_document, simulation_requirement } = asked;
148
- // Step 2: generate knowledge-graph ontology from the seed document
149
- const form = new FormData();
150
- form.append("simulation_requirement", simulation_requirement ?? a.scenario);
151
- form.append("project_name", (title ?? a.scenario).slice(0, 100));
152
- form.append("url_docs", JSON.stringify([{
153
- title: title ?? "Simulation Context",
154
- url: "",
155
- text: seed_document ?? a.scenario,
156
- }]));
157
- const ontology = await miroForm("/miroshark/api/graph/ontology/generate", form);
158
- const projectId = ontology.project_id;
159
- if (!projectId)
160
- throw new Error("No project_id in ontology response");
161
- // Step 3: kick off the async graph build
162
- const built = await miroJson("/miroshark/api/graph/build", "POST", { project_id: projectId });
163
- const graphTaskId = built.task_id;
164
- if (!graphTaskId)
165
- throw new Error("No task_id in graph build response");
166
- // Step 4: wait for graph to finish (up to 3 min)
167
- await pollUntilDone(`/miroshark/api/graph/task/${graphTaskId}`);
168
- // Step 5: create simulation from the built graph
169
- const created = await miroJson("/miroshark/api/simulation/create", "POST", { project_id: projectId });
170
- const simId = created.simulation_id ?? created.id;
171
- if (!simId)
172
- throw new Error("No simulation_id in create response");
173
- // Step 6: kick off agent preparation (async - don't block)
174
- const prepared = await miroJson("/miroshark/api/simulation/prepare", "POST", { simulation_id: simId, parallel_profile_count: 10 });
175
- const prepTaskId = prepared.task_id;
176
- return {
177
- content: [{
178
- type: "text",
179
- text: [
180
- `**MiroShark simulation queued** โœ“`,
181
- ``,
182
- `Scenario: ${a.scenario}`,
183
- `Project: \`${projectId}\``,
184
- `Simulation ID: \`${simId}\``,
185
- `Status: preparing agents${prepTaskId ? ` (task: ${prepTaskId})` : ""}`,
186
- ``,
187
- `Agent preparation runs in the background. Poll progress with:`,
188
- `\`miroshark_status simulation_id="${simId}"\``,
189
- ].join("\n"),
190
- }],
191
- };
192
- }
193
- catch (err) {
194
- return { content: [{ type: "text", text: `MiroShark error: ${err.message}` }], isError: true };
195
- }
196
- }
197
- // โ”€โ”€ miroshark_status โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
198
- if (name === "miroshark_status") {
199
- if (!a.simulation_id?.trim()) {
200
- return { content: [{ type: "text", text: "simulation_id is required" }], isError: true };
201
- }
202
- const simId = a.simulation_id.trim();
203
- if (!/^[a-zA-Z0-9_-]{5,100}$/.test(simId)) {
204
- return { content: [{ type: "text", text: "Invalid simulation_id format." }], isError: true };
205
- }
206
- try {
207
- // Check run status first
208
- const runStatus = await miroJson(`/miroshark/api/simulation/${simId}/run-status`, "GET").catch(() => ({ runner_status: "idle" }));
209
- const runnerStatus = (runStatus?.runner_status ?? "idle").toLowerCase();
210
- // If not yet running, check whether agents are prepared by probing /config
211
- // (config only exists after /prepare completes)
212
- if (runnerStatus === "idle") {
213
- const config = await miroJson(`/miroshark/api/simulation/${simId}/config`, "GET").catch(() => null);
214
- if (!config) {
215
- // Preparation still in progress - check profiles for real-time progress
216
- const profiles = await miroJson(`/miroshark/api/simulation/${simId}/profiles/realtime`, "GET").catch(() => null);
217
- const total = profiles?.total_expected ?? "?";
218
- const ready = profiles?.profiles_ready ?? 0;
219
- return {
220
- content: [{
221
- type: "text",
222
- text: [
223
- `**MiroShark \`${simId}\`** - preparing agents`,
224
- total !== "?" ? `Profiles: ${ready} / ${total} ready` : `Profiles generating...`,
225
- ``,
226
- `Poll again in ~10 seconds.`,
227
- ].join("\n"),
228
- }],
229
- };
230
- }
231
- // Config exists โ†’ agents prepared โ†’ auto-start
232
- await miroJson("/miroshark/api/simulation/start", "POST", {
233
- simulation_id: simId,
234
- platform: "parallel",
235
- });
236
- return {
237
- content: [{
238
- type: "text",
239
- text: [
240
- `**MiroShark \`${simId}\`** - simulation started`,
241
- ``,
242
- `Agents are now active. Poll again in ~15 seconds for progress.`,
243
- `\`miroshark_status simulation_id="${simId}"\``,
244
- ].join("\n"),
245
- }],
246
- };
247
- }
248
- if (runnerStatus === "running") {
249
- const round = runStatus.current_round ?? 0;
250
- const total = runStatus.total_rounds ?? "?";
251
- const pct = runStatus.progress_percent?.toFixed(1) ?? "0";
252
- const twitterActs = runStatus.twitter_actions_count ?? 0;
253
- const redditActs = runStatus.reddit_actions_count ?? 0;
254
- return {
255
- content: [{
256
- type: "text",
257
- text: [
258
- `**MiroShark \`${simId}\`** - running`,
259
- `Round: ${round} / ${total} (${pct}%)`,
260
- `Actions: ${twitterActs} Twitter ยท ${redditActs} Reddit`,
261
- ``,
262
- `Simulation in progress - poll again in ~15 seconds.`,
263
- ].join("\n"),
264
- }],
265
- };
266
- }
267
- if (runnerStatus === "completed" || runnerStatus === "stopped") {
268
- const actionsData = await miroJson(`/miroshark/api/simulation/${simId}/actions?limit=100`, "GET").catch(() => ({ actions: [] }));
269
- const actions = actionsData?.actions ?? [];
270
- const totalActions = runStatus.total_actions_count ?? actions.length;
271
- const rounds = runStatus.current_round ?? "?";
272
- const ACTION_EMOJI = {
273
- tweet: "๐Ÿฆ", post: "๐Ÿ“", sell: "๐Ÿ“‰", buy: "๐Ÿ“ˆ",
274
- article: "๐Ÿ“ฐ", comment: "๐Ÿ’ฌ", alert: "๐Ÿšจ", analyze: "๐Ÿ”",
275
- };
276
- // The tool's job is polling the external simulation and returning what
277
- // the agents actually did. Reading sentiment off that log is model work
278
- // - and the caller can weigh it against the user's actual question,
279
- // which a fixed server-side summariser prompt never could. The feed is
280
- // returned in full (was: 20 of 50 shown, the rest silently dropped).
281
- const feed = actions.map((act) => {
282
- const who = act.agent_name ?? act.agent_id ?? "agent";
283
- const what = (act.action_type ?? act.type ?? "action").toLowerCase();
284
- const emoji = ACTION_EMOJI[what] ?? "โ€ข";
285
- const content = act.content ?? act.text ?? "";
286
- return `${emoji} **${who}** [${what}]${content ? `: ${String(content).slice(0, 200)}` : ""}`;
287
- });
288
- // Counts per action type - mechanical, and it tells the caller at a
289
- // glance whether the run was trade-heavy or chatter-heavy.
290
- const byType = new Map();
291
- for (const act of actions) {
292
- const what = String(act.action_type ?? act.type ?? "action").toLowerCase();
293
- byType.set(what, (byType.get(what) ?? 0) + 1);
294
- }
295
- const typeLine = [...byType.entries()]
296
- .sort((x, y) => y[1] - x[1])
297
- .map(([t, n]) => `${t} ร—${n}`)
298
- .join(" ยท ");
299
- const lines = [
300
- `**MiroShark \`${simId}\`** - ${runnerStatus}`,
301
- `Rounds: ${rounds} ยท Actions: ${totalActions}`,
302
- ...(typeLine ? [`Breakdown: ${typeLine}`] : []),
303
- "",
304
- ];
305
- if (feed.length > 0) {
306
- lines.push(`**Agent Feed** (${feed.length}${totalActions > feed.length ? ` of ${totalActions} โ€” API caps the page at 100` : ""}):`);
307
- lines.push(...feed);
308
- 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 " +
309
- "real position. Worth keeping? Save it with `vault_save` " +
310
- `(type: research, key: \`miroshark-${simId.slice(0, 8)}\`).`);
311
- }
312
- if (feed.length === 0)
313
- lines.push("_No agent actions recorded for this run._");
314
- return { content: [{ type: "text", text: lines.join("\n") }] };
315
- }
316
- // Fallback: unknown state
317
- return {
318
- content: [{
319
- type: "text",
320
- text: [
321
- `**MiroShark \`${simId}\`** - status: ${runnerStatus || "unknown"}`,
322
- ``,
323
- `If agents are still preparing, poll again shortly.`,
324
- ].filter(Boolean).join("\n"),
325
- }],
326
- };
327
- }
328
- catch (err) {
329
- return { content: [{ type: "text", text: `MiroShark error: ${err.message}` }], isError: true };
330
- }
331
- }
332
- // โ”€โ”€ miroshark_stop โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
333
- if (name === "miroshark_stop") {
334
- if (!a.simulation_id?.trim()) {
335
- return { content: [{ type: "text", text: "simulation_id is required" }], isError: true };
336
- }
337
- const simId = a.simulation_id.trim();
338
- if (!/^[a-zA-Z0-9_-]{5,100}$/.test(simId)) {
339
- return { content: [{ type: "text", text: "Invalid simulation_id format." }], isError: true };
340
- }
341
- try {
342
- await miroJson(`/miroshark/api/simulation/${simId}/stop`, "POST", {});
343
- return { content: [{ type: "text", text: `โน๏ธ Simulation \`${simId}\` stopped.` }] };
344
- }
345
- catch (err) {
346
- return { content: [{ type: "text", text: `MiroShark error: ${err.message}` }], isError: true };
347
- }
348
- }
349
- return null;
350
- }
@@ -1,319 +0,0 @@
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
- "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") {
107
- const parsed = CreateSchema.safeParse(args);
108
- if (!parsed.success)
109
- return { content: [{ type: "text", text: `${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"] }, "vault_list");
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: "schedule_research 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 and save findings to vault.`
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 \`schedule_research\` 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: `${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
- }