@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.
@@ -47,6 +47,7 @@ const zod_1 = require("zod");
47
47
  const crypto = __importStar(require("crypto"));
48
48
  const convex_js_1 = require("../convex.js");
49
49
  const public_url_js_1 = require("../public-url.js");
50
+ const local_vault_js_1 = require("../local-vault.js");
50
51
  const local_memory_js_1 = require("../local-memory.js");
51
52
  // memory_extract and memory_consolidate used to run their own LLM calls here
52
53
  // (and a matching pair of Convex routes did the same server-side). Both are now
@@ -89,6 +90,18 @@ function lookupRecentHash(hash) {
89
90
  }
90
91
  return hit;
91
92
  }
93
+ // memory_delete must purge this too - otherwise a deleted memory's hash stays
94
+ // cached (up to an hour) and re-adding the exact same content within that
95
+ // window gets silently skipped as a "duplicate" of an id that no longer
96
+ // exists, when there is no longer any real duplicate to skip.
97
+ function forgetRecentHashById(id) {
98
+ for (const [hash, entry] of recentHashCache) {
99
+ if (entry.id === id) {
100
+ recentHashCache.delete(hash);
101
+ break;
102
+ }
103
+ }
104
+ }
92
105
  // Two-tier dedup lookup: in-process cache first (catches same-session
93
106
  // dupes during eventual-consistency window), then a list call (catches
94
107
  // cross-session dupes once they've been indexed). Returns null on any
@@ -101,7 +114,7 @@ async function findDuplicateMemory(hash) {
101
114
  const local = (0, local_memory_js_1.getLocalMemoryConfig)();
102
115
  const results = local
103
116
  ? await (0, local_memory_js_1.localMemoryList)(local, 50)
104
- : ((await (0, convex_js_1.callConvex)("/memory/list", "POST", { n: 50 }))?.results ?? []);
117
+ : ((await (0, convex_js_1.callConvex)("/memory/list", "POST", { n: 50 }, "memory_list"))?.results ?? []);
105
118
  const match = results.find((r) => r.metadata?.contentHash === hash);
106
119
  if (!match)
107
120
  return null;
@@ -154,7 +167,7 @@ async function syncToSupermemory(content, metadata, sourceUrl) {
154
167
  };
155
168
  for (let attempt = 0; attempt <= SYNC_RETRY_DELAYS_MS.length; attempt++) {
156
169
  try {
157
- await (0, convex_js_1.callConvex)("/memory/add", "POST", payload);
170
+ await (0, convex_js_1.callConvex)("/memory/add", "POST", payload, "memory_add");
158
171
  return;
159
172
  }
160
173
  catch (err) {
@@ -183,7 +196,7 @@ async function searchSupermemory(query, limit = 10) {
183
196
  const local = (0, local_memory_js_1.getLocalMemoryConfig)();
184
197
  if (local)
185
198
  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 });
199
+ const data = await (0, convex_js_1.callConvex)("/memory/search", "POST", { q: query, n: limit }, "memory_search");
187
200
  return data?.results ?? [];
188
201
  }
189
202
  catch {
@@ -195,7 +208,7 @@ async function searchSupermemory(query, limit = 10) {
195
208
  // directly without re-normalizing raw scores.
196
209
  async function lexicalSearch(query, limit = 30) {
197
210
  try {
198
- const data = await (0, convex_js_1.callConvex)("/memory/lexical", "POST", { q: query, n: limit });
211
+ const data = await (0, convex_js_1.callConvex)("/memory/lexical", "POST", { q: query, n: limit }, "memory_search");
199
212
  const rows = (data?.results ?? []);
200
213
  return rows.map((r, idx) => ({
201
214
  id: r.id,
@@ -284,12 +297,13 @@ async function hybridMemorySearch(query, limit = 30) {
284
297
  exports.MEMORY_TOOLS = [
285
298
  {
286
299
  name: "memory_add",
287
- description: "Add content to your Finch semantic memory - no setup needed, no extra API keys. " +
300
+ description: "Add content to your Finch memory - no setup needed, no extra API keys. " +
288
301
  "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. " +
302
+ "Use for notes, decisions, preferences, or anything you want to find later. " +
290
303
  "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. " +
304
+ "searchable in ~30s. Retrieval is full-text (keyword) search, not embeddings - " +
305
+ "'what did I say about ETH yield?' finds notes containing those words or close variants, " +
306
+ "not unrelated phrasing with the same meaning. " +
293
307
  "Auto-deduplicates: identical content in your recent 50 memories is skipped (override with force:true).",
294
308
  inputSchema: {
295
309
  type: "object",
@@ -305,10 +319,11 @@ exports.MEMORY_TOOLS = [
305
319
  },
306
320
  {
307
321
  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).",
322
+ description: "Full-text (keyword) search over your stored memories, with 90-day time-decay weighting so " +
323
+ "recent notes outrank stale ones with similar wording. Good for exact-token lookups (env var " +
324
+ "names, contract addresses, IDs, specific phrases) - it does not understand meaning, so " +
325
+ "'low risk crypto yield' will not match a note phrased as 'conservative DeFi strategies' " +
326
+ "unless the words themselves overlap.",
312
327
  inputSchema: {
313
328
  type: "object",
314
329
  properties: {
@@ -320,9 +335,10 @@ exports.MEMORY_TOOLS = [
320
335
  },
321
336
  {
322
337
  name: "memory_context",
323
- description: "Retrieve the most semantically relevant memories for a topic, formatted as AI-ready context. " +
338
+ description: "Retrieve the most relevant memories for a topic, formatted as AI-ready context. " +
324
339
  "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.",
340
+ "Uses full-text (keyword) search, not embeddings - phrase your topic with the words " +
341
+ "you expect were actually used when the memory was saved.",
326
342
  inputSchema: {
327
343
  type: "object",
328
344
  properties: {
@@ -334,7 +350,7 @@ exports.MEMORY_TOOLS = [
334
350
  },
335
351
  {
336
352
  name: "memory_profile",
337
- description: "Show your semantic memory stats - total memories stored, your memory space, and connected sources. " +
353
+ description: "Show your memory stats - total memories stored, your memory space, and connected sources. " +
338
354
  "Useful for auditing what Finch knows about you.",
339
355
  inputSchema: {
340
356
  type: "object",
@@ -373,7 +389,7 @@ exports.MEMORY_TOOLS = [
373
389
  },
374
390
  {
375
391
  name: "memory_insight",
376
- description: "Get a full intelligence report on any topic - combines semantic memory AND vault entries, " +
392
+ description: "Get a full intelligence report on any topic - combines memory AND vault entries, " +
377
393
  "then identifies knowledge gaps and suggests next actions. " +
378
394
  "Use this before starting any research or trade decision to see everything Finch already knows. " +
379
395
  "Returns: confidence level, what you know, coverage timeline, gaps, and recommended next steps.",
@@ -388,7 +404,7 @@ exports.MEMORY_TOOLS = [
388
404
  },
389
405
  {
390
406
  name: "memory_extract",
391
- description: "Save discrete facts, preferences and decisions to semantic memory as individually searchable atoms " +
407
+ description: "Save discrete facts, preferences and decisions to memory as individually searchable atoms " +
392
408
  "instead of one wall of text. Two-pass, no API key needed. " +
393
409
  "PASS 1 — call with `text`: returns the text with the extraction rubric. " +
394
410
  "PASS 2 — call with `facts: [...]`: stores each fact separately, deduped. " +
@@ -407,26 +423,6 @@ exports.MEMORY_TOOLS = [
407
423
  required: [],
408
424
  },
409
425
  },
410
- {
411
- name: "memory_publish",
412
- description: "IRREVERSIBLE, PUBLIC. Publish a memory snippet to the Memory Marketplace — visible to " +
413
- "ALL Finch users at /memory-marketplace. Reversible with vault_unpublish, but only for " +
414
- "future discovery — anyone who already read it keeps what they saw. " +
415
- "Requires confirm: true. Never call this on the user's behalf without them explicitly asking " +
416
- "to publish; re-read the content for anything private (keys, addresses, personal details) first. " +
417
- "Saved as a public vault entry (type=memory).",
418
- inputSchema: {
419
- type: "object",
420
- properties: {
421
- title: { type: "string", description: "Short title for the memory (shown publicly in the marketplace)" },
422
- content: { type: "string", description: "The memory content to share — this becomes PUBLIC" },
423
- tags: { type: "array", items: { type: "string" }, description: "Optional tags (e.g. ['DeFi', 'Base', 'research'])" },
424
- authorName: { type: "string", description: "Public display name. Defaults to \"Anonymous\" — do NOT pass a wallet address unless the user asks to be identified." },
425
- confirm: { type: "boolean", description: "Must be true to publish. Guards against accidental public disclosure." },
426
- },
427
- required: ["title", "content", "confirm"],
428
- },
429
- },
430
426
  {
431
427
  name: "memory_consolidate",
432
428
  description: "Clean up fragmented knowledge after heavy research sessions. Two-pass, no API key needed. " +
@@ -560,7 +556,7 @@ async function handleMemoryTool(name, args) {
560
556
  const localAdd = (0, local_memory_js_1.getLocalMemoryConfig)();
561
557
  const data = localAdd
562
558
  ? 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 }));
559
+ : await (0, convex_js_1.callConvex)("/memory/add", "POST", { content, metadata: addMetadata, ...(sourceUrl ? { sourceUrl } : {}) }, "memory_add").catch((err) => ({ error: err.message }));
564
560
  if (data?.error)
565
561
  return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
566
562
  // Cache the hash so an immediate second call with identical content
@@ -588,11 +584,12 @@ async function handleMemoryTool(name, args) {
588
584
  const { query, limit = 10 } = parsed.data;
589
585
  // Over-fetch so post-decay ranking still has enough material.
590
586
  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.
587
+ // ─── Retrieval ───────────────────────────────────────────────────
588
+ // Fan out two full-text queries (searchSupermemory and lexicalSearch -
589
+ // both call Convex full-text search under different names, there is no
590
+ // embedding step despite the "semantic"/"Supermemory" naming) in
591
+ // parallel, fuse via Reciprocal Rank Fusion so results present in both
592
+ // rank highest.
596
593
  const fused = await hybridMemorySearch(query, overfetch);
597
594
  const raw = fused;
598
595
  if (!raw.length)
@@ -697,7 +694,7 @@ async function handleMemoryTool(name, args) {
697
694
  const localProfileCfg = (0, local_memory_js_1.getLocalMemoryConfig)();
698
695
  const data = localProfileCfg
699
696
  ? await (0, local_memory_js_1.localMemoryProfile)(localProfileCfg).catch(() => null)
700
- : await (0, convex_js_1.callConvex)("/memory/profile", "GET").catch(() => null);
697
+ : await (0, convex_js_1.callConvex)("/memory/profile", "GET", undefined, "memory_profile").catch(() => null);
701
698
  const total = data?.total ?? 0;
702
699
  const status = data?.status ?? "unknown";
703
700
  const space = data?.space ?? "-";
@@ -705,7 +702,7 @@ async function handleMemoryTool(name, args) {
705
702
  content: [{
706
703
  type: "text",
707
704
  text: [
708
- `🧠 **Finch Semantic Memory**`,
705
+ `🧠 **Finch Memory**`,
709
706
  ``,
710
707
  `Space: \`${space}\``,
711
708
  `Total memories: **${total}**`,
@@ -716,7 +713,7 @@ async function handleMemoryTool(name, args) {
716
713
  `• memory_add (URL indexing) - ✅`,
717
714
  `• Google Drive / Gmail / Notion - connect at finchagentic.com`,
718
715
  ``,
719
- `**Capabilities:** Semantic search · Vector context · 81.6% LongMemEval`,
716
+ `**Capabilities:** Full-text (keyword) search with 90-day time-decay ranking - not embeddings, no vector search.`,
720
717
  ].join("\n"),
721
718
  }],
722
719
  structuredContent: buildMemoryProfile(data),
@@ -738,7 +735,7 @@ async function handleMemoryTool(name, args) {
738
735
  }
739
736
  }
740
737
  else {
741
- const data = await (0, convex_js_1.callConvex)("/memory/list", "POST", { n: limit, tag }).catch((err) => ({ error: err.message }));
738
+ const data = await (0, convex_js_1.callConvex)("/memory/list", "POST", { n: limit, tag }, "memory_list").catch((err) => ({ error: err.message }));
742
739
  if (data?.error)
743
740
  return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
744
741
  results = data?.results ?? [];
@@ -777,10 +774,11 @@ async function handleMemoryTool(name, args) {
777
774
  }
778
775
  }
779
776
  else {
780
- const data = await (0, convex_js_1.callConvex)("/memory/delete", "POST", { id: parsed.data.id }).catch((err) => ({ error: err.message }));
777
+ const data = await (0, convex_js_1.callConvex)("/memory/delete", "POST", { id: parsed.data.id }, "memory_delete").catch((err) => ({ error: err.message }));
781
778
  if (data?.error)
782
779
  return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
783
780
  }
781
+ forgetRecentHashById(parsed.data.id);
784
782
  return { content: [{ type: "text", text: `🗑️ Memory deleted: \`${parsed.data.id}\`` }] };
785
783
  }
786
784
  case "memory_insight": {
@@ -791,10 +789,15 @@ async function handleMemoryTool(name, args) {
791
789
  const memLimit = depth === "deep" ? 15 : depth === "quick" ? 5 : 8;
792
790
  // v3.25.1: hybrid retrieval for memory side (was semantic-only) so the
793
791
  // intelligence report surfaces exact-token matches alongside meaning
794
- // matches. Vault side runs in parallel as before.
792
+ // matches. Vault side runs in parallel as before - but only against
793
+ // Convex when the vault isn't local; a local vault's topic string must
794
+ // never leave the machine, and this never checked that before.
795
+ const localVault = (0, local_vault_js_1.getLocalVaultConfig)();
795
796
  const [memResults, vaultData] = await Promise.all([
796
797
  hybridMemorySearch(topic, memLimit),
797
- (0, convex_js_1.callConvex)(`/vault/search?q=${encodeURIComponent(topic)}&limit=6`, "GET", undefined, "memory_insight").catch(() => ({ results: [] })),
798
+ localVault
799
+ ? Promise.resolve((0, local_vault_js_1.localVaultSearch)(localVault, topic, { limit: 6 }))
800
+ : (0, convex_js_1.callConvex)(`/vault/search?q=${encodeURIComponent(topic)}&limit=6`, "GET", undefined, "vault_search").catch(() => ({ results: [] })),
798
801
  ]);
799
802
  const vaultResults = vaultData.results ?? [];
800
803
  const total = memResults.length + vaultResults.length;
@@ -905,7 +908,7 @@ async function handleMemoryTool(name, args) {
905
908
  const metaFor = () => ({ source, addedAt: Date.now() });
906
909
  const results = await Promise.allSettled(facts.map((fact) => localExtractCfg
907
910
  ? (0, local_memory_js_1.localMemoryAdd)(localExtractCfg, fact, metaFor())
908
- : (0, convex_js_1.callConvex)("/memory/add", "POST", { content: fact, metadata: metaFor() })));
911
+ : (0, convex_js_1.callConvex)("/memory/add", "POST", { content: fact, metadata: metaFor() }, "memory_add")));
909
912
  const saved = results.filter((r) => r.status === "fulfilled").length;
910
913
  const failed = results.length - saved;
911
914
  return {
@@ -934,7 +937,7 @@ async function handleMemoryTool(name, args) {
934
937
  const meta = { title: `Consolidated: ${topic}`, source: "memory_consolidate", addedAt: Date.now() };
935
938
  const saved = localConsolidateCfg
936
939
  ? 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 }));
940
+ : await (0, convex_js_1.callConvex)("/memory/add", "POST", { content: summary, metadata: meta }, "memory_add").catch((err) => ({ error: err.message }));
938
941
  if (saved?.error) {
939
942
  return { content: [{ type: "text", text: `Error saving consolidated memory: ${saved.error}` }], isError: true };
940
943
  }
@@ -951,7 +954,11 @@ async function handleMemoryTool(name, args) {
951
954
  };
952
955
  }
953
956
  // ── PASS 1: fetch every memory on the topic, numbered ───────────────
954
- const rows = await searchSupermemory(topic, limit);
957
+ // Uses the same RRF-fused semantic+lexical retrieval as memory_search/
958
+ // memory_context - a plain single-source searchSupermemory() call here
959
+ // used to mean consolidation could miss memories only the lexical/BM25
960
+ // side would surface, while still claiming an exhaustive "N memories".
961
+ const rows = await hybridMemorySearch(topic, limit);
955
962
  if (rows.length === 0) {
956
963
  return { content: [{ type: "text", text: `No memories found for "${topic}" to consolidate.` }], isError: true };
957
964
  }
@@ -985,61 +992,22 @@ async function handleMemoryTool(name, args) {
985
992
  }],
986
993
  };
987
994
  }
988
- case "memory_publish": {
989
- const { title, content, tags, authorName, confirm } = args;
990
- if (!title || !content)
991
- return { content: [{ type: "text", text: "title and content are required" }], isError: true };
992
- if (confirm !== true) {
993
- return {
994
- content: [{
995
- type: "text",
996
- text: "Refusing to publish: this makes the content **public to all Finch users**. " +
997
- "It can be hidden again with `vault_unpublish`, but not un-read. Review the content " +
998
- "for keys, addresses and personal details, then pass `confirm: true`.",
999
- }],
1000
- isError: true,
1001
- };
1002
- }
1003
- if (authorName && /^0x[a-fA-F0-9]{40}$/.test(authorName.trim())) {
1004
- return {
1005
- content: [{
1006
- type: "text",
1007
- text: "Refusing to publish with a wallet address as the author name — that permanently " +
1008
- "links your on-chain identity to this public entry. Use a handle, or omit authorName " +
1009
- "to publish as \"Anonymous\".",
1010
- }],
1011
- isError: true,
1012
- };
1013
- }
1014
- const data = await (0, convex_js_1.callConvex)("/vault/save", "POST", {
1015
- type: "memory",
1016
- title,
1017
- content,
1018
- tags: tags ?? [],
1019
- isPublic: true,
1020
- authorName: authorName ?? "Anonymous",
1021
- commitMsg: "published to marketplace",
1022
- }, "vault_save");
1023
- if (data.error)
1024
- return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
1025
- return {
1026
- content: [{
1027
- type: "text",
1028
- text: [
1029
- `🧠 **Memory Published**`,
1030
- ``,
1031
- `**Title:** ${title}`,
1032
- `**Key:** \`${data.key}\``,
1033
- `**Version:** ${data.version ?? 1}`,
1034
- ``,
1035
- `Now visible at the Memory Marketplace in the Finch app.`,
1036
- ``,
1037
- `Make it private again: \`vault_unpublish key: "${data.key}"\``,
1038
- `That stops future discovery. Anyone who already read it keeps what they saw.`,
1039
- ].join("\n"),
1040
- }],
1041
- };
1042
- }
995
+ // memory_publish was removed - it was broken two levels deep. The tool
996
+ // called POST /vault/save with isPublic/authorName fields that the
997
+ // handler silently dropped (only type/title/content/key/contentType/
998
+ // agentId/tags/commitMsg/metadata are forwarded to vault.saveEntry - see
999
+ // app/convex/http.ts), so `published` was never actually set true; the
1000
+ // real publishEntry mutation (POST /vault/publish) existed but this tool
1001
+ // never called it. And even a correctly-wired publish would have done
1002
+ // nothing observable: nothing anywhere reads the `published` field for
1003
+ // cross-user browsing - no /vault/community route (dangling comment
1004
+ // only, same pattern as the other removed routes), no backend query for
1005
+ // it, no "Memory Marketplace" page in app/src. The tool asked users to
1006
+ // accept an "IRREVERSIBLE, PUBLIC" risk for a marketplace that doesn't
1007
+ // exist at any layer. Re-add only alongside building the actual
1008
+ // discovery path: a community-browse query + route + UI that reads
1009
+ // `published: true` entries. vault_unpublish is left in place - it's a
1010
+ // correct, harmless no-op until then.
1043
1011
  default:
1044
1012
  return null;
1045
1013
  }
@@ -22,8 +22,8 @@ const MONITOR_INPUT_SCHEMA = {
22
22
  exports.MONITOR_TOOLS = [
23
23
  {
24
24
  name: "schedule_research",
25
- description: "Schedule recurring autonomous research on any topic - runs on a cron schedule, saves findings to vault, " +
26
- "and sends a Telegram notification. The agent runs completely on its own with no prompting needed. " +
25
+ description: "Schedule recurring autonomous research on any topic - runs on a cron schedule, saves findings to vault. " +
26
+ "The agent runs completely on its own with no prompting needed. " +
27
27
  "Requires TRIGGER_SECRET_KEY env var (trigger.dev). " +
28
28
  "Examples: daily morning briefing, weekly competitor analysis, hourly price alerts, monthly industry report.",
29
29
  inputSchema: MONITOR_INPUT_SCHEMA,
@@ -103,7 +103,7 @@ function buildMonitorList(schedules, configs = {}) {
103
103
  };
104
104
  }
105
105
  async function handleMonitorTool(name, args) {
106
- if (name === "schedule_research" || name === "create_monitor") {
106
+ if (name === "schedule_research") {
107
107
  const parsed = CreateSchema.safeParse(args);
108
108
  if (!parsed.success)
109
109
  return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
@@ -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 ?? "{}");
@@ -192,7 +192,7 @@ async function handleMonitorTool(name, args) {
192
192
  key: `monitor-config/${externalId}`,
193
193
  agentId: "os",
194
194
  tags: ["monitor-config"],
195
- commitMsg: "create_monitor config",
195
+ commitMsg: "schedule_research config",
196
196
  }, "vault_save");
197
197
  }
198
198
  catch {
@@ -210,7 +210,7 @@ async function handleMonitorTool(name, args) {
210
210
  data.nextRun ? `⏭️ Next run: ${new Date(data.nextRun).toUTCString()}` : "",
211
211
  ``,
212
212
  configSaved
213
- ? `The agent will research "${topic}" on schedule, save findings to vault, and send a Telegram notification if configured.`
213
+ ? `The agent will research "${topic}" on schedule and save findings to vault.`
214
214
  : `⚠️ Monitor schedule created but config save failed - the agent may use a default topic on first run. Try \`cancel_monitor\` and recreate.`,
215
215
  `Use \`list_monitors\` to see all active monitors.`,
216
216
  ].filter(Boolean).join("\n"),
@@ -240,7 +240,7 @@ async function handleMonitorTool(name, args) {
240
240
  return {
241
241
  content: [{
242
242
  type: "text",
243
- text: `No active monitors.\n\nUse \`create_monitor\` to set up an autonomous agent that runs on a schedule.`,
243
+ text: `No active monitors.\n\nUse \`schedule_research\` to set up an autonomous agent that runs on a schedule.`,
244
244
  }],
245
245
  structuredContent: buildMonitorList([]),
246
246
  };
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;
@@ -138,7 +138,6 @@ async function handleOsTool(name, args) {
138
138
  "FIRECRAWL_API_KEY": !!process.env.FIRECRAWL_API_KEY,
139
139
  "FINCH_SESSION_TOKEN": !!process.env.FINCH_SESSION_TOKEN,
140
140
  "FINCH_API_KEY": !!process.env.FINCH_API_KEY,
141
- "TELEGRAM_BOT_TOKEN": !!process.env.TELEGRAM_BOT_TOKEN,
142
141
  };
143
142
  const localMemCfg = (0, local_memory_js_1.getLocalMemoryConfig)();
144
143
  const pingLocalMemory = async () => {
@@ -171,8 +170,13 @@ async function handleOsTool(name, args) {
171
170
  hints.push(`→ No FIRECRAWL_API_KEY — deep_research falls back to Finch proxy (requires session token).`);
172
171
  if (!localMemCfg)
173
172
  hints.push(`→ Memory tools use the Finch-hosted proxy. Run \`finch setup\` for free, self-hosted local memory.`);
174
- else if (localSmStatus !== "ok")
173
+ else if (localMemCfg.kind === "supermemory" && localSmStatus !== "ok")
175
174
  hints.push(`→ Local memory configured but ${localMemCfg.url} isn't reachable — memory tools will fail. Run \`npx -y supermemory local\`.`);
175
+ const localMemLabel = !localMemCfg
176
+ ? "not configured — run `finch setup`"
177
+ : localMemCfg.kind === "file"
178
+ ? "file-based at ~/.finch/memory"
179
+ : localSmStatus === "ok" ? `reachable at ${localMemCfg.url}` : `configured but unreachable at ${localMemCfg.url}`;
176
180
  const lines = [
177
181
  `## 🩺 Finch Diagnostics`,
178
182
  ``,
@@ -180,7 +184,7 @@ async function handleOsTool(name, args) {
180
184
  ` ${statusIcon(convexStatus)} Convex backend ${convexStatus === "ok" ? "reachable" : "unreachable — check FINCH_CONVEX_URL"}`,
181
185
  ` ${statusIcon(fcStatus)} Firecrawl ${fcStatus === "ok" ? "reachable" : fcStatus === "unconfigured" ? "no FIRECRAWL_API_KEY — deep_research will use proxy" : "unreachable"}`,
182
186
  ` ${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`"}`,
187
+ ` ${statusIcon(localSmStatus)} Local memory ${localMemLabel}`,
184
188
  ``,
185
189
  `**API Keys configured:** _(none of the LLM keys are required — see below)_`,
186
190
  ...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.` }],
@@ -245,8 +245,8 @@ async function handlePacket(toolName, args) {
245
245
  }
246
246
  if (toolName === "packet_share") {
247
247
  const { name, authorName, confirm } = args;
248
- // Same bar as memory_publish and vault_delete: an action the user cannot
249
- // fully take back needs an explicit second step, not a single call.
248
+ // Same bar as vault_delete: an action the user cannot fully take back
249
+ // needs an explicit second step, not a single call.
250
250
  if (confirm !== true) {
251
251
  return {
252
252
  content: [{
@@ -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) {
@@ -14,6 +14,7 @@ exports.dexTokenBest = dexTokenBest;
14
14
  exports.resolveTokenSmart = resolveTokenSmart;
15
15
  exports.buildRhAnalysis = buildRhAnalysis;
16
16
  exports.buildRhStocksList = buildRhStocksList;
17
+ exports.rhProviderAsync = rhProviderAsync;
17
18
  exports.rhErc20Balance = rhErc20Balance;
18
19
  exports.rhPriceUsd = rhPriceUsd;
19
20
  exports.buildRhSafetyStructured = buildRhSafetyStructured;
@@ -33,6 +34,15 @@ exports.RH_EXPLORER = "https://robinhoodchain.blockscout.com";
33
34
  const NATIVE_ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
34
35
  const RH_PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
35
36
  const RH_UNIVERSAL_ROUTER = "0x8876789976decbfcbbbe364623c63652db8c0904";
37
+ /**
38
+ * Canonical Global Dollar (USDG) — RH chain's settlement stablecoin (same
39
+ * address as app/convex/_settlement.ts). Hardcoded rather than resolved via
40
+ * DexScreener ticker search: "USDG" is the exact symbol an imposter contract
41
+ * would spoof, and dexSearchByTicker ranks by pool liquidity, not authenticity
42
+ * — a fake pool with inflated liquidity could otherwise outrank the real token.
43
+ */
44
+ const USDG_ADDRESS = "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168";
45
+ const USDG_DECIMALS = 6;
36
46
  /** ClawHood / Finch catalog (22 tokenized stocks — official "Robinhood Token" contracts). */
37
47
  exports.RH_STOCKS = [
38
48
  { address: "0xaf3d76f1834a1d425780943c99ea8a608f8a93f9", symbol: "AAPL", name: "Apple" },
@@ -336,6 +346,9 @@ async function resolveTokenSmart(input) {
336
346
  if (isEth(input))
337
347
  return { kind: "eth", address: NATIVE_ETH, symbol: "ETH", decimals: 18 };
338
348
  const t = input.trim();
349
+ if (t.toUpperCase() === "USDG") {
350
+ return { kind: "token", address: USDG_ADDRESS, symbol: "USDG", name: "Global Dollar", decimals: USDG_DECIMALS };
351
+ }
339
352
  // 1) catalog symbol fast-path (22 tokenized stocks, all 18-decimals)
340
353
  const cat = exports.RH_STOCKS.find((s) => s.symbol === t.toUpperCase());
341
354
  if (cat)
@@ -978,7 +991,16 @@ async function quoteRh(args) {
978
991
  throw new Error("RH swaps route ETH ↔ token. Buy a token with ETH, or sell a token for ETH.");
979
992
  }
980
993
  const sellAmount = parseHumanToWei(args.amount, from.decimals);
981
- const slippageBps = Math.round((args.maxSlippagePct ?? 2.0) * 100);
994
+ const slippagePct = args.maxSlippagePct ?? 2.0;
995
+ // Same bound defi.ts's SwapSchema already enforces for Base swaps
996
+ // (.positive().max(50)) - this file had no equivalent check anywhere, so a
997
+ // negative or absurd value flowed straight through to the backend as
998
+ // slippageBps, both for direct rh_mcp_swap calls and for every unattended
999
+ // rh_orders_tick execution of a DCA/bracket order created with a bad value.
1000
+ if (!(slippagePct > 0) || slippagePct > 50) {
1001
+ throw new Error(`maxSlippagePct must be greater than 0 and at most 50 (got ${args.maxSlippagePct}).`);
1002
+ }
1003
+ const slippageBps = Math.round(slippagePct * 100);
982
1004
  const result = await (0, convex_js_1.callConvex)("/mcp/rh/quote", "POST", {
983
1005
  sellToken: from.address,
984
1006
  buyToken: to.address,
@@ -987,7 +1009,7 @@ async function quoteRh(args) {
987
1009
  slippageBps,
988
1010
  fromSymbol: from.symbol,
989
1011
  toSymbol: to.symbol,
990
- }, "rh_mcp_estimate");
1012
+ }, args.toolName ?? "rh_mcp_estimate");
991
1013
  if (result.error)
992
1014
  throw new Error(result.error);
993
1015
  return { ...result, from, to, sellAmount, slippageBps };
@@ -1257,6 +1279,7 @@ async function handleRhMcpTool(name, args) {
1257
1279
  amount: String(a.amount),
1258
1280
  maxSlippagePct: a.maxSlippagePct,
1259
1281
  taker: wallet.address,
1282
+ toolName: "rh_mcp_swap",
1260
1283
  });
1261
1284
  const quote = q.quote ?? q;
1262
1285
  const tx = quote.transaction ?? {