@finchagentic/mcp 4.0.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +96 -249
- package/dist/agent-loop.js +139 -69
- package/dist/annotations.js +14 -13
- package/dist/cli.js +78 -21
- package/dist/config.js +16 -16
- package/dist/convex.js +15 -18
- package/dist/index.js +4 -5
- package/dist/llm.js +24 -49
- package/dist/local-memory-file.js +147 -0
- package/dist/local-memory.js +42 -9
- package/dist/output-schemas.js +71 -17
- package/dist/resources.js +8 -13
- package/dist/server.js +27 -13
- package/dist/token-gate.js +2 -2
- package/dist/tool-filter.js +13 -4
- package/dist/tools/agents.js +66 -394
- package/dist/tools/base-mcp.js +7 -19
- package/dist/tools/base.js +34 -20
- package/dist/tools/chronicle.js +4 -4
- package/dist/tools/deep-research.js +10 -5
- package/dist/tools/defi.js +36 -35
- package/dist/tools/equity.js +9 -1
- package/dist/tools/insight.js +25 -29
- package/dist/tools/memory.js +76 -108
- package/dist/tools/monitor.js +7 -7
- package/dist/tools/os.js +10 -6
- package/dist/tools/packets.js +5 -5
- package/dist/tools/research.js +2 -2
- package/dist/tools/rh-mcp.js +25 -2
- package/dist/tools/rh-orders.js +201 -123
- package/dist/tools/scanner.js +30 -0
- package/dist/tools/stake.js +329 -0
- package/dist/tools/vault.js +122 -42
- package/dist/wallet.js +212 -24
- package/package.json +9 -21
- package/dist/tools/framework.js +0 -150
package/dist/tools/base.js
CHANGED
|
@@ -4,10 +4,35 @@ exports.BASE_TOOLS = void 0;
|
|
|
4
4
|
exports.handleBaseTool = handleBaseTool;
|
|
5
5
|
const MORPHO_API = "https://blue-api.morpho.org/graphql";
|
|
6
6
|
const MOONWELL_API = "https://api.moonwell.fi/v1/markets";
|
|
7
|
+
/** Morpho's public API occasionally 403s transiently (rate-limit/WAF blip,
|
|
8
|
+
* confirmed by hand: identical requests succeed moments later) - one retry
|
|
9
|
+
* after a short delay clears most of these without adding real latency to
|
|
10
|
+
* the common case. */
|
|
11
|
+
async function fetchMorphoGql(query) {
|
|
12
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
13
|
+
if (attempt > 0)
|
|
14
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
15
|
+
const res = await fetch(MORPHO_API, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: { "Content-Type": "application/json" },
|
|
18
|
+
body: JSON.stringify({ query }),
|
|
19
|
+
signal: AbortSignal.timeout(15000),
|
|
20
|
+
});
|
|
21
|
+
if (res.ok)
|
|
22
|
+
return res.json();
|
|
23
|
+
if (attempt === 1)
|
|
24
|
+
throw new Error(`Morpho API error: ${res.status} (after retry)`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
7
27
|
exports.BASE_TOOLS = [
|
|
8
28
|
{
|
|
9
29
|
name: "base_mcp_yield_vaults",
|
|
10
|
-
description: "Find the best yield/earning opportunities on Base chain using Morpho vaults
|
|
30
|
+
description: "Find the best yield/earning opportunities on Base chain using Morpho vaults - queries Morpho's own " +
|
|
31
|
+
"API directly, so numbers are fresher than get_defi_yields' DeFiLlama-aggregated Morpho figures, " +
|
|
32
|
+
"but this tool only covers Morpho (not Aerodrome/Uniswap/other protocols - use get_defi_yields for " +
|
|
33
|
+
"cross-protocol comparison). Returns all vaults ranked by APY — use this when the user asks about " +
|
|
34
|
+
"yield farming, APY, best rates to earn, where to put USDC, or passive income on Base. For yield on " +
|
|
35
|
+
"a specific token, also see base_mcp_lend.",
|
|
11
36
|
inputSchema: {
|
|
12
37
|
type: "object",
|
|
13
38
|
properties: {
|
|
@@ -25,7 +50,12 @@ exports.BASE_TOOLS = [
|
|
|
25
50
|
},
|
|
26
51
|
{
|
|
27
52
|
name: "base_mcp_lending_rates",
|
|
28
|
-
description: "Get lending and borrowing rates across all Moonwell markets on Base
|
|
53
|
+
description: "Get lending and borrowing rates across all Moonwell markets on Base - queries Moonwell's own API " +
|
|
54
|
+
"directly, so numbers are fresher than get_defi_yields' DeFiLlama-aggregated Moonwell figures, but " +
|
|
55
|
+
"this tool only covers Moonwell (use get_defi_yields for cross-protocol comparison, or " +
|
|
56
|
+
"base_mcp_yield_vaults for Morpho specifically). Returns supply APY, borrow APY, liquidity, and " +
|
|
57
|
+
"utilization per asset. Use when the user asks about borrow rates, lending rates, interest rates, " +
|
|
58
|
+
"or supply APY on Base.",
|
|
29
59
|
inputSchema: {
|
|
30
60
|
type: "object",
|
|
31
61
|
properties: {
|
|
@@ -99,15 +129,7 @@ async function fetchMorphoVaults(asset, limit = 10) {
|
|
|
99
129
|
}
|
|
100
130
|
}
|
|
101
131
|
}`;
|
|
102
|
-
const
|
|
103
|
-
method: "POST",
|
|
104
|
-
headers: { "Content-Type": "application/json" },
|
|
105
|
-
body: JSON.stringify({ query: gql }),
|
|
106
|
-
signal: AbortSignal.timeout(15000),
|
|
107
|
-
});
|
|
108
|
-
if (!res.ok)
|
|
109
|
-
throw new Error(`Morpho API error: ${res.status}`);
|
|
110
|
-
const data = await res.json();
|
|
132
|
+
const data = await fetchMorphoGql(gql);
|
|
111
133
|
let vaults = data?.data?.vaults?.items ?? [];
|
|
112
134
|
// Filter out test/spam vaults: min $10k TVL, max 500% APY
|
|
113
135
|
vaults = vaults.filter((v) => {
|
|
@@ -199,15 +221,7 @@ async function prepareDeposit(vaultName, asset, amount) {
|
|
|
199
221
|
}
|
|
200
222
|
}
|
|
201
223
|
}`;
|
|
202
|
-
const
|
|
203
|
-
method: "POST",
|
|
204
|
-
headers: { "Content-Type": "application/json" },
|
|
205
|
-
body: JSON.stringify({ query: gql }),
|
|
206
|
-
signal: AbortSignal.timeout(15000),
|
|
207
|
-
});
|
|
208
|
-
if (!res.ok)
|
|
209
|
-
throw new Error(`Morpho API error: ${res.status}`);
|
|
210
|
-
const data = await res.json();
|
|
224
|
+
const data = await fetchMorphoGql(gql);
|
|
211
225
|
let vaults = data?.data?.vaults?.items ?? [];
|
|
212
226
|
// Filter by asset first
|
|
213
227
|
vaults = vaults.filter((v) => v.asset?.symbol?.toLowerCase() === asset.toLowerCase());
|
package/dist/tools/chronicle.js
CHANGED
|
@@ -10,7 +10,7 @@ const CHRONICLE_TYPES = ["vault", "memory", "agent", "tool", "automation", "moni
|
|
|
10
10
|
exports.CHRONICLE_TOOLS = [
|
|
11
11
|
{
|
|
12
12
|
name: "chronicle_add",
|
|
13
|
-
description: "Log an event to
|
|
13
|
+
description: "Log an event to Finch Chronicle - the system-wide audit log for your AI runtime. " +
|
|
14
14
|
"Records anything meaningful: vault saves, agent updates, automation triggers, " +
|
|
15
15
|
"custom milestones, research completions. Chronicle is your permanent timeline of what happened. " +
|
|
16
16
|
"Types: vault | memory | agent | tool | automation | monitor | system | custom.",
|
|
@@ -40,7 +40,7 @@ exports.CHRONICLE_TOOLS = [
|
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
name: "chronicle_list",
|
|
43
|
-
description: "Read the
|
|
43
|
+
description: "Read the Finch Chronicle event log - your AI runtime timeline. Returns recent events in reverse chronological order. " +
|
|
44
44
|
"Filter by type to see only vault saves, agent activity, automations, etc.",
|
|
45
45
|
inputSchema: {
|
|
46
46
|
type: "object",
|
|
@@ -59,7 +59,7 @@ exports.CHRONICLE_TOOLS = [
|
|
|
59
59
|
},
|
|
60
60
|
{
|
|
61
61
|
name: "chronicle_search",
|
|
62
|
-
description: "Search the
|
|
62
|
+
description: "Search the Finch Chronicle by keyword. Matches against event titles and details. " +
|
|
63
63
|
"Useful for finding when something specific happened: 'when did I last research ETH?' or 'find all vault saves for Base'.",
|
|
64
64
|
inputSchema: {
|
|
65
65
|
type: "object",
|
|
@@ -187,7 +187,7 @@ async function handleChronicle(name, args) {
|
|
|
187
187
|
};
|
|
188
188
|
}
|
|
189
189
|
const lines = [
|
|
190
|
-
`## 📜
|
|
190
|
+
`## 📜 Finch Chronicle${type ? ` · ${type}` : ""}`,
|
|
191
191
|
`*${entries.length} event${entries.length !== 1 ? "s" : ""}*`,
|
|
192
192
|
"",
|
|
193
193
|
];
|
|
@@ -267,7 +267,7 @@ async function fcSearch(query, limit) {
|
|
|
267
267
|
}
|
|
268
268
|
// Backend-proxy path - session-authed; Finch covers Firecrawl cost.
|
|
269
269
|
try {
|
|
270
|
-
const data = await (0, convex_js_1.callConvex)("/research/firecrawl-search", "POST", { query, limit }, "
|
|
270
|
+
const data = await (0, convex_js_1.callConvex)("/research/firecrawl-search", "POST", { query, limit }, "web_search", 20000);
|
|
271
271
|
return normalizeSearchHits(data?.results);
|
|
272
272
|
}
|
|
273
273
|
catch {
|
|
@@ -300,7 +300,7 @@ async function fcScrape(url) {
|
|
|
300
300
|
// which means continueFrom can't auto-date proxied scrapes. Acceptable
|
|
301
301
|
// tradeoff for now; markdown is the primary signal.
|
|
302
302
|
try {
|
|
303
|
-
const data = await (0, convex_js_1.callConvex)("/research/firecrawl-scrape", "POST", { url }, "
|
|
303
|
+
const data = await (0, convex_js_1.callConvex)("/research/firecrawl-scrape", "POST", { url }, "web_scrape", 25000);
|
|
304
304
|
if (data?.markdown)
|
|
305
305
|
return { markdown: data.markdown };
|
|
306
306
|
}
|
|
@@ -568,7 +568,7 @@ async function synthesize(query, sources, isFinal, liveSearch, priorContext, fre
|
|
|
568
568
|
let memoryContext = "";
|
|
569
569
|
if (isFinal) {
|
|
570
570
|
const [profileData, memHits] = await Promise.allSettled([
|
|
571
|
-
(0, convex_js_1.callConvex)("/vault/profile-context?maxChars=1200", "GET", undefined, "
|
|
571
|
+
(0, convex_js_1.callConvex)("/vault/profile-context?maxChars=1200", "GET", undefined, "vault_read"),
|
|
572
572
|
(0, memory_js_1.searchSupermemory)(query, 4),
|
|
573
573
|
]);
|
|
574
574
|
if (profileData.status === "fulfilled") {
|
|
@@ -744,7 +744,12 @@ Write the ${isFinal ? "final" : "draft"} report now. Markdown only - no preamble
|
|
|
744
744
|
// pick for reasoning, JSON, code). Override via env or pass the same
|
|
745
745
|
// model string to FINCH_MODEL to bypass.
|
|
746
746
|
const researchModel = process.env.FINCH_RESEARCH_MODEL ?? "grok-4.3";
|
|
747
|
-
|
|
747
|
+
// Deep mode (critic notes present, or the 5-angle planner ran) produces a
|
|
748
|
+
// longer report than the standard 6-section template - a fixed 4000-token
|
|
749
|
+
// budget was cutting deep reports off mid-sentence around risk #6-7 of 13+.
|
|
750
|
+
const isDeepMode = !!criticNotes || (angles?.length ?? 0) >= 5;
|
|
751
|
+
const finalTokens = isDeepMode ? 7000 : 4000;
|
|
752
|
+
const raw = await (0, llm_js_1.callLLM)(sys, user, isFinal ? finalTokens : 2000, [], 90000, { liveSearch, model: researchModel });
|
|
748
753
|
const { content: report, liveCitations } = extractLiveCitations(raw);
|
|
749
754
|
// Citation density check - only for final reports. If the report has many
|
|
750
755
|
// numerical claims but very few [N] citations, retry once with a stricter
|
|
@@ -757,7 +762,7 @@ Write the ${isFinal ? "final" : "draft"} report now. Markdown only - no preamble
|
|
|
757
762
|
|
|
758
763
|
⚠️ Your previous draft had ${density.numericalClaims} numerical claims but only ${density.citations} [N] citations. That ratio is too low. Rewrite with stricter citation density: every percentage, dollar amount, count, date, and named entity must carry [N]. Use the At a Glance table to anchor the key metrics.`;
|
|
759
764
|
try {
|
|
760
|
-
const rawRetry = await (0, llm_js_1.callLLM)(sys, retryUser,
|
|
765
|
+
const rawRetry = await (0, llm_js_1.callLLM)(sys, retryUser, finalTokens, [], 90000, { liveSearch, model: researchModel });
|
|
761
766
|
const { content: retryReport, liveCitations: retryCitations } = extractLiveCitations(rawRetry);
|
|
762
767
|
return { report: retryReport, liveCitations: retryCitations.length > 0 ? retryCitations : liveCitations };
|
|
763
768
|
}
|
package/dist/tools/defi.js
CHANGED
|
@@ -11,7 +11,11 @@ exports.DEFI_TOOLS = [
|
|
|
11
11
|
{
|
|
12
12
|
name: "get_defi_yields",
|
|
13
13
|
description: "Fetch top DeFi yield opportunities on Base - Morpho, Moonwell, Aerodrome, Uniswap, and more. " +
|
|
14
|
-
"Returns
|
|
14
|
+
"Returns APY, TVL, and pool info aggregated from DeFiLlama (no API key required) - broadest " +
|
|
15
|
+
"protocol coverage, but DeFiLlama's numbers can lag the protocol's own API by hours. Good first " +
|
|
16
|
+
"stop for comparing across protocols. For the freshest Morpho-vault-specific numbers use " +
|
|
17
|
+
"base_mcp_yield_vaults instead; for the freshest Moonwell supply/borrow rates use " +
|
|
18
|
+
"base_mcp_lending_rates instead - both query the protocol directly. " +
|
|
15
19
|
"Filter by token or minimum APY. Use before depositing to find the best rates.",
|
|
16
20
|
inputSchema: {
|
|
17
21
|
type: "object",
|
|
@@ -34,7 +38,6 @@ const SwapSchema = zod_1.z.object({
|
|
|
34
38
|
const DEFAULT_MAX_SLIPPAGE_PCT = 1.0;
|
|
35
39
|
const DEFAULT_MAX_PRICE_IMPACT_PCT = 3.0;
|
|
36
40
|
const SendSchema = zod_1.z.object({ token: zod_1.z.string().min(1), toAddress: zod_1.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "must be a valid 0x address"), amount: zod_1.z.string().min(1) });
|
|
37
|
-
const AnalyzeWalletSchema = zod_1.z.object({ address: zod_1.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "must be a valid 0x address"), label: zod_1.z.string().optional() });
|
|
38
41
|
const DefiYieldsSchema = zod_1.z.object({
|
|
39
42
|
token: zod_1.z.string().optional(),
|
|
40
43
|
minApy: zod_1.z.number().optional(),
|
|
@@ -103,7 +106,7 @@ async function handleDefiTool(name, args) {
|
|
|
103
106
|
// Pass the MCP local wallet as the swap taker so 0x routes output back
|
|
104
107
|
// to the wallet that's signing - not the backend's custodial wallet.
|
|
105
108
|
const localWallet = await (0, wallet_js_1.getOrCreateWallet)();
|
|
106
|
-
const result = await (0, convex_js_1.callConvex)("/mcp/defi/swap", "POST", { fromToken, toToken, amount, taker: localWallet.address, slippagePercentage: slippageLimit / 100 }, "
|
|
109
|
+
const result = await (0, convex_js_1.callConvex)("/mcp/defi/swap", "POST", { fromToken, toToken, amount, taker: localWallet.address, slippagePercentage: slippageLimit / 100 }, "swap_tokens");
|
|
107
110
|
if (!result.success)
|
|
108
111
|
return { content: [{ type: "text", text: `Estimate failed: ${result.error}` }], isError: true };
|
|
109
112
|
const q = result.quote;
|
|
@@ -163,17 +166,25 @@ async function handleDefiTool(name, args) {
|
|
|
163
166
|
}
|
|
164
167
|
const txHash = await (0, wallet_js_1.signAndBroadcast)(wallet, q);
|
|
165
168
|
const buyAmountHuman = formatTokenAmount(q.buyAmount, q.buyToken ?? toToken);
|
|
169
|
+
const receipt = await (0, wallet_js_1.waitForReceipt)(txHash);
|
|
170
|
+
const header = !receipt.mined
|
|
171
|
+
? `⏳ Broadcast, not yet confirmed within the wait window.`
|
|
172
|
+
: receipt.ok
|
|
173
|
+
? `✅ Swap confirmed on-chain (block ${receipt.blockNumber}).`
|
|
174
|
+
: `❌ Transaction REVERTED - no swap occurred (gas was still spent).`;
|
|
166
175
|
return {
|
|
167
176
|
content: [{
|
|
168
177
|
type: "text",
|
|
169
178
|
text: [
|
|
170
|
-
|
|
171
|
-
`${amount} ${fromToken.toUpperCase()} → ${buyAmountHuman} ${q.buyToken}`,
|
|
179
|
+
header,
|
|
180
|
+
`${amount} ${fromToken.toUpperCase()} → ${receipt.mined && receipt.ok ? buyAmountHuman : "(quoted, not received)"} ${q.buyToken}`,
|
|
172
181
|
`Slippage cap: ${slippageLimit}% · Price impact: ${impactPct.toFixed(3)}%`,
|
|
173
182
|
`Tx Hash: \`${txHash}\``,
|
|
174
183
|
`https://basescan.org/tx/${txHash}`,
|
|
175
|
-
|
|
184
|
+
!receipt.mined ? `Check the link above before telling the user this succeeded or failed.` : "",
|
|
185
|
+
].filter(Boolean).join("\n"),
|
|
176
186
|
}],
|
|
187
|
+
isError: receipt.mined && !receipt.ok,
|
|
177
188
|
};
|
|
178
189
|
}
|
|
179
190
|
case "send_token": {
|
|
@@ -186,41 +197,31 @@ async function handleDefiTool(name, args) {
|
|
|
186
197
|
if (!result.success)
|
|
187
198
|
return { content: [{ type: "text", text: `Send failed: ${result.error}` }], isError: true };
|
|
188
199
|
const txHash = await (0, wallet_js_1.signAndBroadcast)(wallet, result.txData);
|
|
200
|
+
const receipt = await (0, wallet_js_1.waitForReceipt)(txHash);
|
|
201
|
+
const header = !receipt.mined
|
|
202
|
+
? `⏳ Broadcast, not yet confirmed within the wait window.`
|
|
203
|
+
: receipt.ok
|
|
204
|
+
? `✅ Sent - confirmed on-chain (block ${receipt.blockNumber}).`
|
|
205
|
+
: `❌ Transaction REVERTED - funds were NOT sent (gas was still spent).`;
|
|
189
206
|
return {
|
|
190
207
|
content: [{
|
|
191
208
|
type: "text",
|
|
192
|
-
text: [
|
|
209
|
+
text: [
|
|
210
|
+
header,
|
|
211
|
+
`${amount} ${token.toUpperCase()} → \`${toAddress}\``,
|
|
212
|
+
`Tx Hash: \`${txHash}\``,
|
|
213
|
+
`https://basescan.org/tx/${txHash}`,
|
|
214
|
+
!receipt.mined ? `Check the link above before telling the user this succeeded or failed.` : "",
|
|
215
|
+
].filter(Boolean).join("\n"),
|
|
193
216
|
}],
|
|
217
|
+
isError: receipt.mined && !receipt.ok,
|
|
194
218
|
};
|
|
195
219
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const data = await (0, convex_js_1.callConvex)("/wallet/analyze", "POST", { address, label }, "analyze_wallet");
|
|
202
|
-
if (data.error)
|
|
203
|
-
return { content: [{ type: "text", text: `Wallet analysis failed: ${data.error}` }], isError: true };
|
|
204
|
-
const total = (data.totalUsd ?? 0).toFixed(2);
|
|
205
|
-
const walletLabel = label ? ` - ${label}` : "";
|
|
206
|
-
const topHoldings = (data.holdings ?? [])
|
|
207
|
-
.slice(0, 8)
|
|
208
|
-
.map(h => `• **${h.token}**: $${(h.valueUsd ?? 0).toFixed(2)}${h.pct != null ? ` (${h.pct}%)` : ""}`)
|
|
209
|
-
.join("\n");
|
|
210
|
-
const profileLine = data.profile ? `**Profile:** ${data.profile}\n` : "";
|
|
211
|
-
const header = [
|
|
212
|
-
`**Wallet Analysis**${walletLabel}`,
|
|
213
|
-
`\`${address}\``,
|
|
214
|
-
`**Portfolio value:** $${total}`,
|
|
215
|
-
``,
|
|
216
|
-
profileLine,
|
|
217
|
-
`**Holdings:**`,
|
|
218
|
-
topHoldings || "No token holdings found.",
|
|
219
|
-
``,
|
|
220
|
-
].join("\n");
|
|
221
|
-
const body = data.analysis ?? (data.analysisError ? `*AI analysis unavailable: ${data.analysisError}*` : "*AI analysis not available*");
|
|
222
|
-
return { content: [{ type: "text", text: header + body }] };
|
|
223
|
-
}
|
|
220
|
+
// analyze_wallet was removed - it called POST /wallet/analyze, which was
|
|
221
|
+
// never registered in app/convex/http.ts (only a dangling section-header
|
|
222
|
+
// comment exists there). The tool was never in the exported DEFI_TOOLS
|
|
223
|
+
// list either, so this case was already unreachable dead code - no Tool
|
|
224
|
+
// registers the name "analyze_wallet" for the MCP dispatcher to route to.
|
|
224
225
|
case "get_defi_yields": {
|
|
225
226
|
const parsed = DefiYieldsSchema.safeParse(args ?? {});
|
|
226
227
|
if (!parsed.success)
|
package/dist/tools/equity.js
CHANGED
|
@@ -11,8 +11,13 @@
|
|
|
11
11
|
// and is a stronger analyst than anything this module could embed.
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
13
|
exports.EQUITY_TOOLS = void 0;
|
|
14
|
+
exports.buildStockFundamentals = buildStockFundamentals;
|
|
14
15
|
exports.handleEquityTool = handleEquityTool;
|
|
15
16
|
const zod_1 = require("zod");
|
|
17
|
+
// ── Structured output builder (schema in output-schemas.ts) ─────────────────
|
|
18
|
+
function buildStockFundamentals(ticker, companyName, cik, quote, quarterly, annual) {
|
|
19
|
+
return { ticker, companyName, cik, quote, quarterly, annual };
|
|
20
|
+
}
|
|
16
21
|
const SEC_UA = "Finch MCP research (contact: support@finchagentic.com)";
|
|
17
22
|
const SEC_TICKERS = "https://www.sec.gov/files/company_tickers.json";
|
|
18
23
|
const SEC_CONCEPT = (cik, tag) => `https://data.sec.gov/api/xbrl/companyconcept/CIK${cik}/us-gaap/${tag}.json`;
|
|
@@ -360,5 +365,8 @@ async function handleEquityTool(name, args) {
|
|
|
360
365
|
// Issuers tag the same concept differently, so naming the tag behind each
|
|
361
366
|
// series makes the figures checkable against EDGAR rather than trusted.
|
|
362
367
|
`_XBRL concepts used: ${usedTags.join(" · ")}_`, ``, `_Figures are as filed with the SEC. Not investment advice._`);
|
|
363
|
-
return {
|
|
368
|
+
return {
|
|
369
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
370
|
+
structuredContent: buildStockFundamentals(ticker, company.name ?? null, company.cik ?? null, quote, quarterly, annual),
|
|
371
|
+
};
|
|
364
372
|
}
|
package/dist/tools/insight.js
CHANGED
|
@@ -11,11 +11,11 @@ const signal_gate_js_1 = require("../signal-gate.js");
|
|
|
11
11
|
exports.INSIGHT_TOOLS = [
|
|
12
12
|
{
|
|
13
13
|
name: "ask_finch",
|
|
14
|
-
description: "Ask
|
|
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
15
|
inputSchema: {
|
|
16
16
|
type: "object",
|
|
17
17
|
properties: {
|
|
18
|
-
question: { type: "string", description: "Your question or request for
|
|
18
|
+
question: { type: "string", description: "Your question or request for Finch" },
|
|
19
19
|
messages: {
|
|
20
20
|
type: "array",
|
|
21
21
|
description: "Previous conversation messages for context (optional)",
|
|
@@ -63,7 +63,7 @@ exports.INSIGHT_TOOLS = [
|
|
|
63
63
|
},
|
|
64
64
|
},
|
|
65
65
|
];
|
|
66
|
-
const
|
|
66
|
+
const AskFinchSchema = zod_1.z.object({
|
|
67
67
|
question: zod_1.z.string().min(1),
|
|
68
68
|
messages: zod_1.z.array(zod_1.z.object({ role: zod_1.z.enum(["user", "assistant"]), content: zod_1.z.string() })).optional(),
|
|
69
69
|
});
|
|
@@ -78,12 +78,12 @@ const TradePlanSchema = zod_1.z.object({
|
|
|
78
78
|
riskTolerance: zod_1.z.enum(["conservative", "moderate", "aggressive"]).optional(),
|
|
79
79
|
timeframe: zod_1.z.string().optional(),
|
|
80
80
|
});
|
|
81
|
-
const
|
|
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
82
|
|
|
83
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
84
|
async function searchVault(question, limit = 3) {
|
|
85
85
|
try {
|
|
86
|
-
const data = await (0, convex_js_1.callConvex)("/vault/search", "POST", { query: question, limit }, "
|
|
86
|
+
const data = await (0, convex_js_1.callConvex)("/vault/search", "POST", { query: question, limit }, "vault_search");
|
|
87
87
|
const entries = data?.entries ?? data?.results ?? [];
|
|
88
88
|
return Array.isArray(entries) ? entries.slice(0, limit) : [];
|
|
89
89
|
}
|
|
@@ -93,7 +93,7 @@ async function searchVault(question, limit = 3) {
|
|
|
93
93
|
}
|
|
94
94
|
async function fetchProfileContext() {
|
|
95
95
|
try {
|
|
96
|
-
const data = await (0, convex_js_1.callConvex)("/vault/profile-context?maxChars=3000", "GET", undefined, "
|
|
96
|
+
const data = await (0, convex_js_1.callConvex)("/vault/profile-context?maxChars=3000", "GET", undefined, "vault_read");
|
|
97
97
|
return (data?.context ?? "").trim();
|
|
98
98
|
}
|
|
99
99
|
catch {
|
|
@@ -138,8 +138,8 @@ async function buildSystemPrompt(question) {
|
|
|
138
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
139
|
}
|
|
140
140
|
const prompt = blocks.length === 0
|
|
141
|
-
?
|
|
142
|
-
: `${
|
|
141
|
+
? FINCH_BASE_PROMPT
|
|
142
|
+
: `${FINCH_BASE_PROMPT}\n\n${blocks.join("\n\n")}`;
|
|
143
143
|
return { prompt, meta };
|
|
144
144
|
}
|
|
145
145
|
function formatContextHeader(meta) {
|
|
@@ -391,7 +391,7 @@ function spreadNote(priceData) {
|
|
|
391
391
|
}
|
|
392
392
|
async function handleInsightTool(name, args) {
|
|
393
393
|
if (name === "ask_finch") {
|
|
394
|
-
const parsed =
|
|
394
|
+
const parsed = AskFinchSchema.safeParse(args);
|
|
395
395
|
if (!parsed.success)
|
|
396
396
|
return { content: [{ type: "text", text: `Invalid input: question ${parsed.error.issues[0].message}` }], isError: true };
|
|
397
397
|
const { question, messages = [] } = parsed.data;
|
|
@@ -419,25 +419,21 @@ async function handleInsightTool(name, args) {
|
|
|
419
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
420
|
}
|
|
421
421
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
question,
|
|
435
|
-
agentId: "noel-default",
|
|
436
|
-
messages,
|
|
437
|
-
systemPrompt,
|
|
438
|
-
}, "ask_finch");
|
|
439
|
-
answer = data.answer ?? JSON.stringify(data);
|
|
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
|
+
};
|
|
440
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);
|
|
441
437
|
// Signal gate - if the model returned thin/meta output, flag it so the
|
|
442
438
|
// user knows to retry with more specific framing or use deep_research.
|
|
443
439
|
const signal = (0, signal_gate_js_1.checkSignal)(answer);
|
|
@@ -485,7 +481,7 @@ async function handleInsightTool(name, args) {
|
|
|
485
481
|
// thesis is model work — and the caller is a model that can also weigh the
|
|
486
482
|
// user's stated context. Hand over grounded data plus the structure.
|
|
487
483
|
const suggest = process.env.TRIGGER_SECRET_KEY
|
|
488
|
-
? `\n\n---\n💡 Use \`
|
|
484
|
+
? `\n\n---\n💡 Use \`schedule_research\` for scheduled briefings on ${token.toUpperCase()}.`
|
|
489
485
|
: "";
|
|
490
486
|
return {
|
|
491
487
|
content: [{
|
|
@@ -579,7 +575,7 @@ async function handleInsightTool(name, args) {
|
|
|
579
575
|
]
|
|
580
576
|
: [`**Max position (${riskTolerance}):** ${band.label} — pass \`portfolioSize\` for USD figures.`];
|
|
581
577
|
const suggest = process.env.TRIGGER_SECRET_KEY
|
|
582
|
-
? `\n\n---\n💡 Use \`
|
|
578
|
+
? `\n\n---\n💡 Use \`schedule_research\` for scheduled briefings on ${token.toUpperCase()}.`
|
|
583
579
|
: "";
|
|
584
580
|
return {
|
|
585
581
|
content: [{
|