@finchagentic/mcp 4.0.0 โ†’ 4.1.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.
@@ -331,7 +331,7 @@ async function handleBaseMcpTool(name, args) {
331
331
  case "base_mcp_status": {
332
332
  // Combine chain stats + wallet address into one status snapshot
333
333
  const [chainRes, walletRes] = await Promise.all([
334
- (0, base_js_1.handleBaseTool)("base_chain_stats", {}),
334
+ (0, base_js_1.handleBaseTool)("base_mcp_network", {}),
335
335
  (0, wallet_js_1.handleWalletTool)("get_wallet_address", {}).catch(() => null),
336
336
  ]);
337
337
  const chainText = chainRes?.content?.[0]?.text ?? "Chain stats unavailable";
@@ -415,9 +415,9 @@ async function handleBaseMcpTool(name, args) {
415
415
  // Two parallel lookups - Morpho vaults + Moonwell markets - unless venue specified
416
416
  const promises = [];
417
417
  if (venue !== "moonwell")
418
- promises.push((0, base_js_1.handleBaseTool)("base_query_vaults", { asset }));
418
+ promises.push((0, base_js_1.handleBaseTool)("base_mcp_yield_vaults", { asset }));
419
419
  if (venue !== "morpho")
420
- promises.push((0, base_js_1.handleBaseTool)("base_list_markets", { asset }));
420
+ promises.push((0, base_js_1.handleBaseTool)("base_mcp_lending_rates", { asset }));
421
421
  const results = await Promise.all(promises);
422
422
  const blocks = results
423
423
  .filter((r) => !!r && Array.isArray(r.content))
@@ -430,7 +430,7 @@ async function handleBaseMcpTool(name, args) {
430
430
  ];
431
431
  if (a.amount) {
432
432
  // If amount provided, also prepare a deposit tx for the top option
433
- const prep = await (0, base_js_1.handleBaseTool)("base_prepare_deposit", { asset, amount: a.amount });
433
+ const prep = await (0, base_js_1.handleBaseTool)("base_mcp_deposit_guide", { asset, amount: a.amount });
434
434
  if (prep?.content?.[0]?.text) {
435
435
  lines.push("", "### ๐Ÿ“ Prepared deposit", prep.content[0].text);
436
436
  }
@@ -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 Noel Chronicle - the system-wide audit log for your AI runtime. " +
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 Noel Chronicle event log - your AI runtime timeline. Returns recent events in reverse chronological order. " +
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 Noel Chronicle by keyword. Matches against event titles and details. " +
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
- `## ๐Ÿ“œ Noel Chronicle${type ? ` ยท ${type}` : ""}`,
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 }, "deep_research_search_proxy", 20000);
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 }, "deep_research_scrape_proxy", 25000);
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, "profile_context"),
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
- const raw = await (0, llm_js_1.callLLM)(sys, user, isFinal ? 4000 : 2000, [], 90000, { liveSearch, model: researchModel });
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, 4000, [], 90000, { liveSearch, model: researchModel });
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
  }
@@ -103,7 +103,7 @@ async function handleDefiTool(name, args) {
103
103
  // Pass the MCP local wallet as the swap taker so 0x routes output back
104
104
  // to the wallet that's signing - not the backend's custodial wallet.
105
105
  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 }, "estimate_swap");
106
+ const result = await (0, convex_js_1.callConvex)("/mcp/defi/swap", "POST", { fromToken, toToken, amount, taker: localWallet.address, slippagePercentage: slippageLimit / 100 }, "swap_tokens");
107
107
  if (!result.success)
108
108
  return { content: [{ type: "text", text: `Estimate failed: ${result.error}` }], isError: true };
109
109
  const q = result.quote;
@@ -163,17 +163,25 @@ async function handleDefiTool(name, args) {
163
163
  }
164
164
  const txHash = await (0, wallet_js_1.signAndBroadcast)(wallet, q);
165
165
  const buyAmountHuman = formatTokenAmount(q.buyAmount, q.buyToken ?? toToken);
166
+ const receipt = await (0, wallet_js_1.waitForReceipt)(txHash);
167
+ const header = !receipt.mined
168
+ ? `โณ Broadcast, not yet confirmed within the wait window.`
169
+ : receipt.ok
170
+ ? `โœ… Swap confirmed on-chain (block ${receipt.blockNumber}).`
171
+ : `โŒ Transaction REVERTED - no swap occurred (gas was still spent).`;
166
172
  return {
167
173
  content: [{
168
174
  type: "text",
169
175
  text: [
170
- `โœ… Swap executed!`,
171
- `${amount} ${fromToken.toUpperCase()} โ†’ ${buyAmountHuman} ${q.buyToken}`,
176
+ header,
177
+ `${amount} ${fromToken.toUpperCase()} โ†’ ${receipt.mined && receipt.ok ? buyAmountHuman : "(quoted, not received)"} ${q.buyToken}`,
172
178
  `Slippage cap: ${slippageLimit}% ยท Price impact: ${impactPct.toFixed(3)}%`,
173
179
  `Tx Hash: \`${txHash}\``,
174
180
  `https://basescan.org/tx/${txHash}`,
175
- ].join("\n"),
181
+ !receipt.mined ? `Check the link above before telling the user this succeeded or failed.` : "",
182
+ ].filter(Boolean).join("\n"),
176
183
  }],
184
+ isError: receipt.mined && !receipt.ok,
177
185
  };
178
186
  }
179
187
  case "send_token": {
@@ -186,11 +194,24 @@ async function handleDefiTool(name, args) {
186
194
  if (!result.success)
187
195
  return { content: [{ type: "text", text: `Send failed: ${result.error}` }], isError: true };
188
196
  const txHash = await (0, wallet_js_1.signAndBroadcast)(wallet, result.txData);
197
+ const receipt = await (0, wallet_js_1.waitForReceipt)(txHash);
198
+ const header = !receipt.mined
199
+ ? `โณ Broadcast, not yet confirmed within the wait window.`
200
+ : receipt.ok
201
+ ? `โœ… Sent - confirmed on-chain (block ${receipt.blockNumber}).`
202
+ : `โŒ Transaction REVERTED - funds were NOT sent (gas was still spent).`;
189
203
  return {
190
204
  content: [{
191
205
  type: "text",
192
- text: [`โœ… Sent!`, `${amount} ${token.toUpperCase()} โ†’ \`${toAddress}\``, `Tx Hash: \`${txHash}\``, `https://basescan.org/tx/${txHash}`].join("\n"),
206
+ text: [
207
+ header,
208
+ `${amount} ${token.toUpperCase()} โ†’ \`${toAddress}\``,
209
+ `Tx Hash: \`${txHash}\``,
210
+ `https://basescan.org/tx/${txHash}`,
211
+ !receipt.mined ? `Check the link above before telling the user this succeeded or failed.` : "",
212
+ ].filter(Boolean).join("\n"),
193
213
  }],
214
+ isError: receipt.mined && !receipt.ok,
194
215
  };
195
216
  }
196
217
  case "analyze_wallet": {
@@ -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 Noel anything - analysis, opinions, explanations, strategy, or ideas. Noel 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.",
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 Noel" },
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 AskNoelSchema = zod_1.z.object({
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 NOEL_BASE_PROMPT = `You are Noel, 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.
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 }, "ask_finch_vault");
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, "profile_context");
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
- ? NOEL_BASE_PROMPT
142
- : `${NOEL_BASE_PROMPT}\n\n${blocks.join("\n\n")}`;
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 = AskNoelSchema.safeParse(args);
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
- let answer = null;
423
- if (process.env.BANKR_API_KEY || process.env.ANTHROPIC_API_KEY) {
424
- try {
425
- const history = messages.map(m => ({ role: m.role, content: m.content }));
426
- answer = await (0, llm_js_1.callLLM)(systemPrompt, question, 1024, history);
427
- }
428
- catch {
429
- // fall through to Convex backend
430
- }
431
- }
432
- if (!answer) {
433
- const data = await (0, convex_js_1.callConvex)("/mcp/chat", "POST", {
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);
@@ -101,7 +101,7 @@ async function findDuplicateMemory(hash) {
101
101
  const local = (0, local_memory_js_1.getLocalMemoryConfig)();
102
102
  const results = local
103
103
  ? await (0, local_memory_js_1.localMemoryList)(local, 50)
104
- : ((await (0, convex_js_1.callConvex)("/memory/list", "POST", { n: 50 }))?.results ?? []);
104
+ : ((await (0, convex_js_1.callConvex)("/memory/list", "POST", { n: 50 }, "memory_list"))?.results ?? []);
105
105
  const match = results.find((r) => r.metadata?.contentHash === hash);
106
106
  if (!match)
107
107
  return null;
@@ -154,7 +154,7 @@ async function syncToSupermemory(content, metadata, sourceUrl) {
154
154
  };
155
155
  for (let attempt = 0; attempt <= SYNC_RETRY_DELAYS_MS.length; attempt++) {
156
156
  try {
157
- await (0, convex_js_1.callConvex)("/memory/add", "POST", payload);
157
+ await (0, convex_js_1.callConvex)("/memory/add", "POST", payload, "memory_add");
158
158
  return;
159
159
  }
160
160
  catch (err) {
@@ -183,7 +183,7 @@ async function searchSupermemory(query, limit = 10) {
183
183
  const local = (0, local_memory_js_1.getLocalMemoryConfig)();
184
184
  if (local)
185
185
  return await (0, local_memory_js_1.localMemorySearch)(local, query, limit);
186
- const data = await (0, convex_js_1.callConvex)("/memory/search", "POST", { q: query, n: limit });
186
+ const data = await (0, convex_js_1.callConvex)("/memory/search", "POST", { q: query, n: limit }, "memory_search");
187
187
  return data?.results ?? [];
188
188
  }
189
189
  catch {
@@ -195,7 +195,7 @@ async function searchSupermemory(query, limit = 10) {
195
195
  // directly without re-normalizing raw scores.
196
196
  async function lexicalSearch(query, limit = 30) {
197
197
  try {
198
- const data = await (0, convex_js_1.callConvex)("/memory/lexical", "POST", { q: query, n: limit });
198
+ const data = await (0, convex_js_1.callConvex)("/memory/lexical", "POST", { q: query, n: limit }, "memory_search");
199
199
  const rows = (data?.results ?? []);
200
200
  return rows.map((r, idx) => ({
201
201
  id: r.id,
@@ -284,12 +284,13 @@ async function hybridMemorySearch(query, limit = 30) {
284
284
  exports.MEMORY_TOOLS = [
285
285
  {
286
286
  name: "memory_add",
287
- description: "Add content to your Finch semantic memory - no setup needed, no extra API keys. " +
287
+ description: "Add content to your Finch memory - no setup needed, no extra API keys. " +
288
288
  "Unlike vault_save, memory_add is instant: no versioning, no type required. " +
289
- "Use for notes, decisions, preferences, or anything you want to find later with natural language. " +
289
+ "Use for notes, decisions, preferences, or anything you want to find later. " +
290
290
  "Pass sourceUrl to fetch and index any web page, GitHub repo, or Notion page automatically - " +
291
- "searchable in ~30s. Memory is indexed semantically - 'what did I say about ETH yield?' " +
292
- "will find it even without exact keywords. " +
291
+ "searchable in ~30s. Retrieval is full-text (keyword) search, not embeddings - " +
292
+ "'what did I say about ETH yield?' finds notes containing those words or close variants, " +
293
+ "not unrelated phrasing with the same meaning. " +
293
294
  "Auto-deduplicates: identical content in your recent 50 memories is skipped (override with force:true).",
294
295
  inputSchema: {
295
296
  type: "object",
@@ -305,10 +306,11 @@ exports.MEMORY_TOOLS = [
305
306
  },
306
307
  {
307
308
  name: "memory_search",
308
- description: "Hybrid memory search - fuses semantic (embedding) + lexical (full-text BM25) retrieval via Reciprocal Rank Fusion. " +
309
- "Catches both meaning matches ('low risk crypto yield' โ†’ 'conservative DeFi strategies') and exact-token lookups (env var names, contract addresses, IDs) that pure semantic search misses. " +
310
- "90-day time-decay weighting on top so recent precise notes outrank stale ones. " +
311
- "Falls back to semantic-only for memories added before v3.24 (no lexical mirror).",
309
+ description: "Full-text (keyword) search over your stored memories, with 90-day time-decay weighting so " +
310
+ "recent notes outrank stale ones with similar wording. Good for exact-token lookups (env var " +
311
+ "names, contract addresses, IDs, specific phrases) - it does not understand meaning, so " +
312
+ "'low risk crypto yield' will not match a note phrased as 'conservative DeFi strategies' " +
313
+ "unless the words themselves overlap.",
312
314
  inputSchema: {
313
315
  type: "object",
314
316
  properties: {
@@ -320,9 +322,10 @@ exports.MEMORY_TOOLS = [
320
322
  },
321
323
  {
322
324
  name: "memory_context",
323
- description: "Retrieve the most semantically relevant memories for a topic, formatted as AI-ready context. " +
325
+ description: "Retrieve the most relevant memories for a topic, formatted as AI-ready context. " +
324
326
  "Use at the start of research tasks to prime with everything stored about a topic. " +
325
- "Uses vector search - finds semantically related content, not just exact keyword matches.",
327
+ "Uses full-text (keyword) search, not embeddings - phrase your topic with the words " +
328
+ "you expect were actually used when the memory was saved.",
326
329
  inputSchema: {
327
330
  type: "object",
328
331
  properties: {
@@ -334,7 +337,7 @@ exports.MEMORY_TOOLS = [
334
337
  },
335
338
  {
336
339
  name: "memory_profile",
337
- description: "Show your semantic memory stats - total memories stored, your memory space, and connected sources. " +
340
+ description: "Show your memory stats - total memories stored, your memory space, and connected sources. " +
338
341
  "Useful for auditing what Finch knows about you.",
339
342
  inputSchema: {
340
343
  type: "object",
@@ -373,7 +376,7 @@ exports.MEMORY_TOOLS = [
373
376
  },
374
377
  {
375
378
  name: "memory_insight",
376
- description: "Get a full intelligence report on any topic - combines semantic memory AND vault entries, " +
379
+ description: "Get a full intelligence report on any topic - combines memory AND vault entries, " +
377
380
  "then identifies knowledge gaps and suggests next actions. " +
378
381
  "Use this before starting any research or trade decision to see everything Finch already knows. " +
379
382
  "Returns: confidence level, what you know, coverage timeline, gaps, and recommended next steps.",
@@ -388,7 +391,7 @@ exports.MEMORY_TOOLS = [
388
391
  },
389
392
  {
390
393
  name: "memory_extract",
391
- description: "Save discrete facts, preferences and decisions to semantic memory as individually searchable atoms " +
394
+ description: "Save discrete facts, preferences and decisions to memory as individually searchable atoms " +
392
395
  "instead of one wall of text. Two-pass, no API key needed. " +
393
396
  "PASS 1 โ€” call with `text`: returns the text with the extraction rubric. " +
394
397
  "PASS 2 โ€” call with `facts: [...]`: stores each fact separately, deduped. " +
@@ -560,7 +563,7 @@ async function handleMemoryTool(name, args) {
560
563
  const localAdd = (0, local_memory_js_1.getLocalMemoryConfig)();
561
564
  const data = localAdd
562
565
  ? await (0, local_memory_js_1.localMemoryAdd)(localAdd, content, addMetadata, sourceUrl).catch((err) => ({ error: err.message }))
563
- : await (0, convex_js_1.callConvex)("/memory/add", "POST", { content, metadata: addMetadata, ...(sourceUrl ? { sourceUrl } : {}) }).catch((err) => ({ error: err.message }));
566
+ : await (0, convex_js_1.callConvex)("/memory/add", "POST", { content, metadata: addMetadata, ...(sourceUrl ? { sourceUrl } : {}) }, "memory_add").catch((err) => ({ error: err.message }));
564
567
  if (data?.error)
565
568
  return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
566
569
  // Cache the hash so an immediate second call with identical content
@@ -588,11 +591,12 @@ async function handleMemoryTool(name, args) {
588
591
  const { query, limit = 10 } = parsed.data;
589
592
  // Over-fetch so post-decay ranking still has enough material.
590
593
  const overfetch = Math.min(50, Math.max(limit * 2, 20));
591
- // โ”€โ”€โ”€ Hybrid retrieval โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
592
- // Fan out semantic (Supermemory embedding) + lexical (Convex full-text)
593
- // in parallel, fuse via Reciprocal Rank Fusion. Falls back gracefully:
594
- // if lexical is empty (e.g. user has only pre-v3.24 memories) the
595
- // fused list equals semantic.
594
+ // โ”€โ”€โ”€ Retrieval โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
595
+ // Fan out two full-text queries (searchSupermemory and lexicalSearch -
596
+ // both call Convex full-text search under different names, there is no
597
+ // embedding step despite the "semantic"/"Supermemory" naming) in
598
+ // parallel, fuse via Reciprocal Rank Fusion so results present in both
599
+ // rank highest.
596
600
  const fused = await hybridMemorySearch(query, overfetch);
597
601
  const raw = fused;
598
602
  if (!raw.length)
@@ -697,7 +701,7 @@ async function handleMemoryTool(name, args) {
697
701
  const localProfileCfg = (0, local_memory_js_1.getLocalMemoryConfig)();
698
702
  const data = localProfileCfg
699
703
  ? await (0, local_memory_js_1.localMemoryProfile)(localProfileCfg).catch(() => null)
700
- : await (0, convex_js_1.callConvex)("/memory/profile", "GET").catch(() => null);
704
+ : await (0, convex_js_1.callConvex)("/memory/profile", "GET", undefined, "memory_profile").catch(() => null);
701
705
  const total = data?.total ?? 0;
702
706
  const status = data?.status ?? "unknown";
703
707
  const space = data?.space ?? "-";
@@ -738,7 +742,7 @@ async function handleMemoryTool(name, args) {
738
742
  }
739
743
  }
740
744
  else {
741
- const data = await (0, convex_js_1.callConvex)("/memory/list", "POST", { n: limit, tag }).catch((err) => ({ error: err.message }));
745
+ const data = await (0, convex_js_1.callConvex)("/memory/list", "POST", { n: limit, tag }, "memory_list").catch((err) => ({ error: err.message }));
742
746
  if (data?.error)
743
747
  return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
744
748
  results = data?.results ?? [];
@@ -777,7 +781,7 @@ async function handleMemoryTool(name, args) {
777
781
  }
778
782
  }
779
783
  else {
780
- const data = await (0, convex_js_1.callConvex)("/memory/delete", "POST", { id: parsed.data.id }).catch((err) => ({ error: err.message }));
784
+ const data = await (0, convex_js_1.callConvex)("/memory/delete", "POST", { id: parsed.data.id }, "memory_delete").catch((err) => ({ error: err.message }));
781
785
  if (data?.error)
782
786
  return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
783
787
  }
@@ -794,7 +798,7 @@ async function handleMemoryTool(name, args) {
794
798
  // matches. Vault side runs in parallel as before.
795
799
  const [memResults, vaultData] = await Promise.all([
796
800
  hybridMemorySearch(topic, memLimit),
797
- (0, convex_js_1.callConvex)(`/vault/search?q=${encodeURIComponent(topic)}&limit=6`, "GET", undefined, "memory_insight").catch(() => ({ results: [] })),
801
+ (0, convex_js_1.callConvex)(`/vault/search?q=${encodeURIComponent(topic)}&limit=6`, "GET", undefined, "vault_search").catch(() => ({ results: [] })),
798
802
  ]);
799
803
  const vaultResults = vaultData.results ?? [];
800
804
  const total = memResults.length + vaultResults.length;
@@ -905,7 +909,7 @@ async function handleMemoryTool(name, args) {
905
909
  const metaFor = () => ({ source, addedAt: Date.now() });
906
910
  const results = await Promise.allSettled(facts.map((fact) => localExtractCfg
907
911
  ? (0, local_memory_js_1.localMemoryAdd)(localExtractCfg, fact, metaFor())
908
- : (0, convex_js_1.callConvex)("/memory/add", "POST", { content: fact, metadata: metaFor() })));
912
+ : (0, convex_js_1.callConvex)("/memory/add", "POST", { content: fact, metadata: metaFor() }, "memory_add")));
909
913
  const saved = results.filter((r) => r.status === "fulfilled").length;
910
914
  const failed = results.length - saved;
911
915
  return {
@@ -934,7 +938,7 @@ async function handleMemoryTool(name, args) {
934
938
  const meta = { title: `Consolidated: ${topic}`, source: "memory_consolidate", addedAt: Date.now() };
935
939
  const saved = localConsolidateCfg
936
940
  ? await (0, local_memory_js_1.localMemoryAdd)(localConsolidateCfg, summary, meta).catch((err) => ({ error: err.message }))
937
- : await (0, convex_js_1.callConvex)("/memory/add", "POST", { content: summary, metadata: meta }).catch((err) => ({ error: err.message }));
941
+ : await (0, convex_js_1.callConvex)("/memory/add", "POST", { content: summary, metadata: meta }, "memory_add").catch((err) => ({ error: err.message }));
938
942
  if (saved?.error) {
939
943
  return { content: [{ type: "text", text: `Error saving consolidated memory: ${saved.error}` }], isError: true };
940
944
  }
@@ -117,7 +117,7 @@ async function handleMonitorTool(name, args) {
117
117
  // created 4ร— by an over-eager agent loop, which is what produced the
118
118
  // duplicate Telegram briefings users complained about.
119
119
  try {
120
- const existingMonitors = await (0, convex_js_1.callConvex)("/vault/list", "POST", { type: "workflow", tags: ["monitor-config"] }, "list_monitors_for_dedup");
120
+ const existingMonitors = await (0, convex_js_1.callConvex)("/vault/list", "POST", { type: "workflow", tags: ["monitor-config"] }, "vault_list");
121
121
  const duplicates = (existingMonitors?.entries ?? []).filter((e) => {
122
122
  try {
123
123
  const cfg = JSON.parse(e.content ?? "{}");
package/dist/tools/os.js CHANGED
@@ -45,14 +45,14 @@ async function handleOsTool(name, args) {
45
45
  const localStatusCfg = (0, local_memory_js_1.getLocalMemoryConfig)();
46
46
  const memoryProfileCall = localStatusCfg
47
47
  ? (0, local_memory_js_1.localMemoryProfile)(localStatusCfg)
48
- : (0, convex_js_1.callConvex)("/memory/profile", "GET");
48
+ : (0, convex_js_1.callConvex)("/memory/profile", "GET", undefined, "memory_profile");
49
49
  const [tierResult, walletResult, memRes, autoRes, vaultRes, agentsRes] = await Promise.allSettled([
50
50
  (0, token_gate_js_1.getTier)(),
51
51
  (0, wallet_js_1.getOrCreateWallet)(),
52
52
  memoryProfileCall,
53
53
  (0, convex_js_1.callConvex)("/automations/list", "GET", undefined, "list_automations"),
54
- (0, convex_js_1.callConvex)("/vault/list?type=research&limit=5", "GET", undefined, "finch_status"),
55
- (0, convex_js_1.callConvex)("/vault/list?type=memory&limit=20", "GET", undefined, "finch_status"),
54
+ (0, convex_js_1.callConvex)("/vault/list?type=research&limit=5", "GET", undefined, "vault_list"),
55
+ (0, convex_js_1.callConvex)("/vault/list?type=memory&limit=20", "GET", undefined, "vault_list"),
56
56
  ]);
57
57
  const tier = tierResult.status === "fulfilled" ? tierResult.value : "basic";
58
58
  const wallet = walletResult.status === "fulfilled" ? walletResult.value : null;
@@ -171,8 +171,13 @@ async function handleOsTool(name, args) {
171
171
  hints.push(`โ†’ No FIRECRAWL_API_KEY โ€” deep_research falls back to Finch proxy (requires session token).`);
172
172
  if (!localMemCfg)
173
173
  hints.push(`โ†’ Memory tools use the Finch-hosted proxy. Run \`finch setup\` for free, self-hosted local memory.`);
174
- else if (localSmStatus !== "ok")
174
+ else if (localMemCfg.kind === "supermemory" && localSmStatus !== "ok")
175
175
  hints.push(`โ†’ Local memory configured but ${localMemCfg.url} isn't reachable โ€” memory tools will fail. Run \`npx -y supermemory local\`.`);
176
+ const localMemLabel = !localMemCfg
177
+ ? "not configured โ€” run `finch setup`"
178
+ : localMemCfg.kind === "file"
179
+ ? "file-based at ~/.finch/memory"
180
+ : localSmStatus === "ok" ? `reachable at ${localMemCfg.url}` : `configured but unreachable at ${localMemCfg.url}`;
176
181
  const lines = [
177
182
  `## ๐Ÿฉบ Finch Diagnostics`,
178
183
  ``,
@@ -180,7 +185,7 @@ async function handleOsTool(name, args) {
180
185
  ` ${statusIcon(convexStatus)} Convex backend ${convexStatus === "ok" ? "reachable" : "unreachable โ€” check FINCH_CONVEX_URL"}`,
181
186
  ` ${statusIcon(fcStatus)} Firecrawl ${fcStatus === "ok" ? "reachable" : fcStatus === "unconfigured" ? "no FIRECRAWL_API_KEY โ€” deep_research will use proxy" : "unreachable"}`,
182
187
  ` ${statusIcon(smStatus)} Supermemory (cloud) ${localMemCfg ? "not used โ€” local memory active" : smStatus === "ok" ? "reachable" : "unreachable โ€” memory tools may fail"}`,
183
- ` ${statusIcon(localSmStatus)} Supermemory (local) ${localMemCfg ? (localSmStatus === "ok" ? `reachable at ${localMemCfg.url}` : `configured but unreachable at ${localMemCfg.url}`) : "not configured โ€” run `finch setup`"}`,
188
+ ` ${statusIcon(localSmStatus)} Local memory ${localMemLabel}`,
184
189
  ``,
185
190
  `**API Keys configured:** _(none of the LLM keys are required โ€” see below)_`,
186
191
  ...Object.entries(envKeys).map(([k, v]) => ` ${v ? "โœ…" : "โšช"} ${k}`),
@@ -134,7 +134,7 @@ async function handlePacket(toolName, args) {
134
134
  content,
135
135
  contentType: "json",
136
136
  tags: ["packet", ...(tags ?? [])],
137
- }, "packet_create");
137
+ }, "vault_save");
138
138
  return {
139
139
  content: [{
140
140
  type: "text",
@@ -153,7 +153,7 @@ async function handlePacket(toolName, args) {
153
153
  }
154
154
  if (toolName === "packet_run") {
155
155
  const { name } = args;
156
- const data = await (0, convex_js_1.callConvex)(`/vault/entry?key=${encodeURIComponent(packetKey(name))}`, "GET", undefined, "packet_run");
156
+ const data = await (0, convex_js_1.callConvex)(`/vault/entry?key=${encodeURIComponent(packetKey(name))}`, "GET", undefined, "vault_read");
157
157
  if (!data?.content || data?.error) {
158
158
  return {
159
159
  content: [{ type: "text", text: `Packet \`${name}\` not found. Use \`packet_list\` to see available packets.` }],
@@ -271,7 +271,7 @@ async function handlePacket(toolName, args) {
271
271
  const data = await (0, convex_js_1.callConvex)("/vault/publish", "POST", {
272
272
  key: packetKey(name),
273
273
  authorName: authorName ?? "anonymous",
274
- }, "packet_share");
274
+ }, "vault_publish");
275
275
  if (data.error)
276
276
  return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
277
277
  return {
@@ -68,7 +68,7 @@ async function firecrawlScrape(url) {
68
68
  // FIRECRAWL_API_KEY set either, in which case web_scrape silently falls
69
69
  // through to basicFetch below.
70
70
  try {
71
- const data = await (0, convex_js_1.callConvex)("/research/firecrawl-scrape", "POST", { url }, "web_scrape_proxy", 25000);
71
+ const data = await (0, convex_js_1.callConvex)("/research/firecrawl-scrape", "POST", { url }, "web_scrape", 25000);
72
72
  if (data?.markdown)
73
73
  return data.markdown;
74
74
  }
@@ -149,7 +149,7 @@ async function handleResearchTool(name, args) {
149
149
  }
150
150
  if (!results) {
151
151
  try {
152
- const data = await (0, convex_js_1.callConvex)("/research/firecrawl-search", "POST", { query, limit }, "web_search_proxy", 30000);
152
+ const data = await (0, convex_js_1.callConvex)("/research/firecrawl-search", "POST", { query, limit }, "web_search", 30000);
153
153
  results = data?.results ?? [];
154
154
  }
155
155
  catch (err) {
@@ -33,6 +33,15 @@ exports.RH_EXPLORER = "https://robinhoodchain.blockscout.com";
33
33
  const NATIVE_ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
34
34
  const RH_PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
35
35
  const RH_UNIVERSAL_ROUTER = "0x8876789976decbfcbbbe364623c63652db8c0904";
36
+ /**
37
+ * Canonical Global Dollar (USDG) โ€” RH chain's settlement stablecoin (same
38
+ * address as app/convex/_settlement.ts). Hardcoded rather than resolved via
39
+ * DexScreener ticker search: "USDG" is the exact symbol an imposter contract
40
+ * would spoof, and dexSearchByTicker ranks by pool liquidity, not authenticity
41
+ * โ€” a fake pool with inflated liquidity could otherwise outrank the real token.
42
+ */
43
+ const USDG_ADDRESS = "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168";
44
+ const USDG_DECIMALS = 6;
36
45
  /** ClawHood / Finch catalog (22 tokenized stocks โ€” official "Robinhood Token" contracts). */
37
46
  exports.RH_STOCKS = [
38
47
  { address: "0xaf3d76f1834a1d425780943c99ea8a608f8a93f9", symbol: "AAPL", name: "Apple" },
@@ -336,6 +345,9 @@ async function resolveTokenSmart(input) {
336
345
  if (isEth(input))
337
346
  return { kind: "eth", address: NATIVE_ETH, symbol: "ETH", decimals: 18 };
338
347
  const t = input.trim();
348
+ if (t.toUpperCase() === "USDG") {
349
+ return { kind: "token", address: USDG_ADDRESS, symbol: "USDG", name: "Global Dollar", decimals: USDG_DECIMALS };
350
+ }
339
351
  // 1) catalog symbol fast-path (22 tokenized stocks, all 18-decimals)
340
352
  const cat = exports.RH_STOCKS.find((s) => s.symbol === t.toUpperCase());
341
353
  if (cat)