@finchagentic/mcp 4.1.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -14
- package/dist/agent-loop.js +75 -72
- package/dist/annotations.js +14 -13
- package/dist/convex.js +15 -18
- package/dist/index.js +4 -5
- package/dist/llm.js +12 -1
- package/dist/local-memory-file.js +14 -1
- package/dist/local-memory.js +13 -0
- package/dist/output-schemas.js +71 -17
- package/dist/resources.js +6 -11
- package/dist/server.js +27 -13
- package/dist/token-gate.js +2 -2
- package/dist/tool-filter.js +13 -4
- package/dist/tools/agents.js +62 -390
- package/dist/tools/base-mcp.js +3 -15
- package/dist/tools/base.js +34 -20
- package/dist/tools/defi.js +10 -30
- package/dist/tools/equity.js +9 -1
- package/dist/tools/insight.js +2 -2
- package/dist/tools/memory.js +44 -80
- package/dist/tools/monitor.js +6 -6
- package/dist/tools/os.js +0 -1
- package/dist/tools/packets.js +2 -2
- package/dist/tools/rh-mcp.js +13 -2
- package/dist/tools/rh-orders.js +201 -123
- package/dist/tools/scanner.js +30 -0
- package/dist/tools/stake.js +329 -0
- package/dist/tools/vault.js +99 -21
- package/dist/wallet.js +130 -19
- package/package.json +2 -2
- 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.
|
|
32
|
+
npx -y -p @finchagentic/mcp@4.2.0 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.
|
|
38
|
+
claude mcp add finch -s user -- npx -y -p @finchagentic/mcp@4.2.0 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.
|
|
48
|
+
"args": ["-y", "-p", "@finchagentic/mcp@4.2.0", "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.
|
|
62
|
+
"args": ["-y", "-p", "@finchagentic/mcp@4.2.0", "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 —
|
|
82
|
+
No LLM API key is required to start — 108 of 113 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,18 +93,18 @@ 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
|
|
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
|
-
**
|
|
102
|
+
**113 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
|
|
107
|
+
| **Agents** | Spawn, recall, update named agents |
|
|
108
108
|
| **Workflows** | Automations, monitors, packets, deep research |
|
|
109
109
|
| **Execution** | Base DeFi, Robinhood Chain, market data, web, GitHub |
|
|
110
110
|
|
|
@@ -119,7 +119,7 @@ Default palette is `core` (lighter context). Full set:
|
|
|
119
119
|
Finch is the runtime. **Your LLM is the brain. Your data stays yours.**
|
|
120
120
|
|
|
121
121
|
```bash
|
|
122
|
-
npx -y -p @finchagentic/mcp@4.
|
|
122
|
+
npx -y -p @finchagentic/mcp@4.2.0 finch setup
|
|
123
123
|
# enable local vault (and optional local memory)
|
|
124
124
|
```
|
|
125
125
|
|
|
@@ -146,12 +146,12 @@ Scheduled/cloud features still need an account. Core memory, vault, and public-d
|
|
|
146
146
|
| `GITHUB_TOKEN` | For `github_search_code` |
|
|
147
147
|
| `ALCHEMY_API_KEY` | Faster Base RPC (optional) |
|
|
148
148
|
|
|
149
|
-
**Cost model:** almost everything is free to run — the other
|
|
149
|
+
**Cost model:** almost everything is free to run — the other 108 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
150
|
|
|
151
151
|
Guided setup:
|
|
152
152
|
|
|
153
153
|
```bash
|
|
154
|
-
npx -y -p @finchagentic/mcp@4.
|
|
154
|
+
npx -y -p @finchagentic/mcp@4.2.0 finch setup
|
|
155
155
|
```
|
|
156
156
|
|
|
157
157
|
## Security
|
|
@@ -160,12 +160,12 @@ npx -y -p @finchagentic/mcp@4.1.0 finch setup
|
|
|
160
160
|
|:-:|----------|------|
|
|
161
161
|
| 1 | Prompt injection | External content is data only — never instructions |
|
|
162
162
|
| 2 | Mainnet confirm | Estimate → preview → confirm → execute |
|
|
163
|
-
| 3 | Pinned install | Always `@finchagentic/mcp@4.
|
|
163
|
+
| 3 | Pinned install | Always `@finchagentic/mcp@4.2.0`, never `@latest` |
|
|
164
164
|
| 4 | Credential vault | Never paste secrets into prompts or third-party tools |
|
|
165
165
|
| 5 | Data disclosure | Know what leaves the machine (LLM, Firecrawl, GitHub, chain RPCs) |
|
|
166
166
|
| 6 | Server monitors | Scheduled jobs need explicit confirmation |
|
|
167
|
-
| 7 |
|
|
168
|
-
| 8 |
|
|
167
|
+
| 7 | Fund-moving confirm | `stake_finch`/`unstake_finch`/`base_mcp_send`/`base_mcp_swap`/`rh_mcp_swap` all require `confirm: true` |
|
|
168
|
+
| 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
169
|
|
|
170
170
|
## Troubleshooting
|
|
171
171
|
|
package/dist/agent-loop.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
}
|
|
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
|
-
|
|
196
|
+
function runConvexProxiedLoop(userMessage, history, onToolCall) {
|
|
161
197
|
const model = process.env.FINCH_MODEL ?? process.env.ANTHROPIC_MODEL ?? "claude-haiku-4-5-20251001";
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
const
|
|
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
|
-
}
|
|
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
|
+
}
|
package/dist/annotations.js
CHANGED
|
@@ -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
29
|
"create_automation",
|
|
37
|
-
"create_monitor",
|
|
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,11 +49,18 @@ const DESTRUCTIVE = new Set([
|
|
|
58
49
|
"rh_order_cancel",
|
|
59
50
|
"vault_delete",
|
|
60
51
|
// irreversible public exposure
|
|
61
|
-
"
|
|
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
|
-
|
|
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
|
|
@@ -70,8 +68,11 @@ const DESTRUCTIVE = new Set([
|
|
|
70
68
|
"rh_orders_tick", // preview by default, but can execute:true and move funds
|
|
71
69
|
// executors that run other (possibly fund-moving) tools
|
|
72
70
|
"run_automation",
|
|
73
|
-
"run_playbook",
|
|
74
71
|
"packet_run",
|
|
72
|
+
// money movement (FINCH staking, custodial wallet) - stake locks real value
|
|
73
|
+
// for a fixed period; unstake moves it (plus rewards) back
|
|
74
|
+
"stake_finch",
|
|
75
|
+
"unstake_finch",
|
|
75
76
|
]);
|
|
76
77
|
function annotationsFor(name) {
|
|
77
78
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
@@ -108,24 +108,23 @@ async function main() {
|
|
|
108
108
|
(0, config_js_1.hydrateEnvFromConfig)();
|
|
109
109
|
process.stderr.write(BANNER);
|
|
110
110
|
const CAT_RULES = [
|
|
111
|
-
{ label: "Market", match: n => /^(get_market_data|get_token_data|compare_tokens|market_overview|token_history)$/.test(n) },
|
|
111
|
+
{ 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
112
|
{ label: "Insight", match: n => /^(ask_finch|market_thesis|trade_plan)$/.test(n) },
|
|
113
113
|
{ label: "DeFi", match: n => n === "get_defi_yields" },
|
|
114
114
|
{ label: "Base MCP", match: n => n.startsWith("base_mcp_") },
|
|
115
115
|
{ label: "RH MCP", match: n => n.startsWith("rh_") },
|
|
116
116
|
{ 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
117
|
{ label: "Vault", match: n => n.startsWith("vault_") },
|
|
119
118
|
{ label: "Wallet", match: n => /^(get_wallet_address|get_wallet_balance|wallet_sign_message)$/.test(n) },
|
|
119
|
+
{ label: "Staking", match: n => /^(stake_finch|unstake_finch|stake_finch_status|stake_auto_restake)$/.test(n) },
|
|
120
120
|
{ label: "MiroShark", match: n => n.startsWith("miroshark_") },
|
|
121
121
|
{ label: "Scanner", match: n => /^(scan_market|score_token|check_token)$/.test(n) },
|
|
122
|
-
{ label: "Agents", match: n => n.startsWith("agent_")
|
|
122
|
+
{ label: "Agents", match: n => n.startsWith("agent_") },
|
|
123
123
|
{ label: "Coder", match: n => n === "audit_contract" },
|
|
124
|
-
{ label: "Base", match: n => /^(query_vaults|list_markets|prepare_deposit|chain_stats)$/.test(n) },
|
|
125
124
|
{ label: "Memory", match: n => n.startsWith("memory_") },
|
|
126
125
|
{ label: "OS", match: n => /^(finch_status|finch_diagnostics|finch_shell_chat)$/.test(n) },
|
|
127
126
|
{ label: "Research", match: n => /^(web_scrape|web_search|deep_research|research_compare|research_chain)$/.test(n) },
|
|
128
|
-
{ label: "Monitor", match: n => /^(schedule_research|
|
|
127
|
+
{ label: "Monitor", match: n => /^(schedule_research|list_monitors|cancel_monitor)$/.test(n) },
|
|
129
128
|
{ label: "GitHub", match: n => n.startsWith("github_") },
|
|
130
129
|
{ label: "Chronicle", match: n => n.startsWith("chronicle_") },
|
|
131
130
|
{ 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
|
-
|
|
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);
|
package/dist/local-memory.js
CHANGED
|
@@ -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
|