@finchagentic/mcp 4.6.2 โ 4.6.3
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.
- package/README.md +39 -74
- package/dist/_http-cache.js +96 -0
- package/dist/_text-search.js +39 -0
- package/dist/agent-loop.js +301 -0
- package/dist/annotations.js +122 -0
- package/dist/cli.js +1391 -0
- package/dist/clink-input.js +15 -0
- package/dist/config.js +132 -0
- package/dist/convex.js +175 -0
- package/dist/dex-pair.js +54 -0
- package/dist/enrichment-router.js +315 -0
- package/dist/index.js +258 -0
- package/dist/llm.js +298 -0
- package/dist/local-memory-file.js +150 -0
- package/dist/local-memory.js +135 -0
- package/dist/local-vault.js +456 -0
- package/dist/output-schemas.js +605 -0
- package/dist/project.js +36 -0
- package/dist/prompts.js +111 -0
- package/dist/public-url.js +107 -0
- package/dist/resources.js +111 -0
- package/dist/server.js +322 -0
- package/dist/signal-gate.js +57 -0
- package/dist/token-decimals.js +26 -0
- package/dist/token-gate.js +88 -0
- package/dist/tool-filter.js +53 -0
- package/dist/tools/_solidity-scan.js +313 -0
- package/dist/tools/agents.js +441 -0
- package/dist/tools/automation.js +354 -0
- package/dist/tools/base-mcp.js +466 -0
- package/dist/tools/base.js +283 -0
- package/dist/tools/chronicle.js +268 -0
- package/dist/tools/coder.js +94 -0
- package/dist/tools/deep-research.js +1421 -0
- package/dist/tools/defi.js +292 -0
- package/dist/tools/equity.js +372 -0
- package/dist/tools/events.js +182 -0
- package/dist/tools/github.js +564 -0
- package/dist/tools/insider.js +264 -0
- package/dist/tools/insight.js +630 -0
- package/dist/tools/market.js +555 -0
- package/dist/tools/memory.js +1059 -0
- package/dist/tools/miroshark.js +350 -0
- package/dist/tools/monitor.js +319 -0
- package/dist/tools/os.js +236 -0
- package/dist/tools/packets.js +296 -0
- package/dist/tools/research-chain.js +226 -0
- package/dist/tools/research-compare.js +280 -0
- package/dist/tools/research.js +188 -0
- package/dist/tools/rh-bridge.js +148 -0
- package/dist/tools/rh-mcp.js +1448 -0
- package/dist/tools/rh-orders.js +556 -0
- package/dist/tools/scanner.js +564 -0
- package/dist/tools/stake.js +369 -0
- package/dist/tools/vault.js +1020 -0
- package/dist/tools/wallet.js +200 -0
- package/dist/types.js +2 -0
- package/dist/wallet.js +372 -0
- package/package.json +4 -7
|
@@ -0,0 +1,350 @@
|
|
|
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
|
+
}
|
|
@@ -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
|
+
"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
|
+
}
|