@finchagentic/mcp 4.1.0 → 4.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +19 -15
  2. package/dist/agent-loop.js +75 -72
  3. package/dist/annotations.js +23 -14
  4. package/dist/convex.js +15 -18
  5. package/dist/index.js +14 -12
  6. package/dist/llm.js +12 -1
  7. package/dist/local-memory-file.js +14 -1
  8. package/dist/local-memory.js +13 -0
  9. package/dist/output-schemas.js +71 -17
  10. package/dist/project.js +36 -0
  11. package/dist/resources.js +6 -11
  12. package/dist/server.js +35 -13
  13. package/dist/token-gate.js +2 -2
  14. package/dist/tool-filter.js +14 -5
  15. package/dist/tools/agents.js +106 -394
  16. package/dist/tools/automation.js +42 -6
  17. package/dist/tools/base-mcp.js +3 -15
  18. package/dist/tools/base.js +34 -20
  19. package/dist/tools/coder.js +1 -1
  20. package/dist/tools/deep-research.js +1 -1
  21. package/dist/tools/defi.js +14 -34
  22. package/dist/tools/equity.js +10 -2
  23. package/dist/tools/events.js +1 -1
  24. package/dist/tools/github.js +51 -1
  25. package/dist/tools/insider.js +1 -1
  26. package/dist/tools/insight.js +4 -4
  27. package/dist/tools/market.js +5 -5
  28. package/dist/tools/memory.js +52 -88
  29. package/dist/tools/miroshark.js +8 -1
  30. package/dist/tools/monitor.js +8 -8
  31. package/dist/tools/os.js +9 -4
  32. package/dist/tools/packets.js +2 -2
  33. package/dist/tools/research-chain.js +1 -1
  34. package/dist/tools/research-compare.js +1 -1
  35. package/dist/tools/research.js +2 -2
  36. package/dist/tools/rh-bridge.js +1 -1
  37. package/dist/tools/rh-mcp.js +29 -4
  38. package/dist/tools/rh-orders.js +201 -123
  39. package/dist/tools/scanner.js +33 -3
  40. package/dist/tools/stake.js +369 -0
  41. package/dist/tools/vault.js +294 -40
  42. package/dist/wallet.js +130 -19
  43. package/package.json +4 -5
  44. package/dist/tools/framework.js +0 -150
package/README.md CHANGED
@@ -29,13 +29,13 @@ Always pin the version. Never use `@latest`.
29
29
 
30
30
  ```bash
31
31
  # One-command installer (detects common MCP clients)
32
- npx -y -p @finchagentic/mcp@4.1.0 finch install
32
+ npx -y -p @finchagentic/mcp@4.4.1 finch install
33
33
  ```
34
34
 
35
35
  ### Claude Code
36
36
 
37
37
  ```bash
38
- claude mcp add finch -s user -- npx -y -p @finchagentic/mcp@4.1.0 finch-mcp
38
+ claude mcp add finch -s user -- npx -y -p @finchagentic/mcp@4.4.1 finch-mcp
39
39
  ```
40
40
 
41
41
  ### Cursor / Windsurf / Claude Desktop
@@ -45,7 +45,7 @@ claude mcp add finch -s user -- npx -y -p @finchagentic/mcp@4.1.0 finch-mcp
45
45
  "mcpServers": {
46
46
  "finch": {
47
47
  "command": "npx",
48
- "args": ["-y", "-p", "@finchagentic/mcp@4.1.0", "finch-mcp"]
48
+ "args": ["-y", "-p", "@finchagentic/mcp@4.4.1", "finch-mcp"]
49
49
  }
50
50
  }
51
51
  }
@@ -59,7 +59,7 @@ claude mcp add finch -s user -- npx -y -p @finchagentic/mcp@4.1.0 finch-mcp
59
59
  "finch": {
60
60
  "type": "stdio",
61
61
  "command": "npx",
62
- "args": ["-y", "-p", "@finchagentic/mcp@4.1.0", "finch-mcp"]
62
+ "args": ["-y", "-p", "@finchagentic/mcp@4.4.1", "finch-mcp"]
63
63
  }
64
64
  }
65
65
  }
@@ -79,7 +79,7 @@ claude mcp add finch -s user -- npx -y -p @finchagentic/mcp@4.1.0 finch-mcp
79
79
 
80
80
  </details>
81
81
 
82
- No LLM API key is required to start — 116 of 121 tools are plain reads/writes/on-chain calls that your MCP client's own model already drives; only 5 (`ask_finch`, `deep_research`, and scheduled agent learning) do their own multi-step reasoning server-side and need a key (see [Configuration](#configuration)). Tools load on first use.
82
+ No LLM API key is required to start — 111 of 116 tools are plain reads/writes/on-chain calls that your MCP client's own model already drives; only 5 (`ask_finch`, `deep_research`, and scheduled agent learning) do their own multi-step reasoning server-side and need a key (see [Configuration](#configuration)). Tools load on first use.
83
83
 
84
84
  ## Quick start
85
85
 
@@ -93,21 +93,25 @@ Try in your MCP client:
93
93
 
94
94
  ```
95
95
  remember: I prefer conservative DeFi strategies, max 5% risk
96
- spawn an agent called research-bot to track AI agent news weekly
96
+ spawn an agent called research-bot to track AI agent news, update it after each session
97
97
  save this thesis to vault
98
98
  ```
99
99
 
100
100
  ## What you get
101
101
 
102
- **121 tools** across four pillars:
102
+ **116 tools** across four pillars:
103
103
 
104
104
  | Pillar | What it does |
105
105
  |--------|----------------|
106
106
  | **Memory** | Full-text searchable memory + versioned vault + chronicle |
107
- | **Agents** | Spawn, recall, update, schedule named agents |
108
- | **Workflows** | Automations, monitors, packets, deep research |
107
+ | **Agents** | Spawn, recall, update named agents — `agent_recall` also pulls related memory/vault context matching the agent's goal, not just its own logged updates |
108
+ | **Workflows** | Automations, monitors, packets, deep research (auto-saves reports + auto-links related past research) |
109
109
  | **Execution** | Base DeFi, Robinhood Chain, market data, web, GitHub |
110
110
 
111
+ Coding and research sessions persist the same way: `deep_research` auto-saves its report to vault and links it to related past reports; `code_session_save` does the same for coding/debugging sessions (`vault_save type=code`, versioned per project, auto-linked). Both exist so the next session — yours or another agent's — starts with real context instead of cold.
112
+
113
+ `vault_save` and `agent_spawn` also take an optional `workspaceProject` - the same named Projects a user organizes their Agents/vault content into on the webapp's Agents page. Pass a name and it's matched case-insensitively or created automatically (`list_projects` to browse what exists first). Hosted vault only - local-vault mode has no project concept.
114
+
111
115
  Default palette is `core` (lighter context). Full set:
112
116
 
113
117
  ```json
@@ -119,7 +123,7 @@ Default palette is `core` (lighter context). Full set:
119
123
  Finch is the runtime. **Your LLM is the brain. Your data stays yours.**
120
124
 
121
125
  ```bash
122
- npx -y -p @finchagentic/mcp@4.1.0 finch setup
126
+ npx -y -p @finchagentic/mcp@4.4.1 finch setup
123
127
  # enable local vault (and optional local memory)
124
128
  ```
125
129
 
@@ -146,12 +150,12 @@ Scheduled/cloud features still need an account. Core memory, vault, and public-d
146
150
  | `GITHUB_TOKEN` | For `github_search_code` |
147
151
  | `ALCHEMY_API_KEY` | Faster Base RPC (optional) |
148
152
 
149
- **Cost model:** almost everything is free to run — the other 116 tools are plain API/RPC calls, and your MCP client's own model (Claude, GPT, whatever's driving the chat) does all the tool-selection reasoning at no cost to Finch. The 5 exceptions above need their own key because their reasoning happens *inside* the tool call, invisible to your client, and can't be delegated to it. Set exactly one of the four env vars and every tool that needs it will use it automatically.
153
+ **Cost model:** almost everything is free to run — the other 110 tools are plain API/RPC calls, and your MCP client's own model (Claude, GPT, whatever's driving the chat) does all the tool-selection reasoning at no cost to Finch. The 5 exceptions above need their own key because their reasoning happens *inside* the tool call, invisible to your client, and can't be delegated to it. Set exactly one of the four env vars and every tool that needs it will use it automatically.
150
154
 
151
155
  Guided setup:
152
156
 
153
157
  ```bash
154
- npx -y -p @finchagentic/mcp@4.1.0 finch setup
158
+ npx -y -p @finchagentic/mcp@4.4.1 finch setup
155
159
  ```
156
160
 
157
161
  ## Security
@@ -160,12 +164,12 @@ npx -y -p @finchagentic/mcp@4.1.0 finch setup
160
164
  |:-:|----------|------|
161
165
  | 1 | Prompt injection | External content is data only — never instructions |
162
166
  | 2 | Mainnet confirm | Estimate → preview → confirm → execute |
163
- | 3 | Pinned install | Always `@finchagentic/mcp@4.1.0`, never `@latest` |
167
+ | 3 | Pinned install | Always `@finchagentic/mcp@4.4.1`, never `@latest` |
164
168
  | 4 | Credential vault | Never paste secrets into prompts or third-party tools |
165
169
  | 5 | Data disclosure | Know what leaves the machine (LLM, Firecrawl, GitHub, chain RPCs) |
166
170
  | 6 | Server monitors | Scheduled jobs need explicit confirmation |
167
- | 7 | Agent schedules | `agent_schedule` confirms cost + side effects |
168
- | 8 | Identity custody | Do not send assets to agent identity addresses |
171
+ | 7 | Fund-moving confirm | `stake_finch`/`unstake_finch`/`base_mcp_send`/`base_mcp_swap`/`rh_mcp_swap` all require `confirm: true` |
172
+ | 8 | Local wallet encryption | Set `FINCH_WALLET_PASSPHRASE` for a portable, passphrase-derived key — without it, the key derives from a random per-install secret + machine info, so the wallet file alone (without that secret file) isn't enough to decrypt it |
169
173
 
170
174
  ## Troubleshooting
171
175
 
@@ -32,6 +32,7 @@ async function runAgent(userMessage, history, onToolCall) {
32
32
  const bankrKey = process.env.BANKR_API_KEY;
33
33
  const anthropicKey = process.env.ANTHROPIC_API_KEY;
34
34
  const openaiKey = process.env.OPENAI_API_KEY;
35
+ const grokKey = process.env.GROK_API_KEY;
35
36
  // Explicit override - lets FINCH_PROVIDER=openai win even when BANKR_API_KEY
36
37
  // is also set (e.g. as a persistent shell env var), same as llm.ts's callLLM.
37
38
  if (provider === "bankr" && bankrKey)
@@ -40,13 +41,22 @@ async function runAgent(userMessage, history, onToolCall) {
40
41
  return runAnthropicLoop(anthropicKey, userMessage, history, onToolCall);
41
42
  if (provider === "openai" && openaiKey)
42
43
  return runOpenAILoop(openaiKey, userMessage, history, onToolCall);
43
- // Auto-priority
44
+ if (provider === "grok" && grokKey)
45
+ return runGrokLoop(grokKey, userMessage, history, onToolCall);
46
+ // Auto-priority - matches llm.ts's callLLM() order exactly (bankr →
47
+ // anthropic → openai → grok → Convex proxy) so a one-shot LLM tool call
48
+ // (ask_finch, memory extraction, etc.) and the interactive agent loop
49
+ // never silently pick different providers for the same configured keys.
50
+ // A GROK_API_KEY-only user used to fall all the way through to the
51
+ // Convex-proxied Anthropic loop here despite callLLM() using Grok.
44
52
  if (bankrKey)
45
53
  return runBankrLoop(bankrKey, userMessage, history, onToolCall);
46
54
  if (anthropicKey)
47
55
  return runAnthropicLoop(anthropicKey, userMessage, history, onToolCall);
48
56
  if (openaiKey)
49
57
  return runOpenAILoop(openaiKey, userMessage, history, onToolCall);
58
+ if (grokKey)
59
+ return runGrokLoop(grokKey, userMessage, history, onToolCall);
50
60
  // No direct key - proxy through Finch backend. Wallet auto-creates at ~/.finch/wallet.json
51
61
  // on first use and signs requests transparently. No account or config needed.
52
62
  try {
@@ -66,8 +76,14 @@ function toAnthropicTool(tool) {
66
76
  input_schema: tool.inputSchema ?? { type: "object", properties: {} },
67
77
  };
68
78
  }
69
- async function runAnthropicLoop(apiKey, userMessage, history, onToolCall) {
70
- const model = process.env.FINCH_MODEL ?? process.env.ANTHROPIC_MODEL ?? "claude-haiku-4-5-20251001";
79
+ // Shared loop body for any provider that speaks Anthropic's native
80
+ // content-block format (tool_use/tool_result) - runAnthropicLoop (direct
81
+ // api.anthropic.com) and runConvexProxiedLoop (same wire format via Finch's
82
+ // /llm/complete proxy) previously duplicated this ~70-line body verbatim,
83
+ // with only the transport (sendTurn/sendFinal) differing. Keeping one copy
84
+ // means a future fix (retry-on-5xx, tool-result truncation, etc.) can't be
85
+ // applied to one provider and silently miss the other.
86
+ async function runAnthropicStyleLoop(sendTurn, sendFinal, userMessage, history, onToolCall) {
71
87
  const tools = server_js_1.ALL_TOOLS.map(toAnthropicTool);
72
88
  const toolCalls = [];
73
89
  const messages = [
@@ -75,21 +91,7 @@ async function runAnthropicLoop(apiKey, userMessage, history, onToolCall) {
75
91
  { role: "user", content: userMessage },
76
92
  ];
77
93
  for (let turn = 0; turn < 10; turn++) {
78
- const res = await fetch("https://api.anthropic.com/v1/messages", {
79
- method: "POST",
80
- headers: {
81
- "Content-Type": "application/json",
82
- "x-api-key": apiKey,
83
- "anthropic-version": "2023-06-01",
84
- },
85
- body: JSON.stringify({ model, max_tokens: 4096, system: SYSTEM_PROMPT, tools, messages }),
86
- signal: AbortSignal.timeout(90000),
87
- });
88
- if (!res.ok) {
89
- const body = await res.text().catch(() => "");
90
- throw new Error(`Anthropic ${res.status}: ${body.slice(0, 300)}`);
91
- }
92
- const data = await res.json();
94
+ const data = await sendTurn(messages, tools);
93
95
  messages.push({ role: "assistant", content: data.content });
94
96
  if (data.stop_reason !== "tool_use") {
95
97
  const text = data.content
@@ -103,15 +105,27 @@ async function runAnthropicLoop(apiKey, userMessage, history, onToolCall) {
103
105
  for (const block of data.content) {
104
106
  if (block.type !== "tool_use")
105
107
  continue;
106
- onToolCall(block.name);
107
- toolCalls.push({ name: block.name });
108
108
  let resultText;
109
109
  try {
110
110
  const handler = server_js_1.HANDLER_MAP.get(block.name);
111
111
  if (!handler)
112
112
  throw new Error(`Unknown tool: ${block.name}`);
113
+ // Recorded only once the handler is confirmed to exist - an "Unknown
114
+ // tool" miss is a model error, not a real tool execution, and callers
115
+ // reading AgentResult.toolCalls should be able to trust every entry
116
+ // actually ran.
117
+ onToolCall(block.name);
118
+ toolCalls.push({ name: block.name });
113
119
  const result = await handler(block.name, block.input ?? {});
114
- resultText = result?.content?.[0]?.text ?? "Done.";
120
+ // A handler returning null (a name declared in its *_TOOLS array with
121
+ // no matching branch - a latent bug ruled out today but not
122
+ // structurally prevented) must not fall through to "Done." - that
123
+ // would report success to the model for a call that never actually
124
+ // ran, directly contradicting this loop's own system-prompt rule
125
+ // against claiming an unverified result.
126
+ if (result === null)
127
+ throw new Error(`Tool ${block.name} returned no result (handler bug - not executed)`);
128
+ resultText = result.content?.[0]?.text ?? "Done.";
115
129
  }
116
130
  catch (err) {
117
131
  resultText = `Error: ${err.message}`;
@@ -123,7 +137,28 @@ async function runAnthropicLoop(apiKey, userMessage, history, onToolCall) {
123
137
  // Hit the turn cap without a final answer - rather than hand back nothing,
124
138
  // force one more call with no tools available so the model has to
125
139
  // synthesize a real answer from whatever it already gathered.
126
- return finishWithoutTools(async (msgs) => {
140
+ return finishWithoutTools(sendFinal, messages, toolCalls);
141
+ }
142
+ function runAnthropicLoop(apiKey, userMessage, history, onToolCall) {
143
+ const model = process.env.FINCH_MODEL ?? process.env.ANTHROPIC_MODEL ?? "claude-haiku-4-5-20251001";
144
+ const sendTurn = async (messages, tools) => {
145
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
146
+ method: "POST",
147
+ headers: {
148
+ "Content-Type": "application/json",
149
+ "x-api-key": apiKey,
150
+ "anthropic-version": "2023-06-01",
151
+ },
152
+ body: JSON.stringify({ model, max_tokens: 4096, system: SYSTEM_PROMPT, tools, messages }),
153
+ signal: AbortSignal.timeout(90000),
154
+ });
155
+ if (!res.ok) {
156
+ const body = await res.text().catch(() => "");
157
+ throw new Error(`Anthropic ${res.status}: ${body.slice(0, 300)}`);
158
+ }
159
+ return await res.json();
160
+ };
161
+ const sendFinal = async (msgs) => {
127
162
  const res = await fetch("https://api.anthropic.com/v1/messages", {
128
163
  method: "POST",
129
164
  headers: { "Content-Type": "application/json", "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
@@ -134,7 +169,8 @@ async function runAnthropicLoop(apiKey, userMessage, history, onToolCall) {
134
169
  return "";
135
170
  const data = await res.json();
136
171
  return (data.content ?? []).filter(b => b.type === "text").map(b => b.text).join("");
137
- }, messages, toolCalls);
172
+ };
173
+ return runAnthropicStyleLoop(sendTurn, sendFinal, userMessage, history, onToolCall);
138
174
  }
139
175
  // Shared turn-cap fallback: rather than "Reached max tool iterations." with
140
176
  // nothing useful, ask the model to synthesize a real answer from whatever
@@ -157,58 +193,17 @@ async function finishWithoutTools(call, messages, toolCalls) {
157
193
  };
158
194
  }
159
195
  // ── Convex-proxied Anthropic loop (session token only - platform covers LLM) ──
160
- async function runConvexProxiedLoop(userMessage, history, onToolCall) {
196
+ function runConvexProxiedLoop(userMessage, history, onToolCall) {
161
197
  const model = process.env.FINCH_MODEL ?? process.env.ANTHROPIC_MODEL ?? "claude-haiku-4-5-20251001";
162
- const tools = server_js_1.ALL_TOOLS.map(toAnthropicTool);
163
- const toolCalls = [];
164
- const messages = [
165
- ...history.map(h => ({ role: h.role, content: h.content })),
166
- { role: "user", content: userMessage },
167
- ];
168
- for (let turn = 0; turn < 10; turn++) {
169
- // callConvex handles wallet/session auth automatically; 90s timeout matches the proxy endpoint
170
- const data = await (0, convex_js_1.callConvex)("/llm/complete", "POST", {
171
- model,
172
- max_tokens: 4096,
173
- system: SYSTEM_PROMPT,
174
- tools,
175
- messages,
176
- }, "llm_complete", 90000);
177
- messages.push({ role: "assistant", content: data.content });
178
- if (data.stop_reason !== "tool_use") {
179
- const text = data.content
180
- .filter(b => b.type === "text")
181
- .map(b => b.text)
182
- .join("");
183
- return { text, toolCalls };
184
- }
185
- const toolResults = [];
186
- for (const block of data.content) {
187
- if (block.type !== "tool_use")
188
- continue;
189
- onToolCall(block.name);
190
- toolCalls.push({ name: block.name });
191
- let resultText;
192
- try {
193
- const handler = server_js_1.HANDLER_MAP.get(block.name);
194
- if (!handler)
195
- throw new Error(`Unknown tool: ${block.name}`);
196
- const result = await handler(block.name, block.input ?? {});
197
- resultText = result?.content?.[0]?.text ?? "Done.";
198
- }
199
- catch (err) {
200
- resultText = `Error: ${err.message}`;
201
- }
202
- toolResults.push({ type: "tool_result", tool_use_id: block.id, content: resultText });
203
- }
204
- messages.push({ role: "user", content: toolResults });
205
- }
206
- return finishWithoutTools(async (msgs) => {
198
+ // callConvex handles wallet/session auth automatically; 90s timeout matches the proxy endpoint
199
+ const sendTurn = (messages, tools) => (0, convex_js_1.callConvex)("/llm/complete", "POST", { model, max_tokens: 4096, system: SYSTEM_PROMPT, tools, messages }, "llm_complete", 90000);
200
+ const sendFinal = async (msgs) => {
207
201
  const data = await (0, convex_js_1.callConvex)("/llm/complete", "POST", {
208
202
  model, max_tokens: 4096, system: SYSTEM_PROMPT, messages: msgs,
209
203
  }, "llm_complete", 90000);
210
204
  return (data.content ?? []).filter((b) => b.type === "text").map((b) => b.text).join("");
211
- }, messages, toolCalls);
205
+ };
206
+ return runAnthropicStyleLoop(sendTurn, sendFinal, userMessage, history, onToolCall);
212
207
  }
213
208
  // ── Bankr (OpenAI-compatible) agent loop ─────────────────────────────────────
214
209
  function toBankrTool(tool) {
@@ -252,14 +247,16 @@ async function runOpenAICompatibleLoop(url, authHeaders, model, providerLabel, u
252
247
  return { text: choice.content ?? "", toolCalls };
253
248
  }
254
249
  for (const call of choice.tool_calls) {
255
- onToolCall(call.function.name);
256
- toolCalls.push({ name: call.function.name });
257
250
  let resultText;
258
251
  try {
259
252
  const args = JSON.parse(call.function.arguments ?? "{}");
260
253
  const handler = server_js_1.HANDLER_MAP.get(call.function.name);
261
254
  if (!handler)
262
255
  throw new Error(`Unknown tool: ${call.function.name}`);
256
+ // Recorded only once the handler is confirmed to exist - see the
257
+ // matching comment in runAnthropicLoop/runConvexProxiedLoop.
258
+ onToolCall(call.function.name);
259
+ toolCalls.push({ name: call.function.name });
263
260
  const result = await handler(call.function.name, args);
264
261
  resultText = result?.content?.[0]?.text ?? "Done.";
265
262
  }
@@ -296,3 +293,9 @@ async function runOpenAILoop(apiKey, userMessage, history, onToolCall) {
296
293
  const model = process.env.FINCH_MODEL ?? process.env.OPENAI_MODEL ?? "gpt-4o-mini";
297
294
  return runOpenAICompatibleLoop(openAiChatUrl(), { Authorization: `Bearer ${apiKey}` }, model, "OpenAI", userMessage, history, onToolCall);
298
295
  }
296
+ // xAI's Chat Completions API is OpenAI-compatible (same as llm.ts's callGrok),
297
+ // so this reuses runOpenAICompatibleLoop rather than a bespoke loop.
298
+ async function runGrokLoop(apiKey, userMessage, history, onToolCall) {
299
+ const model = process.env.FINCH_MODEL ?? process.env.FINCH_GROK_MODEL ?? process.env.GROK_MODEL ?? "grok-4-fast-reasoning";
300
+ return runOpenAICompatibleLoop("https://api.x.ai/v1/chat/completions", { Authorization: `Bearer ${apiKey}` }, model, "Grok", userMessage, history, onToolCall);
301
+ }
@@ -14,32 +14,23 @@ const READ_ONLY_LOCAL = new Set([
14
14
  // Toggles and upserts: re-running with the same args lands in the same state.
15
15
  const WRITE_IDEMPOTENT = new Set([
16
16
  "agent_update",
17
- "agent_schedule",
18
- "agent_pause",
19
- "agent_resume",
20
- "agent_unschedule",
21
17
  "pause_automation",
18
+ "stake_auto_restake",
22
19
  "vault_link",
23
20
  "vault_pin",
24
21
  "vault_tag",
25
22
  "vault_unpublish",
26
- "wallet_sign_message", // signing is not itself a fund move; re-sign = same sig
27
23
  ]);
28
24
  // readOnly=false, destructive=false.
29
25
  // Additive writes / new resources: they create or append, they don't destroy.
30
- // (hire_agent is deliberately NOT here: it only returns a specialist persona
31
- // scoped to the caller's task - it reads, it does not write - so it stays in
32
- // the read-only default.)
33
26
  const WRITE = new Set([
34
27
  "agent_spawn",
35
28
  "chronicle_add",
36
- "create_automation",
37
- "create_monitor",
29
+ "code_session_save",
38
30
  "memory_add",
39
31
  "memory_extract",
40
32
  "memory_consolidate",
41
33
  "packet_create",
42
- "packet_share",
43
34
  "schedule_research",
44
35
  "vault_save",
45
36
  "vault_store_credential",
@@ -58,20 +49,38 @@ const DESTRUCTIVE = new Set([
58
49
  "rh_order_cancel",
59
50
  "vault_delete",
60
51
  // irreversible public exposure
61
- "memory_publish", // "IRREVERSIBLE, PUBLIC" per its own description
52
+ "packet_share", // "copies already taken remain" per its own description
62
53
  // money movement (Base)
54
+ // NOTE: base_mcp_lend deliberately excluded - per its own description it
55
+ // "Returns deposit INSTRUCTIONS only... Does NOT broadcast" - it's a read,
56
+ // not a fund move, so it falls through to the read-only default below.
63
57
  "base_mcp_send",
64
58
  "base_mcp_swap",
65
- "base_mcp_lend",
59
+ // off-chain signature that can itself authorise value movement (order,
60
+ // session login) without any on-chain tx - same risk class as a real
61
+ // transfer per its own tool description, so it belongs here rather than
62
+ // in WRITE_IDEMPOTENT ("re-sign = same sig" is true but undersells the risk)
63
+ "wallet_sign_message",
66
64
  // money movement (Robinhood Chain)
67
65
  "rh_mcp_swap",
68
66
  "rh_dca_create", // arms recurring real buys
69
67
  "rh_bracket_create", // arms real TP/SL sells
68
+ // a swap/send automation arms unattended, REPEATING real fund movement
69
+ // (the backend's 1-minute cron evaluator fires it) - same risk class as
70
+ // rh_dca_create/rh_bracket_create above, not a plain additive write. An
71
+ // alert-only automation doesn't move funds, but the tool can't tell which
72
+ // kind it's about to create until AFTER the backend parses rawInput, so
73
+ // it's classified by its worst case, same reasoning as rh_orders_tick.
74
+ "create_automation",
70
75
  "rh_orders_tick", // preview by default, but can execute:true and move funds
71
76
  // executors that run other (possibly fund-moving) tools
72
77
  "run_automation",
73
- "run_playbook",
74
78
  "packet_run",
79
+ // money movement (FINCH staking, custodial wallet) - stake locks real value
80
+ // for a fixed period; unstake moves it (plus rewards) back
81
+ "stake_finch",
82
+ "unstake_finch",
83
+ "claim_vested_rewards", // treasury -> custodial wallet USDG transfer
75
84
  ]);
76
85
  function annotationsFor(name) {
77
86
  if (DESTRUCTIVE.has(name)) {
package/dist/convex.js CHANGED
@@ -4,7 +4,6 @@ exports.PaymentRequiredError = exports.CONVEX_SITE = void 0;
4
4
  exports.buildPaymentHeader = buildPaymentHeader;
5
5
  exports.callConvex = callConvex;
6
6
  exports.callConvexRaw = callConvexRaw;
7
- exports.notifyTelegram = notifyTelegram;
8
7
  const wallet_js_1 = require("./wallet.js");
9
8
  const config_js_1 = require("./config.js");
10
9
  exports.CONVEX_SITE = process.env.FINCH_CONVEX_URL ?? "https://befitting-porcupine-276.convex.site";
@@ -66,10 +65,6 @@ async function callConvex(path, method, body, toolName = "unknown", timeoutMs =
66
65
  headers["X-User-Grok-Key"] = process.env.GROK_API_KEY;
67
66
  if (process.env.BANKR_API_KEY)
68
67
  headers["X-User-Bankr-Key"] = process.env.BANKR_API_KEY;
69
- if (process.env.TELEGRAM_BOT_TOKEN)
70
- headers["X-User-Telegram-Token"] = process.env.TELEGRAM_BOT_TOKEN;
71
- if (process.env.TELEGRAM_CHAT_ID)
72
- headers["X-User-Telegram-Chat"] = process.env.TELEGRAM_CHAT_ID;
73
68
  let lastError = null;
74
69
  for (let attempt = 0; attempt < RETRY_DELAYS.length; attempt++) {
75
70
  if (attempt > 0) {
@@ -95,7 +90,13 @@ async function callConvex(path, method, body, toolName = "unknown", timeoutMs =
95
90
  `${b.alternative ? `Alternative: ${b.alternative}` : ""}`);
96
91
  }
97
92
  if (RETRY_STATUSES.has(res.status) && attempt < RETRY_DELAYS.length) {
98
- lastError = new Error(`Finch API error: ${res.status}`);
93
+ // Capture the actual body so a deterministic error (e.g. "unknown
94
+ // token") that happens to come back on a 500 still surfaces its real
95
+ // message if retries exhaust - previously this discarded the body
96
+ // entirely and threw a bare "Finch API error: 500", hiding exactly the
97
+ // information the caller needed to fix the request.
98
+ const bodyText = await res.text().catch(() => "");
99
+ lastError = new Error(`Finch API error ${res.status}: ${bodyText.slice(0, 300) || "(no body)"}`);
99
100
  continue;
100
101
  }
101
102
  if (!res.ok)
@@ -112,10 +113,14 @@ async function callConvexRaw(path, toolName = "unknown", timeoutMs = 60000) {
112
113
  const headers = {};
113
114
  const apiKey = process.env.FINCH_API_KEY;
114
115
  const sessionToken = (0, config_js_1.getSavedToken)();
115
- const authHeader = apiKey
116
- ? `Bearer ${apiKey}`
117
- : sessionToken
118
- ? `Bearer ${sessionToken}`
116
+ // Same precedence as callConvex() above - prefer session token over API
117
+ // key. These two functions previously disagreed (this one checked apiKey
118
+ // first), so the same env/config could pick a different credential
119
+ // depending on which helper a tool happened to call.
120
+ const authHeader = sessionToken
121
+ ? `Bearer ${sessionToken}`
122
+ : apiKey
123
+ ? `Bearer ${apiKey}`
119
124
  : null;
120
125
  if (authHeader) {
121
126
  headers["Authorization"] = authHeader;
@@ -141,11 +146,3 @@ async function callConvexRaw(path, toolName = "unknown", timeoutMs = 60000) {
141
146
  throw new Error(`Finch API error: ${res.status}`);
142
147
  return res.text();
143
148
  }
144
- async function notifyTelegram(userId, message) {
145
- try {
146
- return await callConvex("/user/telegram/notify", "POST", { userId, message }, "set_telegram");
147
- }
148
- catch (error) {
149
- return { sent: false, reason: error.message ?? String(error) };
150
- }
151
- }
package/dist/index.js CHANGED
@@ -66,14 +66,17 @@ const C = {
66
66
  reset: "\x1b[0m",
67
67
  bold: "\x1b[1m",
68
68
  };
69
+ // Was still spelling out the pre-rebrand "NOELCLAW" wordmark in ASCII art -
70
+ // leftover from before the Finch rebrand, never caught because nothing
71
+ // visually diffs a banner string.
69
72
  const BANNER = `
70
73
  ${C.cyan}
71
- ███╗ ██╗ ██████╗ ███████╗██╗ ██████╗██╗ █████╗ ██╗ ██╗
72
- ████╗ ██║██╔═══██╗██╔════╝██║ ██╔════╝██║ ██╔══██╗██║ ██║
73
- ██╔██╗ ██║██║ ██║█████╗ ██║ ██║ ██║ ███████║██║ █╗ ██║
74
- ██║╚██╗██║██║ ██║██╔══╝ ██║ ██║ ██║ ██╔══██║██║███╗██║
75
- ██║ ╚████║╚██████╔╝███████╗███████╗╚██████╗███████╗██║ ██║╚███╔███╔╝
76
- ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝
74
+ ███████╗██╗███╗ ██╗ ██████╗██╗ ██╗
75
+ ██╔════╝██║████╗ ██║██╔════╝██║ ██║
76
+ █████╗ ██║██╔██╗ ██║██║ ███████║
77
+ ██╔══╝ ██║██║╚██╗██║██║ ██╔══██║
78
+ ██║ ██║██║ ╚████║╚██████╗██║ ██║
79
+ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝╚═╝ ╚═╝
77
80
  ${C.reset}`;
78
81
  function line(label, value, color = C.cyan) {
79
82
  const pad = " ".repeat(Math.max(0, 12 - label.length));
@@ -108,24 +111,23 @@ async function main() {
108
111
  (0, config_js_1.hydrateEnvFromConfig)();
109
112
  process.stderr.write(BANNER);
110
113
  const CAT_RULES = [
111
- { label: "Market", match: n => /^(get_market_data|get_token_data|compare_tokens|market_overview|token_history)$/.test(n) },
114
+ { label: "Market", match: n => /^(get_market_data|get_token_data|compare_tokens|market_overview|token_history|get_base_token_data|stock_fundamentals|stock_insider|stock_events)$/.test(n) },
112
115
  { label: "Insight", match: n => /^(ask_finch|market_thesis|trade_plan)$/.test(n) },
113
116
  { label: "DeFi", match: n => n === "get_defi_yields" },
114
117
  { label: "Base MCP", match: n => n.startsWith("base_mcp_") },
115
118
  { label: "RH MCP", match: n => n.startsWith("rh_") },
116
119
  { label: "Automation", match: n => /^(create_automation|list_automations|pause_automation|delete_automation|get_automation_runs|run_automation)$/.test(n) },
117
- { label: "Framework", match: n => /^(list_playbooks|run_playbook|get_finch_ledger)$/.test(n) },
118
- { label: "Vault", match: n => n.startsWith("vault_") },
120
+ { label: "Vault", match: n => n.startsWith("vault_") || n === "code_session_save" || n === "list_projects" },
119
121
  { label: "Wallet", match: n => /^(get_wallet_address|get_wallet_balance|wallet_sign_message)$/.test(n) },
122
+ { label: "Staking", match: n => /^(stake_finch|unstake_finch|stake_finch_status|stake_auto_restake|claim_vested_rewards)$/.test(n) },
120
123
  { label: "MiroShark", match: n => n.startsWith("miroshark_") },
121
124
  { label: "Scanner", match: n => /^(scan_market|score_token|check_token)$/.test(n) },
122
- { label: "Agents", match: n => n.startsWith("agent_") || n === "list_agents" || n === "hire_agent" },
125
+ { label: "Agents", match: n => n.startsWith("agent_") },
123
126
  { label: "Coder", match: n => n === "audit_contract" },
124
- { label: "Base", match: n => /^(query_vaults|list_markets|prepare_deposit|chain_stats)$/.test(n) },
125
127
  { label: "Memory", match: n => n.startsWith("memory_") },
126
128
  { label: "OS", match: n => /^(finch_status|finch_diagnostics|finch_shell_chat)$/.test(n) },
127
129
  { label: "Research", match: n => /^(web_scrape|web_search|deep_research|research_compare|research_chain)$/.test(n) },
128
- { label: "Monitor", match: n => /^(schedule_research|create_monitor|list_monitors|cancel_monitor)$/.test(n) },
130
+ { label: "Monitor", match: n => /^(schedule_research|list_monitors|cancel_monitor)$/.test(n) },
129
131
  { label: "GitHub", match: n => n.startsWith("github_") },
130
132
  { label: "Chronicle", match: n => n.startsWith("chronicle_") },
131
133
  { label: "Packets", match: n => n.startsWith("packet_") },
package/dist/llm.js CHANGED
@@ -227,7 +227,18 @@ async function callOpenAI(apiKey, systemPrompt, userPrompt, maxTokens, history,
227
227
  throw new Error(`OpenAI error ${res.status}: ${body.slice(0, 200)}`);
228
228
  }
229
229
  const data = await res.json();
230
- return data.choices?.[0]?.message?.content ?? "";
230
+ // Most OpenAI-compatible gateways put `choices` at the top level, but
231
+ // OPENAI_BASE_URL can point at anything that speaks this API shape - at
232
+ // least one (9Router) wraps the whole payload in `{ data: {...}, success:
233
+ // true }` for some models (confirmed live: routers9/glm5.2 returns this
234
+ // wrapper while routers9/tencent/hy3 on the SAME endpoint/key returns
235
+ // top-level choices). Without this fallback the call silently returns ""
236
+ // instead of throwing - worse than an error, since every caller of
237
+ // callLLM() just gets an empty synthesis with no indication anything failed.
238
+ const content = data.choices?.[0]?.message?.content ?? data.data?.choices?.[0]?.message?.content;
239
+ if (!content)
240
+ throw new Error(`OpenAI-compatible endpoint returned no content (model: ${model})`);
241
+ return content;
231
242
  }
232
243
  async function callGrok(apiKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, liveSearch, modelOverride) {
233
244
  const model = modelOverride ?? process.env.FINCH_MODEL ?? process.env.GROK_MODEL ?? "grok-4-fast-reasoning";
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.getLocalMemoryFileConfig = getLocalMemoryFileConfig;
37
37
  exports.fileMemoryAdd = fileMemoryAdd;
38
+ exports.fileMemoryDeleteByVaultKey = fileMemoryDeleteByVaultKey;
38
39
  exports.fileMemorySearch = fileMemorySearch;
39
40
  exports.fileMemoryList = fileMemoryList;
40
41
  exports.fileMemoryDelete = fileMemoryDelete;
@@ -71,7 +72,7 @@ function toResult(m) {
71
72
  return {
72
73
  id: m.id,
73
74
  content: m.content,
74
- metadata: { title: m.title, tags: m.tags, source: m.source, sourceUrl: m.sourceUrl, contentHash: m.contentHash, pinned: m.pinned, addedAt: m.addedAt },
75
+ metadata: { title: m.title, tags: m.tags, source: m.source, sourceUrl: m.sourceUrl, contentHash: m.contentHash, pinned: m.pinned, addedAt: m.addedAt, vaultKey: m.vaultKey },
75
76
  };
76
77
  }
77
78
  function fileMemoryAdd(cfg, content, metadata, sourceUrl) {
@@ -87,11 +88,23 @@ function fileMemoryAdd(cfg, content, metadata, sourceUrl) {
87
88
  contentHash: metadata.contentHash ?? "",
88
89
  pinned: metadata.pinned,
89
90
  addedAt: metadata.addedAt ?? Date.now(),
91
+ vaultKey: metadata.vaultKey,
90
92
  };
91
93
  idx.memories.push(row);
92
94
  writeIndex(cfg, idx);
93
95
  return { id };
94
96
  }
97
+ /** Remove every memory row mirroring a given vault entry - called by
98
+ * vault_delete so its "PERMANENT... cannot be undone" claim is actually true,
99
+ * instead of leaving the content fully recoverable via memory_search. */
100
+ function fileMemoryDeleteByVaultKey(cfg, vaultKey) {
101
+ const idx = readIndex(cfg);
102
+ const next = idx.memories.filter((m) => m.vaultKey !== vaultKey);
103
+ const removed = idx.memories.length - next.length;
104
+ if (removed > 0)
105
+ writeIndex(cfg, { memories: next });
106
+ return removed;
107
+ }
95
108
  function fileMemorySearch(cfg, query, limit) {
96
109
  const idx = readIndex(cfg);
97
110
  const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
@@ -6,6 +6,7 @@ exports.localMemoryAdd = localMemoryAdd;
6
6
  exports.localMemorySearch = localMemorySearch;
7
7
  exports.localMemoryList = localMemoryList;
8
8
  exports.localMemoryDelete = localMemoryDelete;
9
+ exports.localMemoryDeleteByVaultKey = localMemoryDeleteByVaultKey;
9
10
  exports.localMemoryProfile = localMemoryProfile;
10
11
  const config_js_1 = require("./config.js");
11
12
  const local_memory_file_js_1 = require("./local-memory-file.js");
@@ -109,6 +110,18 @@ async function localMemoryDelete(cfg, id) {
109
110
  if (!res.ok)
110
111
  throw new Error(`local supermemory delete failed: HTTP ${res.status}`);
111
112
  }
113
+ /**
114
+ * Remove memory rows mirroring a deleted vault entry - called by vault_delete.
115
+ * "file" backend: direct filter+delete, exact. Self-hosted supermemory server:
116
+ * no documented "delete by metadata filter" endpoint, so this is a no-op that
117
+ * returns 0 rather than guessing at an unconfirmed API - vault_delete surfaces
118
+ * that count so a 0 on that backend doesn't get silently mistaken for success.
119
+ */
120
+ function localMemoryDeleteByVaultKey(cfg, vaultKey) {
121
+ if (cfg.kind === "file")
122
+ return (0, local_memory_file_js_1.fileMemoryDeleteByVaultKey)(cfg, vaultKey);
123
+ return 0;
124
+ }
112
125
  // No dedicated count endpoint is documented for the local supermemory server,
113
126
  // so that path approximates via a capped wildcard search - accurate up to
114
127
  // `PROFILE_SAMPLE_LIMIT`, reported as a floor ("200+") beyond that rather