@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.
Files changed (59) hide show
  1. package/README.md +39 -74
  2. package/dist/_http-cache.js +96 -0
  3. package/dist/_text-search.js +39 -0
  4. package/dist/agent-loop.js +301 -0
  5. package/dist/annotations.js +122 -0
  6. package/dist/cli.js +1391 -0
  7. package/dist/clink-input.js +15 -0
  8. package/dist/config.js +132 -0
  9. package/dist/convex.js +175 -0
  10. package/dist/dex-pair.js +54 -0
  11. package/dist/enrichment-router.js +315 -0
  12. package/dist/index.js +258 -0
  13. package/dist/llm.js +298 -0
  14. package/dist/local-memory-file.js +150 -0
  15. package/dist/local-memory.js +135 -0
  16. package/dist/local-vault.js +456 -0
  17. package/dist/output-schemas.js +605 -0
  18. package/dist/project.js +36 -0
  19. package/dist/prompts.js +111 -0
  20. package/dist/public-url.js +107 -0
  21. package/dist/resources.js +111 -0
  22. package/dist/server.js +322 -0
  23. package/dist/signal-gate.js +57 -0
  24. package/dist/token-decimals.js +26 -0
  25. package/dist/token-gate.js +88 -0
  26. package/dist/tool-filter.js +53 -0
  27. package/dist/tools/_solidity-scan.js +313 -0
  28. package/dist/tools/agents.js +441 -0
  29. package/dist/tools/automation.js +354 -0
  30. package/dist/tools/base-mcp.js +466 -0
  31. package/dist/tools/base.js +283 -0
  32. package/dist/tools/chronicle.js +268 -0
  33. package/dist/tools/coder.js +94 -0
  34. package/dist/tools/deep-research.js +1421 -0
  35. package/dist/tools/defi.js +292 -0
  36. package/dist/tools/equity.js +372 -0
  37. package/dist/tools/events.js +182 -0
  38. package/dist/tools/github.js +564 -0
  39. package/dist/tools/insider.js +264 -0
  40. package/dist/tools/insight.js +630 -0
  41. package/dist/tools/market.js +555 -0
  42. package/dist/tools/memory.js +1059 -0
  43. package/dist/tools/miroshark.js +350 -0
  44. package/dist/tools/monitor.js +319 -0
  45. package/dist/tools/os.js +236 -0
  46. package/dist/tools/packets.js +296 -0
  47. package/dist/tools/research-chain.js +226 -0
  48. package/dist/tools/research-compare.js +280 -0
  49. package/dist/tools/research.js +188 -0
  50. package/dist/tools/rh-bridge.js +148 -0
  51. package/dist/tools/rh-mcp.js +1448 -0
  52. package/dist/tools/rh-orders.js +556 -0
  53. package/dist/tools/scanner.js +564 -0
  54. package/dist/tools/stake.js +369 -0
  55. package/dist/tools/vault.js +1020 -0
  56. package/dist/tools/wallet.js +200 -0
  57. package/dist/types.js +2 -0
  58. package/dist/wallet.js +372 -0
  59. package/package.json +4 -7
@@ -0,0 +1,630 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.INSIGHT_TOOLS = void 0;
4
+ exports.handleInsightTool = handleInsightTool;
5
+ const zod_1 = require("zod");
6
+ const convex_js_1 = require("../convex.js");
7
+ const llm_js_1 = require("../llm.js");
8
+ const memory_js_1 = require("./memory.js");
9
+ const enrichment_router_js_1 = require("../enrichment-router.js");
10
+ const signal_gate_js_1 = require("../signal-gate.js");
11
+ exports.INSIGHT_TOOLS = [
12
+ {
13
+ name: "ask_finch",
14
+ description: "Ask Finch anything - analysis, opinions, explanations, strategy, or ideas. Finch loads your saved memory to personalize every answer. Use for: research questions, content ideas, code explanations, decision-making, DeFi analysis, trade ideas, or just thinking out loud. Pass previous messages to continue a conversation across tool calls. If YOU are already a reasoning model (Claude, GPT, etc. calling this via MCP) and other tools already gave you the data you need, just answer directly instead of calling this - it runs a separate LLM call and won't tell you anything you can't already work out yourself from that data.",
15
+ inputSchema: {
16
+ type: "object",
17
+ properties: {
18
+ question: { type: "string", description: "Your question or request for Finch" },
19
+ messages: {
20
+ type: "array",
21
+ description: "Previous conversation messages for context (optional)",
22
+ items: {
23
+ type: "object",
24
+ properties: { role: { type: "string", enum: ["user", "assistant"] }, content: { type: "string" } },
25
+ required: ["role", "content"],
26
+ },
27
+ },
28
+ },
29
+ required: ["question"],
30
+ },
31
+ },
32
+ {
33
+ name: "market_thesis",
34
+ description: "Fetch a cross-checked live price for any token (CoinGecko + DexScreener + Pyth, with a source-spread " +
35
+ "warning when they disagree) and return it with the bull/bear/verdict structure for YOU to write. " +
36
+ "Refuses outright when no source can verify the price. Use before opening a position or when " +
37
+ "researching a token you're watching. No API key needed.",
38
+ inputSchema: {
39
+ type: "object",
40
+ properties: {
41
+ token: { type: "string", description: "Token symbol or CoinGecko ID, e.g. 'ETH', 'bitcoin', 'AERO'" },
42
+ context: { type: "string", description: "Optional: extra context - your time horizon, thesis seed, or specific concerns" },
43
+ },
44
+ required: ["token"],
45
+ },
46
+ },
47
+ {
48
+ name: "trade_plan",
49
+ description: "Return the verified inputs for a trade plan on any token: cross-checked live price, a table of " +
50
+ "stop-loss and take-profit price levels computed off spot (signed for long or short), and position " +
51
+ "sizing math for the given risk tolerance and portfolio size. YOU pick the entry, stop and targets " +
52
+ "from those levels and write the plan. Refuses when no source can verify the price. No API key needed.",
53
+ inputSchema: {
54
+ type: "object",
55
+ properties: {
56
+ token: { type: "string", description: "Token symbol or CoinGecko ID" },
57
+ side: { type: "string", enum: ["long", "short"], description: "Trade direction (default: long)" },
58
+ portfolioSize: { type: "number", description: "Optional: your portfolio size in USD (for position sizing)" },
59
+ riskTolerance: { type: "string", enum: ["conservative", "moderate", "aggressive"], description: "Risk profile (default: moderate)" },
60
+ timeframe: { type: "string", description: "Optional: trade timeframe, e.g. 'intraday', 'swing', 'weeks'" },
61
+ },
62
+ required: ["token"],
63
+ },
64
+ },
65
+ ];
66
+ const AskFinchSchema = zod_1.z.object({
67
+ question: zod_1.z.string().min(1),
68
+ messages: zod_1.z.array(zod_1.z.object({ role: zod_1.z.enum(["user", "assistant"]), content: zod_1.z.string() })).optional(),
69
+ });
70
+ const MarketThesisSchema = zod_1.z.object({
71
+ token: zod_1.z.string().min(1),
72
+ context: zod_1.z.string().optional(),
73
+ });
74
+ const TradePlanSchema = zod_1.z.object({
75
+ token: zod_1.z.string().min(1),
76
+ side: zod_1.z.enum(["long", "short"]).optional(),
77
+ portfolioSize: zod_1.z.number().positive().optional(),
78
+ riskTolerance: zod_1.z.enum(["conservative", "moderate", "aggressive"]).optional(),
79
+ timeframe: zod_1.z.string().optional(),
80
+ });
81
+ const FINCH_BASE_PROMPT = `You are Finch, the core intelligence of the Finch runtime - the persistent state layer for AI assistants. You are direct, sharp, and thorough. You have access to memory that accumulates, vaults that version knowledge, agents that keep running between sessions, workflows that execute on schedule, plus execution domains: web research, market intelligence, code, and DeFi on Base. When asked anything, give your honest read backed by real reasoning. No filler, no disclaimers.
82
+
83
+ When the user's vault or memory contains relevant prior research, build on it explicitly rather than starting from scratch. Reference vault entries by title when you cite them.`;
84
+ async function searchVault(question, limit = 3) {
85
+ try {
86
+ const data = await (0, convex_js_1.callConvex)("/vault/search", "POST", { query: question, limit }, "vault_search");
87
+ const entries = data?.entries ?? data?.results ?? [];
88
+ return Array.isArray(entries) ? entries.slice(0, limit) : [];
89
+ }
90
+ catch {
91
+ return [];
92
+ }
93
+ }
94
+ async function fetchProfileContext() {
95
+ try {
96
+ const data = await (0, convex_js_1.callConvex)("/vault/profile-context?maxChars=3000", "GET", undefined, "vault_read");
97
+ return (data?.context ?? "").trim();
98
+ }
99
+ catch {
100
+ return "";
101
+ }
102
+ }
103
+ async function buildSystemPrompt(question) {
104
+ // Run profile fetch + memory + vault searches in parallel - each fails
105
+ // gracefully so a slow or down one doesn't block the others.
106
+ const [profileContext, memories, vaultHits] = await Promise.all([
107
+ fetchProfileContext(),
108
+ (0, memory_js_1.searchSupermemory)(question, 5),
109
+ searchVault(question, 3),
110
+ ]);
111
+ const blocks = [];
112
+ const meta = { hasProfile: false, memories: [], vault: [] };
113
+ if (profileContext) {
114
+ meta.hasProfile = true;
115
+ blocks.push(`<user_profile>\n${profileContext}\n</user_profile>`);
116
+ }
117
+ if (memories.length) {
118
+ const memBlock = memories
119
+ .map(r => {
120
+ const title = r.metadata?.title ? `[${r.metadata.title}] ` : "";
121
+ const snippet = r.content.slice(0, 250).replace(/\n/g, " ");
122
+ meta.memories.push({ title: r.metadata?.title, snippet: r.content.slice(0, 100).replace(/\n/g, " ") });
123
+ return `- ${title}${snippet}`;
124
+ })
125
+ .join("\n");
126
+ blocks.push(`<user_memory>\nStored knowledge about this user - use it to personalize your response:\n${memBlock}\n</user_memory>`);
127
+ }
128
+ if (vaultHits.length) {
129
+ const vaultBlock = vaultHits
130
+ .map(v => {
131
+ const typeTag = v.type ? `(${v.type}) ` : "";
132
+ const title = v.title ?? v.key ?? "untitled";
133
+ const preview = (v.preview ?? "").slice(0, 300).replace(/\n/g, " ");
134
+ meta.vault.push({ title, type: v.type, preview: (v.preview ?? "").slice(0, 80).replace(/\n/g, " ") });
135
+ return `- ${typeTag}**${title}**${preview ? ` - ${preview}` : ""}`;
136
+ })
137
+ .join("\n");
138
+ blocks.push(`<user_vault>\nThe user already has these prior artifacts on closely related topics - build on them, don't repeat them:\n${vaultBlock}\n</user_vault>`);
139
+ }
140
+ const prompt = blocks.length === 0
141
+ ? FINCH_BASE_PROMPT
142
+ : `${FINCH_BASE_PROMPT}\n\n${blocks.join("\n\n")}`;
143
+ return { prompt, meta };
144
+ }
145
+ function formatContextHeader(meta) {
146
+ const total = meta.memories.length + meta.vault.length + (meta.hasProfile ? 1 : 0);
147
+ if (total === 0)
148
+ return "";
149
+ const parts = [];
150
+ if (meta.hasProfile)
151
+ parts.push("profile");
152
+ if (meta.memories.length)
153
+ parts.push(`${meta.memories.length} memor${meta.memories.length === 1 ? "y" : "ies"}`);
154
+ if (meta.vault.length)
155
+ parts.push(`${meta.vault.length} vault entr${meta.vault.length === 1 ? "y" : "ies"}`);
156
+ const lines = [];
157
+ if (meta.memories.length > 0) {
158
+ lines.push("**Memory:**");
159
+ for (const m of meta.memories) {
160
+ const label = m.title ? `[${m.title}] ` : "";
161
+ lines.push(`- ${label}${m.snippet}${m.snippet.length >= 100 ? "…" : ""}`);
162
+ }
163
+ }
164
+ if (meta.vault.length > 0) {
165
+ if (lines.length)
166
+ lines.push("");
167
+ lines.push("**Vault:**");
168
+ for (const v of meta.vault) {
169
+ const typeTag = v.type ? `(${v.type}) ` : "";
170
+ lines.push(`- ${typeTag}**${v.title}**${v.preview ? ` — ${v.preview}${v.preview.length >= 80 ? "…" : ""}` : ""}`);
171
+ }
172
+ }
173
+ return [
174
+ `<details><summary>🧠 Context loaded: ${parts.join(" · ")}</summary>`,
175
+ ``,
176
+ ...lines,
177
+ ``,
178
+ `</details>`,
179
+ ``,
180
+ ].join("\n");
181
+ }
182
+ // CoinGecko's /markets endpoint filters by coin *id*, not symbol - `ids=eth`
183
+ // returns [] while `ids=ethereum` returns Ethereum. Callers pass symbols far
184
+ // more often than ids (the tool descriptions invite it), so a bare id lookup
185
+ // silently missed on nearly every real call and the caller fell through to a
186
+ // DexScreener symbol search, which happily matched a junk pair with the same
187
+ // ticker. /search resolves symbol → id; it is only hit when the direct lookup
188
+ // misses, so the common case stays at one request.
189
+ async function resolveCgId(token) {
190
+ try {
191
+ const res = await fetch(`https://api.coingecko.com/api/v3/search?query=${encodeURIComponent(token)}`, { signal: AbortSignal.timeout(8000) });
192
+ if (!res.ok)
193
+ return null;
194
+ const data = await res.json();
195
+ const coins = data.coins ?? [];
196
+ if (!coins.length)
197
+ return null;
198
+ // Prefer an exact symbol match, highest market-cap rank first - that's the
199
+ // real asset rather than one of the dozens of copycats sharing its ticker.
200
+ const sym = token.toLowerCase();
201
+ const ranked = (c) => c.market_cap_rank ?? Number.MAX_SAFE_INTEGER;
202
+ const exact = coins.filter((c) => c.symbol?.toLowerCase() === sym).sort((a, b) => ranked(a) - ranked(b));
203
+ return (exact[0] ?? coins[0])?.id ?? null;
204
+ }
205
+ catch {
206
+ return null;
207
+ }
208
+ }
209
+ async function fetchCgMarkets(id, token) {
210
+ const res = await fetch(`https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&ids=${encodeURIComponent(id)}&order=market_cap_desc&per_page=1&page=1`, { signal: AbortSignal.timeout(8000) });
211
+ if (!res.ok)
212
+ return null;
213
+ const data = await res.json();
214
+ const coin = data[0];
215
+ if (!coin)
216
+ return null;
217
+ return {
218
+ price: coin.current_price ?? 0,
219
+ change24h: coin.price_change_percentage_24h ?? 0,
220
+ mcap: coin.market_cap ?? 0,
221
+ symbol: coin.symbol?.toUpperCase() ?? token.toUpperCase(),
222
+ source: "coingecko",
223
+ };
224
+ }
225
+ async function fetchCgPrice(token) {
226
+ try {
227
+ const direct = await fetchCgMarkets(token.toLowerCase().replace(/ /g, "-"), token);
228
+ if (direct)
229
+ return direct;
230
+ const id = await resolveCgId(token);
231
+ return id ? await fetchCgMarkets(id, token) : null;
232
+ }
233
+ catch {
234
+ return null;
235
+ }
236
+ }
237
+ // DexScreener fallback when CoinGecko fails or doesn't have the token.
238
+ // Picks the highest-liquidity pair whose baseToken.symbol matches the query.
239
+ async function fetchDexscreenerPrice(token) {
240
+ try {
241
+ const res = await fetch(`https://api.dexscreener.com/latest/dex/search?q=${encodeURIComponent(token)}`, { signal: AbortSignal.timeout(8000) });
242
+ if (!res.ok)
243
+ return null;
244
+ const data = await res.json();
245
+ const pairs = Array.isArray(data?.pairs) ? data.pairs : [];
246
+ if (!pairs.length)
247
+ return null;
248
+ const targetSym = token.toLowerCase();
249
+ const matches = pairs.filter((p) => typeof p.baseToken?.symbol === "string" &&
250
+ p.baseToken.symbol.toLowerCase() === targetSym);
251
+ const pool = (matches.length ? matches : pairs)
252
+ .sort((a, b) => (b?.liquidity?.usd ?? 0) - (a?.liquidity?.usd ?? 0))[0];
253
+ if (!pool)
254
+ return null;
255
+ const price = parseFloat(pool.priceUsd ?? "0");
256
+ if (!isFinite(price) || price <= 0)
257
+ return null;
258
+ return {
259
+ price,
260
+ change24h: pool.priceChange?.h24 ?? 0,
261
+ mcap: pool.fdv ?? pool.marketCap ?? 0,
262
+ symbol: pool.baseToken?.symbol?.toUpperCase() ?? token.toUpperCase(),
263
+ source: "dexscreener",
264
+ };
265
+ }
266
+ catch {
267
+ return null;
268
+ }
269
+ }
270
+ // Pyth Network price feed - institutional-grade oracle used by 50+ chains.
271
+ // Free HTTPS endpoint, no auth. Adds a 3rd source so single-API rate limits
272
+ // or outages don't break thesis/trade-plan generation. Pyth IDs are stable
273
+ // (https://pyth.network/developers/price-feed-ids).
274
+ const PYTH_FEED_IDS = {
275
+ BTC: "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43",
276
+ ETH: "ff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace",
277
+ SOL: "ef0d8b6fda2ceba41da15d4095d1da392a0d2f8ed0c6c7bc0f4cfac8c280b56d",
278
+ USDC: "eaa020c61cc479712813461ce153894a96a6c00b21ed0cfc2798d1f9a9e9c94a",
279
+ USDT: "2b89b9dc8fdf9f34709a5b106b472f0f39bb6ca9ce04b0fd7f2e971688e2e53b",
280
+ MATIC: "5de33a9112c2b700b8d30b8a3402c103578ccfa2765696471cc672bd5cf6ac52",
281
+ AVAX: "93da3352f9f1d105fdfe4971cfa80e9dd777bfc5d0f683ebb6e1294b92137bb7",
282
+ LINK: "8ac0c70fff57e9aefdf5edf44b51d62c2d433653cbb2cf5cc06bb115af04d221",
283
+ BNB: "2f95862b045670cd22bee3114c39763a4a08beeb663b145d283c31d7d1101c4f",
284
+ };
285
+ async function fetchPythPrice(token) {
286
+ try {
287
+ const sym = token.toUpperCase();
288
+ const id = PYTH_FEED_IDS[sym];
289
+ if (!id)
290
+ return null; // not all tokens are on Pyth
291
+ const url = `https://hermes.pyth.network/api/latest_price_feeds?ids[]=${id}`;
292
+ const res = await fetch(url, { signal: AbortSignal.timeout(8000) });
293
+ if (!res.ok)
294
+ return null;
295
+ const data = await res.json();
296
+ const feed = data[0];
297
+ if (!feed?.price)
298
+ return null;
299
+ // Pyth returns price scaled by 10^expo (e.g. price=4500_00000000, expo=-8).
300
+ const raw = parseInt(feed.price.price, 10);
301
+ const expo = feed.price.expo;
302
+ const price = raw * Math.pow(10, expo);
303
+ if (!isFinite(price) || price <= 0)
304
+ return null;
305
+ return {
306
+ price,
307
+ change24h: 0, // Pyth doesn't expose 24h change directly
308
+ mcap: 0,
309
+ symbol: sym,
310
+ source: "pyth",
311
+ };
312
+ }
313
+ catch {
314
+ return null;
315
+ }
316
+ }
317
+ // Multi-source price quote: CoinGecko first, DexScreener fallback, Pyth third.
318
+ // Returns null only when ALL three sources fail. Also detects disagreement
319
+ // across sources > 5% - flagged for the caller via `inconsistent` flag,
320
+ // which downstream tools (market_thesis, trade_plan) surface to the user.
321
+ async function fetchVerifiedPrice(token) {
322
+ // Fetch in parallel so latency = slowest of the three, not sum.
323
+ const [cg, dex, pyth] = await Promise.all([
324
+ fetchCgPrice(token),
325
+ fetchDexscreenerPrice(token),
326
+ fetchPythPrice(token),
327
+ ]);
328
+ const candidates = [cg, dex, pyth].filter((p) => !!p && p.price > 0);
329
+ if (candidates.length === 0)
330
+ return null;
331
+ // Spread check across all valid sources
332
+ const prices = candidates.map((c) => c.price);
333
+ const min = Math.min(...prices);
334
+ const max = Math.max(...prices);
335
+ const spreadPct = min > 0 ? ((max - min) / min) * 100 : 0;
336
+ // Primary: CoinGecko (id-resolved, has mcap + 24h), else Pyth, else
337
+ // DexScreener. Pyth outranks DexScreener because it is keyed by a fixed feed
338
+ // ID - it cannot resolve to the wrong asset, whereas the DexScreener path is
339
+ // a ticker search and any copycat pair with enough liquidity can win it.
340
+ // Pyth carries no mcap or 24h change, so those are borrowed from DexScreener
341
+ // when it agrees closely enough on price to be describing the same asset.
342
+ let primary;
343
+ if (cg && cg.price > 0) {
344
+ primary = cg;
345
+ }
346
+ else if (pyth && pyth.price > 0) {
347
+ const dexAgrees = dex && dex.price > 0 && Math.abs(dex.price - pyth.price) / pyth.price <= 0.05;
348
+ primary = dexAgrees
349
+ ? { ...pyth, change24h: dex.change24h, mcap: dex.mcap }
350
+ : pyth;
351
+ }
352
+ else {
353
+ primary = dex;
354
+ }
355
+ if (candidates.length >= 2 && spreadPct > 5) {
356
+ // Carry the individual quotes, not just the spread number. "17.4% spread
357
+ // across coingecko/dexscreener/pyth" doesn't tell the caller which source
358
+ // is the outlier; the per-source prices do, and a ticker collision is
359
+ // obvious the moment you see them side by side.
360
+ return {
361
+ ...primary,
362
+ inconsistent: { sources: candidates.map((c) => c.source), spreadPct, quotes: candidates },
363
+ };
364
+ }
365
+ return primary;
366
+ }
367
+ // Market caps span $50k memecoins to $2T majors - a fixed unit renders one of
368
+ // those two ends unreadable ("$231778M", "$0M").
369
+ function fmtMcap(mcap) {
370
+ if (!mcap || mcap <= 0)
371
+ return "unknown";
372
+ if (mcap >= 1e12)
373
+ return `$${(mcap / 1e12).toFixed(2)}T`;
374
+ if (mcap >= 1e9)
375
+ return `$${(mcap / 1e9).toFixed(2)}B`;
376
+ if (mcap >= 1e6)
377
+ return `$${(mcap / 1e6).toFixed(1)}M`;
378
+ return `$${(mcap / 1e3).toFixed(0)}k`;
379
+ }
380
+ // Human-readable price across the same range: $1,919.92 vs $0.000004821.
381
+ function fmtPrice(n) {
382
+ return n >= 1
383
+ ? `$${n.toLocaleString(undefined, { maximumFractionDigits: 2 })}`
384
+ : `$${n.toPrecision(4)}`;
385
+ }
386
+ function spreadNote(priceData) {
387
+ if (!priceData.inconsistent)
388
+ return "";
389
+ const each = priceData.inconsistent.quotes.map((q) => `${q.source} ${fmtPrice(q.price)}`).join(" · ");
390
+ return ` | ⚠ sources disagree ${priceData.inconsistent.spreadPct.toFixed(1)}% — ${each}`;
391
+ }
392
+ async function handleInsightTool(name, args) {
393
+ if (name === "ask_finch") {
394
+ const parsed = AskFinchSchema.safeParse(args);
395
+ if (!parsed.success)
396
+ return { content: [{ type: "text", text: `Invalid input: question ${parsed.error.issues[0].message}` }], isError: true };
397
+ const { question, messages = [] } = parsed.data;
398
+ // Fetch enrichment + base prompt in parallel - enrichment hits free APIs
399
+ // (DefiLlama, CoinGecko, HN, GitHub, arXiv, Wikipedia) based on detected
400
+ // domain. Returns hasData=false if no domain matched or all APIs failed.
401
+ const [{ prompt: base, meta }, enrichment] = await Promise.all([
402
+ buildSystemPrompt(question),
403
+ (0, enrichment_router_js_1.enrichQuery)(question),
404
+ ]);
405
+ // Layered system prompt: persona → time anchor → live data (if any) →
406
+ // strict guardrail tail. Time anchor blocks "Q2 catalysts in July" type
407
+ // calendar hallucinations even when no enrichment ran.
408
+ let systemPrompt = `${base}\n\n${(0, enrichment_router_js_1.todayContext)()}`;
409
+ if (enrichment.hasData) {
410
+ systemPrompt += `\n\n${enrichment.context}`;
411
+ systemPrompt += `\n\nCRITICAL: For any number, price, TVL, APY, or current-state claim, you MUST cite from the AUTHORITATIVE LIVE DATA block above. NEVER state a number not present in that block as if it were current. If the block doesn't cover what's asked, say so plainly - do not improvise.`;
412
+ }
413
+ else {
414
+ // Price-shaped question with no live enrichment - force an honest answer
415
+ // instead of letting the model recall a training-time number.
416
+ const looksLikePriceQuestion = /\b(price|worth|trading at|cost|value|at \$|how much is|how many \$)\b/i.test(question)
417
+ && /\b(btc|eth|sol|usdc|usdt|dai|matic|avax|link|uni|aave|ldo|bitcoin|ethereum|solana|base)\b/i.test(question);
418
+ if (looksLikePriceQuestion) {
419
+ systemPrompt += `\n\nCRITICAL: User is asking about a current price but no live data was retrieved. Reply honestly: "I don't have a live price quote right now - check CoinGecko or DexScreener directly." DO NOT state any specific price number. DO NOT recall a price from training data.`;
420
+ }
421
+ }
422
+ if (!(0, llm_js_1.hasDirectLLMKey)()) {
423
+ return {
424
+ content: [{
425
+ type: "text",
426
+ text: "ask_finch needs its own LLM key to reason server-side - set one of " +
427
+ "BANKR_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY, or GROK_API_KEY as an " +
428
+ "environment variable, then retry. If YOU (the calling model) already have " +
429
+ "the context needed to answer this, just answer directly instead - that's " +
430
+ "usually faster and doesn't need a key at all.",
431
+ }],
432
+ isError: true,
433
+ };
434
+ }
435
+ const history = messages.map(m => ({ role: m.role, content: m.content }));
436
+ const answer = await (0, llm_js_1.callLLM)(systemPrompt, question, 1024, history);
437
+ // Signal gate - if the model returned thin/meta output, flag it so the
438
+ // user knows to retry with more specific framing or use deep_research.
439
+ const signal = (0, signal_gate_js_1.checkSignal)(answer);
440
+ let finalAnswer = answer;
441
+ if (!signal.ok && signal.reason) {
442
+ finalAnswer = `${answer}\n\n_⚠️ ${signal.reason}. Consider rephrasing or using \`deep_research\` for grounded sources._`;
443
+ }
444
+ // syncToSupermemory (not a direct callConvex call) so this respects
445
+ // local-memory mode too - otherwise every ask_finch exchange would keep
446
+ // uploading to Convex regardless of the user's configured backend.
447
+ (0, memory_js_1.syncToSupermemory)(`Q: ${question.slice(0, 200)}\nA: ${finalAnswer.slice(0, 400)}`, { source: "ask_finch", addedAt: Date.now() }).catch(() => { });
448
+ const contextHeader = formatContextHeader(meta);
449
+ return { content: [{ type: "text", text: contextHeader + finalAnswer }] };
450
+ }
451
+ if (name === "market_thesis") {
452
+ const parsed = MarketThesisSchema.safeParse(args);
453
+ if (!parsed.success)
454
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
455
+ const { token, context } = parsed.data;
456
+ const priceData = await fetchVerifiedPrice(token);
457
+ // Hard guard: without a verified live price, refuse to generate. Better
458
+ // to fail loud than to write a thesis around a hallucinated number that
459
+ // a user might trade on.
460
+ if (!priceData) {
461
+ return {
462
+ content: [{
463
+ type: "text",
464
+ text: [
465
+ `❌ **Cannot generate thesis for ${token.toUpperCase()}** - live price unavailable from CoinGecko or DexScreener.`,
466
+ ``,
467
+ `Reason: a bull/bear thesis grounded in a fabricated price is worse than no thesis. We don't write trade-shaped analysis without verified data.`,
468
+ ``,
469
+ `Try:`,
470
+ `- Check CoinGecko or DexScreener directly to confirm the token symbol`,
471
+ `- Retry in 30s (rate-limit) - both APIs are public/free`,
472
+ `- For a less time-sensitive read, use \`deep_research\` instead`,
473
+ ].join("\n"),
474
+ }],
475
+ isError: true,
476
+ };
477
+ }
478
+ const inconsistencyNote = spreadNote(priceData);
479
+ // The tool's real job is the verified price: three sources, cross-checked,
480
+ // with a hard refusal above when they're missing or disagree. Writing the
481
+ // thesis is model work — and the caller is a model that can also weigh the
482
+ // user's stated context. Hand over grounded data plus the structure.
483
+ const suggest = process.env.TRIGGER_SECRET_KEY
484
+ ? `\n\n---\n💡 Use \`schedule_research\` for scheduled briefings on ${token.toUpperCase()}.`
485
+ : "";
486
+ return {
487
+ content: [{
488
+ type: "text",
489
+ text: [
490
+ `# ${token.toUpperCase()} — verified market data`,
491
+ ``,
492
+ `**Price:** ${fmtPrice(priceData.price)}`,
493
+ `**24h:** ${priceData.change24h.toFixed(1)}%`,
494
+ `**Market cap:** ${fmtMcap(priceData.mcap)}`,
495
+ `**Source:** ${priceData.source}${inconsistencyNote}`,
496
+ context ? `` : "",
497
+ context ? `**User context:** ${context}` : "",
498
+ ``,
499
+ `---`,
500
+ ``,
501
+ `## Write the thesis from this`,
502
+ ``,
503
+ `### Bull Case`,
504
+ `3-5 specific catalysts, narratives or technical factors. Be concrete — no vague "adoption".`,
505
+ ``,
506
+ `### Bear Case`,
507
+ `3-5 specific risks, headwinds or red flags. Include on-chain, macro and competitive risks where they apply.`,
508
+ ``,
509
+ `### Net Verdict`,
510
+ `**Conviction: X/10** — the deciding factor, the key risk to watch, your net lean (2-3 sentences).`,
511
+ ``,
512
+ `Anchor every claim to the verified price above. Do not invent figures — if you need data this ` +
513
+ `tool did not supply, say so or fetch it with \`deep_research\`.`,
514
+ suggest,
515
+ ].filter(Boolean).join("\n"),
516
+ }],
517
+ };
518
+ }
519
+ if (name === "trade_plan") {
520
+ const parsed = TradePlanSchema.safeParse(args);
521
+ if (!parsed.success)
522
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
523
+ const { token, side = "long", portfolioSize, riskTolerance = "moderate", timeframe } = parsed.data;
524
+ const priceData = await fetchVerifiedPrice(token);
525
+ // Hard guard: trade plans without verified live price = entry/SL/TP
526
+ // numbers anchored to nothing. Refuse to generate rather than feed the
527
+ // LLM "use general knowledge" which it interprets as "make up a price".
528
+ if (!priceData) {
529
+ return {
530
+ content: [{
531
+ type: "text",
532
+ text: [
533
+ `❌ **Cannot build trade plan for ${token.toUpperCase()}** - live price unavailable from CoinGecko or DexScreener.`,
534
+ ``,
535
+ `Reason: entry, stop loss, and take profit levels anchored to a fabricated price are dangerous. We don't ship trade plans without verified data.`,
536
+ ``,
537
+ `Try:`,
538
+ `- Confirm the token symbol on CoinGecko or DexScreener`,
539
+ `- Retry in 30s if both APIs were briefly rate-limited`,
540
+ `- Use \`get_token_data\` to verify the price is live first`,
541
+ ].join("\n"),
542
+ }],
543
+ isError: true,
544
+ };
545
+ }
546
+ const inconsistencyNote = spreadNote(priceData);
547
+ // Everything below is arithmetic on the verified price - deterministic,
548
+ // keyless, and impossible for the caller to get wrong by eyeballing. The
549
+ // *judgement* (which level is the real invalidation, what the entry
550
+ // trigger is) is model work and stays with the caller.
551
+ const riskBands = {
552
+ conservative: { label: "1-2% of portfolio", min: 1, max: 2 },
553
+ moderate: { label: "2-5% of portfolio", min: 2, max: 5 },
554
+ aggressive: { label: "5-10% of portfolio", min: 5, max: 10 },
555
+ };
556
+ const band = riskBands[riskTolerance];
557
+ const fmt = fmtPrice;
558
+ // Offsets are signed by direction: a long's stop sits below spot, a
559
+ // short's above. Rendering them pre-signed stops the caller inverting
560
+ // the plan on a short.
561
+ const dir = side === "long" ? 1 : -1;
562
+ const adverse = [3, 5, 8, 10, 15];
563
+ const favourable = [5, 10, 20, 30, 50];
564
+ const levelRows = [
565
+ `| Move | Price | Use |`,
566
+ `|---|---|---|`,
567
+ ...adverse.map((p) => `| ${side === "long" ? "−" : "+"}${p}% | ${fmt(priceData.price * (1 - dir * p / 100))} | stop-loss candidate |`),
568
+ ...favourable.map((p) => `| ${side === "long" ? "+" : "−"}${p}% | ${fmt(priceData.price * (1 + dir * p / 100))} | take-profit candidate |`),
569
+ ].join("\n");
570
+ const sizingLines = portfolioSize
571
+ ? [
572
+ `**Portfolio:** $${portfolioSize.toLocaleString()}`,
573
+ `**Max position (${riskTolerance}):** $${(portfolioSize * band.min / 100).toLocaleString(undefined, { maximumFractionDigits: 0 })} – $${(portfolioSize * band.max / 100).toLocaleString(undefined, { maximumFractionDigits: 0 })} (${band.label})`,
574
+ `**Max loss at a 10% stop:** $${(portfolioSize * band.max / 100 * 0.1).toLocaleString(undefined, { maximumFractionDigits: 0 })} (${(band.max * 0.1).toFixed(1)}% of portfolio)`,
575
+ ]
576
+ : [`**Max position (${riskTolerance}):** ${band.label} — pass \`portfolioSize\` for USD figures.`];
577
+ const suggest = process.env.TRIGGER_SECRET_KEY
578
+ ? `\n\n---\n💡 Use \`schedule_research\` for scheduled briefings on ${token.toUpperCase()}.`
579
+ : "";
580
+ return {
581
+ content: [{
582
+ type: "text",
583
+ text: [
584
+ `# ${token.toUpperCase()} ${side.toUpperCase()} — verified levels`,
585
+ ``,
586
+ `**Spot:** ${fmt(priceData.price)}`,
587
+ `**24h:** ${priceData.change24h.toFixed(1)}%`,
588
+ `**Market cap:** ${fmtMcap(priceData.mcap)}`,
589
+ `**Source:** ${priceData.source}${inconsistencyNote}`,
590
+ timeframe ? `**Timeframe:** ${timeframe}` : "",
591
+ ``,
592
+ ...sizingLines,
593
+ ``,
594
+ `## Levels off spot`,
595
+ ``,
596
+ levelRows,
597
+ ``,
598
+ `---`,
599
+ ``,
600
+ `## Write the plan from this`,
601
+ ``,
602
+ `### Entry`,
603
+ `Ideal entry price or range, and the condition that must trigger it.`,
604
+ ``,
605
+ `### Risk Management`,
606
+ `Stop loss (pick from the adverse levels above, justify why *that* one is the invalidation), ` +
607
+ `position size, and the resulting max loss.`,
608
+ ``,
609
+ `### Targets`,
610
+ `TP1/TP2/TP3 with the partial-exit % at each.`,
611
+ ``,
612
+ `### Risk/Reward`,
613
+ `RR ratio computed from your chosen entry, stop and TP1. **If RR is below 1.5:1, say the trade is not worth taking** — do not dress up a bad ratio.`,
614
+ ``,
615
+ `### Thesis in one sentence`,
616
+ ``,
617
+ `### Invalidation`,
618
+ `Exactly what kills the thesis, stated so it is checkable without judgement.`,
619
+ ``,
620
+ `Every price you quote must come from the table above or be derived from spot. Do not invent ` +
621
+ `support/resistance levels this tool did not supply — if you want technicals, fetch them ` +
622
+ `with \`get_token_data\` or \`rh_analyze\` first.${priceData.inconsistent ? ` Sources disagree by ` +
623
+ `${(priceData.inconsistent.spreadPct).toFixed(1)}% — widen the stop or say the quote is unreliable.` : ""}`,
624
+ suggest,
625
+ ].filter(Boolean).join("\n"),
626
+ }],
627
+ };
628
+ }
629
+ return null;
630
+ }