@finchagentic/mcp 4.2.0 β†’ 4.5.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.
@@ -44,10 +44,17 @@ function categorizeError(err) {
44
44
  exports.AUTOMATION_TOOLS = [
45
45
  {
46
46
  name: "create_automation",
47
- description: "Create an automation in plain English. Supports DCA, price alerts, conditional buys/sells, and recurring market updates.",
47
+ description: "Create an automation in plain English. Supports DCA, price alerts, conditional buys/sells, and recurring market updates. " +
48
+ "A swap/send automation arms UNATTENDED, REPEATING real fund movement (a backend cron fires it going forward, not just once) - " +
49
+ "requires two calls. First call WITHOUT confirm: returns a preview of the parsed trigger/action, nothing is created yet. " +
50
+ "Show that preview to the user in plain language and get explicit confirmation. Only then call again with the SAME rawInput " +
51
+ "and confirm: true to actually create it. Never pass confirm: true on the first call.",
48
52
  inputSchema: {
49
53
  type: "object",
50
- properties: { rawInput: { type: "string", description: "Plain English description of the automation" } },
54
+ properties: {
55
+ rawInput: { type: "string", description: "Plain English description of the automation" },
56
+ confirm: { type: "boolean", description: "Must be true to actually create. Omit or false to get a preview only - nothing is created." },
57
+ },
51
58
  required: ["rawInput"],
52
59
  },
53
60
  },
@@ -108,7 +115,7 @@ exports.AUTOMATION_TOOLS = [
108
115
  },
109
116
  },
110
117
  ];
111
- const CreateAutomationSchema = zod_1.z.object({ rawInput: zod_1.z.string().min(1) });
118
+ const CreateAutomationSchema = zod_1.z.object({ rawInput: zod_1.z.string().min(1), confirm: zod_1.z.boolean().optional() });
112
119
  const AutomationIdSchema = zod_1.z.object({ automationId: zod_1.z.string().min(1) });
113
120
  const RunAutomationSchema = zod_1.z.object({ automationId: zod_1.z.string().min(1), dryRun: zod_1.z.boolean().optional() });
114
121
  const RunsSchema = zod_1.z.object({ automationId: zod_1.z.string().min(1), limit: zod_1.z.number().int().min(1).max(100).optional() });
@@ -148,15 +155,44 @@ async function handleAutomationTool(name, args) {
148
155
  const parsed = CreateAutomationSchema.safeParse(args);
149
156
  if (!parsed.success)
150
157
  return { content: [{ type: "text", text: `Invalid input: rawInput ${parsed.error.issues[0].message}` }], isError: true };
151
- const data = await (0, convex_js_1.callConvex)("/automations/create", "POST", { rawInput: parsed.data.rawInput }, "create_automation");
152
- if (!data.success)
153
- return { content: [{ type: "text", text: `Failed: ${data.error}` }], isError: true };
154
158
  const triggerLabel = {
155
159
  schedule: "⏰ Schedule", price_drop_pct: "πŸ“‰ Price Drop %", price_rise_pct: "πŸ“ˆ Price Rise %",
156
160
  price_below: "⬇️ Price Below", price_above: "⬆️ Price Above",
157
161
  dominance_below: "πŸ“Š Dominance Below", dominance_above: "πŸ“Š Dominance Above",
158
162
  };
159
163
  const actionLabel = { swap: "πŸ’± Swap", send: "πŸ“€ Send", alert: "πŸ”” Alert" };
164
+ // Not confirmed yet - dry-run only (backend validates/parses but does
165
+ // NOT create anything - see http.ts's /automations/create dryRun
166
+ // branch). A swap/send automation arms unattended, repeating real
167
+ // fund movement, so it needs a real preview + explicit confirm, same
168
+ // shape as base_mcp_swap/send - not created blind on the first call.
169
+ if (parsed.data.confirm !== true) {
170
+ const preview = await (0, convex_js_1.callConvex)("/automations/create", "POST", { rawInput: parsed.data.rawInput, dryRun: true }, "create_automation");
171
+ if (!preview.success)
172
+ return { content: [{ type: "text", text: `Could not parse this automation: ${preview.error}` }], isError: true };
173
+ const moneyMoving = preview.actionType === "swap" || preview.actionType === "send";
174
+ return {
175
+ content: [{
176
+ type: "text",
177
+ text: [
178
+ `**Preview - nothing created yet**`, ``,
179
+ `**Trigger:** ${triggerLabel[preview.triggerType] ?? preview.triggerType}`,
180
+ `**Action:** ${actionLabel[preview.actionType] ?? preview.actionType}`,
181
+ preview.fromToken && preview.toToken ? `**Swap:** ${preview.amountUsd ? `$${preview.amountUsd}` : `${preview.amountPct}%`} ${preview.fromToken} β†’ ${preview.toToken}` : ``,
182
+ preview.toAddress ? `**Send:** ${preview.amountUsd ? `$${preview.amountUsd}` : ""} ${preview.fromToken ?? ""} to \`${preview.toAddress}\`` : ``,
183
+ preview.priceToken ? `**Price condition:** ${preview.priceToken} ${preview.triggerType?.includes("above") ? "above" : "below"} ${preview.priceThreshold}` : ``,
184
+ preview.priceBaselineUsd ? `**Baseline price:** $${Number(preview.priceBaselineUsd).toLocaleString()}` : ``,
185
+ moneyMoving
186
+ ? `\n⚠️ This will move real funds, repeatedly, unattended, until paused or deleted. Show this preview to the user and get explicit confirmation.`
187
+ : `\nThis is alert-only - it will not move funds.`,
188
+ `\nIf this is correct, call create_automation again with the same rawInput and confirm: true.`,
189
+ ].filter(Boolean).join("\n"),
190
+ }],
191
+ };
192
+ }
193
+ const data = await (0, convex_js_1.callConvex)("/automations/create", "POST", { rawInput: parsed.data.rawInput }, "create_automation");
194
+ if (!data.success)
195
+ return { content: [{ type: "text", text: `Failed: ${data.error}` }], isError: true };
160
196
  return {
161
197
  content: [{
162
198
  type: "text",
@@ -265,7 +301,11 @@ async function handleAutomationTool(name, args) {
265
301
  if (!parsed.success)
266
302
  return { content: [{ type: "text", text: `Invalid input: automationId ${parsed.error.issues[0].message}` }], isError: true };
267
303
  const { automationId, dryRun } = parsed.data;
268
- const data = await (0, convex_js_1.callConvex)("/automations/run", "POST", { automationId, dryRun: !!dryRun }, "run_automation");
304
+ // A real (non-dryRun) trigger moves funds if the automation is a
305
+ // swap/send - noRetry so a lost response never risks re-sending the
306
+ // same trigger. A dryRun call does nothing server-side, so retrying
307
+ // it is harmless and stays on the default retry behavior.
308
+ const data = await (0, convex_js_1.callConvex)("/automations/run", "POST", { automationId, dryRun: !!dryRun }, "run_automation", 30000, !dryRun);
269
309
  if (data.error) {
270
310
  const cat = categorizeError(data.error);
271
311
  return {
@@ -52,7 +52,7 @@ async function handleCoderTool(name, args) {
52
52
  return null;
53
53
  const p = AuditSchema.safeParse(args);
54
54
  if (!p.success)
55
- return err(`Invalid input: ${p.error.message}`);
55
+ return err(p.error.issues[0].message);
56
56
  const { code, focus = [] } = p.data;
57
57
  // The pattern scan grounds the review. Without it an audit is pure model
58
58
  // opinion β€” someone might trust "looks safe" with nothing behind it.
@@ -915,7 +915,7 @@ async function handleDeepResearch(name, args, onProgress) {
915
915
  return null;
916
916
  const parsed = InputSchema.safeParse(args);
917
917
  if (!parsed.success) {
918
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
918
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
919
919
  }
920
920
  const { query, focus, continueFrom } = parsed.data;
921
921
  const clientQueries = parsed.data.queries ?? [];
@@ -99,7 +99,7 @@ async function handleDefiTool(name, args) {
99
99
  case "estimate_swap": {
100
100
  const parsed = SwapSchema.safeParse(args);
101
101
  if (!parsed.success)
102
- return { content: [{ type: "text", text: `Invalid input: ${String(parsed.error.issues[0].path[0])} ${parsed.error.issues[0].message}` }], isError: true };
102
+ return { content: [{ type: "text", text: `${String(parsed.error.issues[0].path[0])}: ${parsed.error.issues[0].message}` }], isError: true };
103
103
  const { fromToken, toToken, amount, maxSlippagePct, maxPriceImpactPct } = parsed.data;
104
104
  const slippageLimit = maxSlippagePct ?? DEFAULT_MAX_SLIPPAGE_PCT;
105
105
  const impactLimit = maxPriceImpactPct ?? DEFAULT_MAX_PRICE_IMPACT_PCT;
@@ -137,7 +137,7 @@ async function handleDefiTool(name, args) {
137
137
  case "swap_tokens": {
138
138
  const parsed = SwapSchema.safeParse(args);
139
139
  if (!parsed.success)
140
- return { content: [{ type: "text", text: `Invalid input: ${String(parsed.error.issues[0].path[0])} ${parsed.error.issues[0].message}` }], isError: true };
140
+ return { content: [{ type: "text", text: `${String(parsed.error.issues[0].path[0])}: ${parsed.error.issues[0].message}` }], isError: true };
141
141
  const { fromToken, toToken, amount, maxSlippagePct, maxPriceImpactPct } = parsed.data;
142
142
  const slippageLimit = maxSlippagePct ?? DEFAULT_MAX_SLIPPAGE_PCT;
143
143
  const impactLimit = maxPriceImpactPct ?? DEFAULT_MAX_PRICE_IMPACT_PCT;
@@ -190,7 +190,7 @@ async function handleDefiTool(name, args) {
190
190
  case "send_token": {
191
191
  const parsed = SendSchema.safeParse(args);
192
192
  if (!parsed.success)
193
- return { content: [{ type: "text", text: `Invalid input: ${String(parsed.error.issues[0].path[0])} ${parsed.error.issues[0].message}` }], isError: true };
193
+ return { content: [{ type: "text", text: `${String(parsed.error.issues[0].path[0])}: ${parsed.error.issues[0].message}` }], isError: true };
194
194
  const { token, toAddress, amount } = parsed.data;
195
195
  const wallet = await (0, wallet_js_1.getOrCreateWallet)();
196
196
  const result = await (0, convex_js_1.callConvex)("/mcp/defi/send", "POST", parsed.data, "send_token");
@@ -225,7 +225,7 @@ async function handleDefiTool(name, args) {
225
225
  case "get_defi_yields": {
226
226
  const parsed = DefiYieldsSchema.safeParse(args ?? {});
227
227
  if (!parsed.success)
228
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
228
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
229
229
  const { token, minApy = 1, limit = 20 } = parsed.data;
230
230
  let pools;
231
231
  try {
@@ -183,7 +183,7 @@ async function handleEquityTool(name, args) {
183
183
  return null;
184
184
  const parsed = Schema.safeParse(args);
185
185
  if (!parsed.success) {
186
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
186
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
187
187
  }
188
188
  const ticker = parsed.data.ticker.trim().toUpperCase();
189
189
  const periods = parsed.data.periods ?? 6;
@@ -97,7 +97,7 @@ async function handleEventTool(name, args) {
97
97
  return null;
98
98
  const parsed = Schema.safeParse(args);
99
99
  if (!parsed.success) {
100
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
100
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
101
101
  }
102
102
  const ticker = parsed.data.ticker.trim().toUpperCase();
103
103
  const limit = parsed.data.limit ?? 15;
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.GITHUB_TOOLS = void 0;
4
37
  exports.buildRepoList = buildRepoList;
@@ -9,13 +42,30 @@ exports.buildIssueDetail = buildIssueDetail;
9
42
  exports.buildCommitList = buildCommitList;
10
43
  exports.buildCodeSearch = buildCodeSearch;
11
44
  exports.handleGithubTool = handleGithubTool;
45
+ const fs = __importStar(require("fs"));
46
+ const path = __importStar(require("path"));
12
47
  const GH_BASE = "https://api.github.com";
48
+ // Was hardcoded "finch-mcp/3.28.0" - stale the moment package.json's version
49
+ // moved on (already 4.4.0 locally as of this fix), unlike server.ts/cli.ts/
50
+ // index.ts which all read it from package.json at runtime per this repo's
51
+ // own stated policy (see CLAUDE.md's mcp-server note). __dirname here is
52
+ // dist/tools/ once built, so two levels up reaches the package root - one
53
+ // level deeper than server.ts's own copy of this pattern (dist/).
54
+ const PKG_VERSION = (() => {
55
+ try {
56
+ const raw = fs.readFileSync(path.join(__dirname, "..", "..", "package.json"), "utf8");
57
+ return JSON.parse(raw).version ?? "unknown";
58
+ }
59
+ catch {
60
+ return "unknown";
61
+ }
62
+ })();
13
63
  function ghHeaders() {
14
64
  const token = process.env.GITHUB_TOKEN;
15
65
  const h = {
16
66
  Accept: "application/vnd.github.v3+json",
17
67
  "X-GitHub-Api-Version": "2022-11-28",
18
- "User-Agent": "finch-mcp/3.28.0",
68
+ "User-Agent": `finch-mcp/${PKG_VERSION}`,
19
69
  };
20
70
  if (token)
21
71
  h.Authorization = `Bearer ${token}`;
@@ -116,7 +116,7 @@ async function handleInsiderTool(name, args) {
116
116
  return null;
117
117
  const parsed = Schema.safeParse(args);
118
118
  if (!parsed.success) {
119
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
119
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
120
120
  }
121
121
  const ticker = parsed.data.ticker.trim().toUpperCase();
122
122
  const limit = parsed.data.limit ?? 15;
@@ -451,7 +451,7 @@ async function handleInsightTool(name, args) {
451
451
  if (name === "market_thesis") {
452
452
  const parsed = MarketThesisSchema.safeParse(args);
453
453
  if (!parsed.success)
454
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
454
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
455
455
  const { token, context } = parsed.data;
456
456
  const priceData = await fetchVerifiedPrice(token);
457
457
  // Hard guard: without a verified live price, refuse to generate. Better
@@ -519,7 +519,7 @@ async function handleInsightTool(name, args) {
519
519
  if (name === "trade_plan") {
520
520
  const parsed = TradePlanSchema.safeParse(args);
521
521
  if (!parsed.success)
522
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
522
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
523
523
  const { token, side = "long", portfolioSize, riskTolerance = "moderate", timeframe } = parsed.data;
524
524
  const priceData = await fetchVerifiedPrice(token);
525
525
  // Hard guard: trade plans without verified live price = entry/SL/TP
@@ -293,7 +293,7 @@ async function handleMarketTool(name, args) {
293
293
  case "get_market_data": {
294
294
  const parsed = GetMarketDataSchema.safeParse(args ?? {});
295
295
  if (!parsed.success)
296
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
296
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
297
297
  const { token } = parsed.data;
298
298
  if (token) {
299
299
  const resolved = await resolveTokenId(token);
@@ -349,7 +349,7 @@ async function handleMarketTool(name, args) {
349
349
  case "get_token_data": {
350
350
  const parsed = GetTokenDataSchema.safeParse(args);
351
351
  if (!parsed.success)
352
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
352
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
353
353
  const q = parsed.data.question;
354
354
  // Try to extract a known symbol first, then fall back to search
355
355
  const upperQ = q.toUpperCase();
@@ -381,7 +381,7 @@ async function handleMarketTool(name, args) {
381
381
  case "compare_tokens": {
382
382
  const parsed = CompareTokensSchema.safeParse(args);
383
383
  if (!parsed.success)
384
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
384
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
385
385
  const syms = parsed.data.tokens.map(t => t.toUpperCase());
386
386
  const ids = syms.map(s => SYMBOL_TO_ID[s]).filter(Boolean);
387
387
  const unknown = syms.filter(s => !SYMBOL_TO_ID[s]);
@@ -449,7 +449,7 @@ async function handleMarketTool(name, args) {
449
449
  case "token_history": {
450
450
  const parsed = TokenHistorySchema.safeParse(args);
451
451
  if (!parsed.success)
452
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
452
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
453
453
  const days = parsed.data.days ?? 7;
454
454
  const resolved = await resolveTokenId(parsed.data.token);
455
455
  if (!resolved)
@@ -498,7 +498,7 @@ async function handleMarketTool(name, args) {
498
498
  case "get_base_token_data": {
499
499
  const parsed = GetBaseTokenDataSchema.safeParse(args);
500
500
  if (!parsed.success)
501
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
501
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
502
502
  const address = parsed.data.tokenAddress.toLowerCase();
503
503
  const [pair, coingeckoId] = await Promise.all([
504
504
  fetchDexscreenerBaseToken(address),
@@ -519,7 +519,7 @@ async function handleMemoryTool(name, args) {
519
519
  case "memory_add": {
520
520
  const parsed = AddSchema.safeParse(args);
521
521
  if (!parsed.success)
522
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
522
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
523
523
  const { content, title, tags, sourceUrl, force } = parsed.data;
524
524
  // `sourceUrl` is not stored, it is fetched and indexed β€” by the local
525
525
  // memory server on the user's own machine when local mode is on. An
@@ -580,7 +580,7 @@ async function handleMemoryTool(name, args) {
580
580
  case "memory_search": {
581
581
  const parsed = SearchSchema.safeParse(args);
582
582
  if (!parsed.success)
583
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
583
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
584
584
  const { query, limit = 10 } = parsed.data;
585
585
  // Over-fetch so post-decay ranking still has enough material.
586
586
  const overfetch = Math.min(50, Math.max(limit * 2, 20));
@@ -662,7 +662,7 @@ async function handleMemoryTool(name, args) {
662
662
  case "memory_context": {
663
663
  const parsed = ContextSchema.safeParse(args);
664
664
  if (!parsed.success)
665
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
665
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
666
666
  const { topic, limit = 8 } = parsed.data;
667
667
  // v3.25.1: use hybrid retrieval so context loading picks up exact-token
668
668
  // matches (env var names, model IDs, contract addresses) that semantic-
@@ -722,7 +722,7 @@ async function handleMemoryTool(name, args) {
722
722
  case "memory_list": {
723
723
  const parsed = ListSchema.safeParse(args ?? {});
724
724
  if (!parsed.success)
725
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
725
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
726
726
  const { limit = 20, tag } = parsed.data;
727
727
  const localList = (0, local_memory_js_1.getLocalMemoryConfig)();
728
728
  let results;
@@ -753,7 +753,7 @@ async function handleMemoryTool(name, args) {
753
753
  case "memory_delete": {
754
754
  const parsed = DeleteMemSchema.safeParse(args);
755
755
  if (!parsed.success)
756
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
756
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
757
757
  if (args?.confirm !== true) {
758
758
  return {
759
759
  content: [{
@@ -784,7 +784,7 @@ async function handleMemoryTool(name, args) {
784
784
  case "memory_insight": {
785
785
  const parsed = InsightSchema.safeParse(args);
786
786
  if (!parsed.success)
787
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
787
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
788
788
  const { topic, depth = "standard" } = parsed.data;
789
789
  const memLimit = depth === "deep" ? 15 : depth === "quick" ? 5 : 8;
790
790
  // v3.25.1: hybrid retrieval for memory side (was semantic-only) so the
@@ -872,7 +872,7 @@ async function handleMemoryTool(name, args) {
872
872
  case "memory_extract": {
873
873
  const parsed = ExtractSchema.safeParse(args);
874
874
  if (!parsed.success)
875
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
875
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
876
876
  const { text, facts, source = "extract" } = parsed.data;
877
877
  // ── PASS 1: hand the text back with the extraction rubric ───────────
878
878
  // Deciding what counts as a fact is judgement about *this user's*
@@ -929,7 +929,7 @@ async function handleMemoryTool(name, args) {
929
929
  case "memory_consolidate": {
930
930
  const parsed = ConsolidateSchema.safeParse(args);
931
931
  if (!parsed.success)
932
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
932
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
933
933
  const { topic, limit = 12, summary } = parsed.data;
934
934
  const localConsolidateCfg = (0, local_memory_js_1.getLocalMemoryConfig)();
935
935
  // ── PASS 2: save the caller's merged version ────────────────────────
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MIROSHARK_TOOLS = void 0;
4
4
  exports.handleMirosharkTool = handleMirosharkTool;
5
+ const config_js_1 = require("../config.js");
5
6
  const CONVEX_SITE = process.env.FINCH_CONVEX_URL ?? "https://befitting-porcupine-276.convex.site";
6
7
  exports.MIROSHARK_TOOLS = [
7
8
  {
@@ -50,7 +51,13 @@ exports.MIROSHARK_TOOLS = [
50
51
  ];
51
52
  // ── HTTP helpers ──────────────────────────────────────────────────────────────
52
53
  function authHeaders() {
53
- const key = process.env.FINCH_API_KEY ?? process.env.FINCH_SESSION_TOKEN;
54
+ // Bug fix: this used to read only process.env.FINCH_API_KEY/
55
+ // FINCH_SESSION_TOKEN directly, bypassing getSavedToken()'s env-var-then-
56
+ // saved-config fallback that every other tool goes through (via
57
+ // callConvex/callConvexRaw in convex.ts). A user authenticated via
58
+ // `finch login` (writes to ~/.finch/config.json, not an env var) got a
59
+ // silent, unauthenticated request here while every other tool worked.
60
+ const key = process.env.FINCH_API_KEY ?? (0, config_js_1.getSavedToken)();
54
61
  return key ? { Authorization: `Bearer ${key}` } : {};
55
62
  }
56
63
  async function miroJson(path, method, body, timeoutMs = 90000) {
@@ -106,7 +106,7 @@ async function handleMonitorTool(name, args) {
106
106
  if (name === "schedule_research") {
107
107
  const parsed = CreateSchema.safeParse(args);
108
108
  if (!parsed.success)
109
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
109
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
110
110
  const key = getKey();
111
111
  if (!key)
112
112
  return noKeyMsg();
@@ -284,7 +284,7 @@ async function handleMonitorTool(name, args) {
284
284
  if (name === "cancel_monitor") {
285
285
  const parsed = CancelSchema.safeParse(args);
286
286
  if (!parsed.success)
287
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
287
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
288
288
  if (args?.confirm !== true) {
289
289
  return {
290
290
  content: [{
package/dist/tools/os.js CHANGED
@@ -6,6 +6,7 @@ const convex_js_1 = require("../convex.js");
6
6
  const token_gate_js_1 = require("../token-gate.js");
7
7
  const wallet_js_1 = require("../wallet.js");
8
8
  const local_memory_js_1 = require("../local-memory.js");
9
+ const config_js_1 = require("../config.js");
9
10
  const CONVEX_SITE = process.env.FINCH_CONVEX_URL ?? "https://befitting-porcupine-276.convex.site";
10
11
  exports.OS_TOOLS = [
11
12
  {
@@ -199,13 +200,18 @@ async function handleOsTool(name, args) {
199
200
  if (!message)
200
201
  return { content: [{ type: "text", text: "Error: message is required" }] };
201
202
  try {
202
- const res = await fetch(`${CONVEX_SITE}/noel/shell/chat`, {
203
+ // Bug fix: this used to read only process.env.FINCH_SESSION_TOKEN/
204
+ // FINCH_API_KEY directly, bypassing getSavedToken()'s fallback to
205
+ // ~/.finch/config.json - a user authenticated via `finch login`
206
+ // (not an env var) silently sent an empty Authorization header here
207
+ // while every other tool (which goes through callConvex) worked.
208
+ const res = await fetch(`${CONVEX_SITE}/finch/shell/chat`, {
203
209
  method: "POST",
204
210
  headers: {
205
211
  "Content-Type": "application/json",
206
- "Authorization": `Bearer ${process.env.FINCH_SESSION_TOKEN ?? process.env.FINCH_API_KEY ?? ""}`,
212
+ "Authorization": `Bearer ${(0, config_js_1.getSavedToken)() ?? process.env.FINCH_API_KEY ?? ""}`,
207
213
  },
208
- body: JSON.stringify({ message, agentId: agent_id ?? "noel-default" }),
214
+ body: JSON.stringify({ message, agentId: agent_id ?? "finch-default" }),
209
215
  signal: AbortSignal.timeout(60000),
210
216
  });
211
217
  if (!res.ok) {
@@ -147,7 +147,7 @@ async function handleResearchChain(name, args) {
147
147
  return null;
148
148
  const parsed = InputSchema.safeParse(args);
149
149
  if (!parsed.success) {
150
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
150
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
151
151
  }
152
152
  const { startKey } = parsed.data;
153
153
  const maxDepth = parsed.data.maxDepth ?? 8;
@@ -154,7 +154,7 @@ async function handleResearchCompare(name, args) {
154
154
  return null;
155
155
  const parsed = InputSchema.safeParse(args);
156
156
  if (!parsed.success) {
157
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
157
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
158
158
  }
159
159
  const { keyA, keyB, focus, comparison } = parsed.data;
160
160
  const saveToVault = parsed.data.saveToVault ?? true;
@@ -100,7 +100,7 @@ async function handleResearchTool(name, args) {
100
100
  if (name === "web_scrape") {
101
101
  const parsed = ScrapeSchema.safeParse(args);
102
102
  if (!parsed.success)
103
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
103
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
104
104
  const { url, focus } = parsed.data;
105
105
  const unsafe = await (0, public_url_js_1.assertPublicUrl)(url);
106
106
  if (unsafe) {
@@ -120,7 +120,7 @@ async function handleResearchTool(name, args) {
120
120
  if (name === "web_search") {
121
121
  const parsed = SearchSchema.safeParse(args);
122
122
  if (!parsed.success)
123
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
123
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
124
124
  const { query, limit = 5 } = parsed.data;
125
125
  // Priority 1: user BYOK direct path. Priority 2: Finch proxy.
126
126
  // Same data shape so the formatting code below is identical for both.
@@ -94,7 +94,7 @@ async function handleBridgeTool(name, args) {
94
94
  return null;
95
95
  const parsed = Schema.safeParse(args);
96
96
  if (!parsed.success) {
97
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
97
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
98
98
  }
99
99
  const wanted = parsed.data.ticker?.trim().toUpperCase();
100
100
  const minGap = parsed.data.minGapPct ?? 0;
@@ -30,6 +30,20 @@ exports.RH_RPC = process.env.ROBINHOOD_RPC_URL ??
30
30
  process.env.RH_RPC_URL ??
31
31
  process.env.FINCH_RH_RPC_URL ??
32
32
  "https://rpc.mainnet.chain.robinhood.com";
33
+ /** Host only, for display in tool output - never the full RH_RPC. An
34
+ * operator can point ROBINHOOD_RPC_URL/RH_RPC_URL at a keyed provider
35
+ * (same pattern app/convex just fixed for its own Alchemy-key leak risk -
36
+ * a `.../v2/<key>`-style URL echoed verbatim into a tool response lands the
37
+ * key in chat transcripts/client logs). Falls back to the raw value only
38
+ * if it isn't a parseable URL, which should never happen in practice. */
39
+ function rhRpcDisplay() {
40
+ try {
41
+ return new URL(exports.RH_RPC).host;
42
+ }
43
+ catch {
44
+ return "(configured)";
45
+ }
46
+ }
33
47
  exports.RH_EXPLORER = "https://robinhoodchain.blockscout.com";
34
48
  const NATIVE_ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";
35
49
  const RH_PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
@@ -754,7 +768,7 @@ async function fetchRhBalances(address) {
754
768
  else
755
769
  for (const h of holdings)
756
770
  lines.push(`- ${h.symbol}: ${h.bal} \`${h.address}\``);
757
- lines.push(``, `_RPC: ${exports.RH_RPC}_`, `_Explorer: ${exports.RH_EXPLORER}/address/${address}_`);
771
+ lines.push(``, `_RPC: ${rhRpcDisplay()}_`, `_Explorer: ${exports.RH_EXPLORER}/address/${address}_`);
758
772
  return lines.join("\n");
759
773
  }
760
774
  // ─── Onchain safety scan (Blockscout + DexScreener; free, no LLM) ─────────────
@@ -1177,7 +1191,7 @@ async function handleRhMcpTool(name, args) {
1177
1191
  "",
1178
1192
  `**Wallet**: \`${addr ?? "(not configured β€” run finch login / first tool)"}\``,
1179
1193
  `**ETH on RH (gas)**: ${eth}`,
1180
- `**RPC**: ${exports.RH_RPC}`,
1194
+ `**RPC**: ${rhRpcDisplay()}`,
1181
1195
  `**Explorer**: ${exports.RH_EXPLORER}`,
1182
1196
  `**Catalog**: ${exports.RH_STOCKS.length} stocks (AAPL…USAR) + any RH-chain crypto via DexScreener`,
1183
1197
  "",
@@ -143,8 +143,11 @@ exports.RH_ORDER_TOOLS = [
143
143
  name: "rh_dca_create",
144
144
  description: "Robinhood Chain β€” create a DCA plan: buy a fixed ETH amount of a token every N hours, " +
145
145
  "up to a total number of buys, capped by maxSpendEth. Token by catalog symbol, crypto " +
146
- "ticker, or 0x contract address (resolved via DexScreener). Does NOT execute now β€” the " +
147
- "scheduler runs it via rh_orders_tick. Always confirm the resolved contract address first.",
146
+ "ticker, or 0x contract address (resolved via DexScreener). Does NOT execute now and does " +
147
+ "NOT run itself automatically β€” this only saves the plan. It fires only when something calls " +
148
+ "rh_orders_tick {execute:true}, which nothing does on its own. Tell the user to run " +
149
+ "`finch orders install-scheduler` (or `finch orders daemon`) after creating this, or the " +
150
+ "order will just sit there forever. Always confirm the resolved contract address first.",
148
151
  inputSchema: {
149
152
  type: "object",
150
153
  properties: {
@@ -165,9 +168,11 @@ exports.RH_ORDER_TOOLS = [
165
168
  {
166
169
  name: "rh_bracket_create",
167
170
  description: "Robinhood Chain β€” set take-profit and/or stop-loss on a token you HOLD. When DexScreener " +
168
- "price crosses tpPriceUsd (β‰₯) or slPriceUsd (≀), the scheduler sells sellPct% of your " +
169
- "current balance for ETH via rh_orders_tick. Provide at least one of tpPriceUsd / slPriceUsd. " +
170
- "Only ever sells tokens already in the wallet β€” never borrows or shorts.",
171
+ "price crosses tpPriceUsd (β‰₯) or slPriceUsd (≀), sellPct% of the current balance sells for " +
172
+ "ETH β€” but only when something calls rh_orders_tick {execute:true}, which nothing does on " +
173
+ "its own by default. Tell the user to run `finch orders install-scheduler` (or `finch orders " +
174
+ "daemon`) after creating this, or the trigger will never actually fire. Provide at least one " +
175
+ "of tpPriceUsd / slPriceUsd. Only ever sells tokens already in the wallet β€” never borrows or shorts.",
171
176
  inputSchema: {
172
177
  type: "object",
173
178
  properties: {
@@ -209,7 +214,9 @@ exports.RH_ORDER_TOOLS = [
209
214
  description: "Robinhood Chain β€” evaluate all active orders and act on any that are due (DCA interval " +
210
215
  "reached) or triggered (TP/SL price crossed). PREVIEW by default; pass execute:true to " +
211
216
  "broadcast real swaps. Refuses to execute when the kill-switch is set (RH_ORDERS_DISABLED=1 " +
212
- "or ~/.finch/rh-orders.OFF). This is the entry point an always-on scheduler calls.",
217
+ "or ~/.finch/rh-orders.OFF). Nothing calls this automatically β€” run `finch orders install-scheduler` " +
218
+ "(OS-level recurring task) or `finch orders daemon` (foreground loop) from a terminal to make " +
219
+ "orders actually fire unattended; calling this tool by hand only ticks once.",
213
220
  inputSchema: {
214
221
  type: "object",
215
222
  properties: {
@@ -345,7 +345,7 @@ async function handleScannerTool(name, args) {
345
345
  if (name === "score_token") {
346
346
  const parsed = ScoreTokenSchema.safeParse(args);
347
347
  if (!parsed.success)
348
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
348
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
349
349
  const { address, minLiquidity = DEFAULT_MIN_LIQ } = parsed.data;
350
350
  const data = await fetchJson(`https://api.dexscreener.com/latest/dex/tokens/${address}`);
351
351
  const pair = (data.pairs ?? [])
@@ -414,7 +414,7 @@ async function handleScannerTool(name, args) {
414
414
  if (name === "check_token") {
415
415
  const parsed = CheckTokenSchema.safeParse(args);
416
416
  if (!parsed.success)
417
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
417
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
418
418
  const { address } = parsed.data;
419
419
  // GoPlusLabs - chain 8453 = Base mainnet
420
420
  const data = await fetchJson(`https://api.gopluslabs.io/api/v1/token_security/8453?contract_addresses=${address}`);
@@ -476,7 +476,7 @@ async function handleScannerTool(name, args) {
476
476
  if (name === "scan_market") {
477
477
  const parsed = ScanDipsSchema.safeParse(args ?? {});
478
478
  if (!parsed.success)
479
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
479
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
480
480
  const input = args;
481
481
  const mode = input?.mode === "momentum" ? "momentum" : "dips";
482
482
  const { minScore = DEFAULT_MIN_SCORE, minLiquidity = DEFAULT_MIN_LIQ, limit = 40 } = parsed.data;