@finchagentic/mcp 4.0.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +96 -249
- package/dist/agent-loop.js +139 -69
- package/dist/annotations.js +14 -13
- package/dist/cli.js +78 -21
- package/dist/config.js +16 -16
- package/dist/convex.js +15 -18
- package/dist/index.js +4 -5
- package/dist/llm.js +24 -49
- package/dist/local-memory-file.js +147 -0
- package/dist/local-memory.js +42 -9
- package/dist/output-schemas.js +71 -17
- package/dist/resources.js +8 -13
- 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 +66 -394
- package/dist/tools/base-mcp.js +7 -19
- package/dist/tools/base.js +34 -20
- package/dist/tools/chronicle.js +4 -4
- package/dist/tools/deep-research.js +10 -5
- package/dist/tools/defi.js +36 -35
- package/dist/tools/equity.js +9 -1
- package/dist/tools/insight.js +25 -29
- package/dist/tools/memory.js +76 -108
- package/dist/tools/monitor.js +7 -7
- package/dist/tools/os.js +10 -6
- package/dist/tools/packets.js +5 -5
- package/dist/tools/research.js +2 -2
- package/dist/tools/rh-mcp.js +25 -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 +122 -42
- package/dist/wallet.js +212 -24
- package/package.json +9 -21
- package/dist/tools/framework.js +0 -150
package/dist/agent-loop.js
CHANGED
|
@@ -15,21 +15,48 @@ const SYSTEM_PROMPT = [
|
|
|
15
15
|
"- For ALL Base chain operations, you MUST use the base_mcp_* family: base_mcp_swap (NOT swap_tokens), base_mcp_send (NOT send_token), base_mcp_balance (NOT get_portfolio), base_mcp_estimate, base_mcp_resolve, base_mcp_lend, base_mcp_status. This is non-negotiable - Base operations go through the Base MCP skill, period.",
|
|
16
16
|
"- For Robinhood Chain tokenized stocks (chainId 4663, NVDA/AAPL/etc on Uniswap V4), you MUST use rh_mcp_*: rh_mcp_status, rh_mcp_list_stocks, rh_mcp_balance, rh_mcp_estimate, rh_mcp_swap. Never use base_mcp_* or 0x for RH stocks. rh_mcp_swap requires confirm:true. Explorer = robinhoodchain.blockscout.com. This is NOT Robinhood Agentic brokerage (agent.robinhood.com).",
|
|
17
17
|
"- Do NOT claim an address belongs to the user (e.g. 'your own address') unless you have verified ownership. Resolving a basename returns whoever owns that name - usually NOT the caller.",
|
|
18
|
+
"- The Finch wallet is the SAME address on both Base and Robinhood Chain, but they are separate ledgers - a balance on one tells you nothing about the other. If the user asks a chain-unspecified question ('what's my balance', 'do I have any funds') call BOTH base_mcp_balance AND rh_mcp_balance before answering. Never report only one chain's result as 'your balance' or 'your wallet is empty' - say which chain(s) you checked, and give both figures.",
|
|
19
|
+
"- Swap/send requests are only chain-unambiguous when the token itself pins the chain (a RH catalog stock like NVDA/AAPL, or a token you already know only exists on one side). A bare request like 'swap 0.001 ETH to a stablecoin' with no chain named is NOT unambiguous just because you picked one - ETH and 'stablecoin' both exist on Base (USDC/USDT/DAI) and Robinhood Chain (USDG). Before estimating or executing an ambiguous swap, call both base_mcp_balance and rh_mcp_balance first: if only one chain actually holds enough of the source token, use that chain and say which one and why; if neither holds enough, say so plainly instead of quoting a swap the wallet can't cover; if both hold enough, ask the user which chain before proceeding.",
|
|
20
|
+
"- There is NO send/transfer tool for Robinhood Chain - base_mcp_send only moves assets on Base mainnet. If the user asks to send/transfer USDG or any RH catalog stock (NVDA, AAPL, etc.), do NOT call base_mcp_send with that token name hoping it resolves - it will either error or, worse, silently match an unrelated Base token with the same symbol. Tell the user directly that on-chain sends are not yet supported on Robinhood Chain.",
|
|
21
|
+
"- Never state a balance, price, quote, token address, or transaction result from memory or inference - every number in your answer must trace to a tool call you made THIS turn. If you are not sure which chain, token, or address a term refers to, ask or resolve it (rh_token_resolve / base_mcp_resolve) before answering - do not guess and present the guess as fact.",
|
|
18
22
|
"",
|
|
19
23
|
"For deep research: prefer deep_research (multi-stage, saves to vault). Use continueFrom when extending prior reports.",
|
|
20
24
|
"For live web info: use web_search. For market questions: use get_market_data or market_thesis.",
|
|
21
25
|
"Save substantive findings to vault; do not save thin or empty outputs.",
|
|
26
|
+
"",
|
|
27
|
+
"NEVER call ask_finch from this shell. It exists for MCP clients that have no reasoning model of their own - you already are one. Calling it mid-task means asking a second model to think for you, which produces nothing you couldn't write yourself from the data you already have, and repeating it when unsatisfied just burns turns. If a tool's result already answers the question, write the answer yourself.",
|
|
28
|
+
"Do not call the same tool with the same or near-identical arguments more than once in a single answer. If deep_research, a search, or an analysis tool already returned data, synthesize from that - do not re-run it hoping for a different result, and do not chain more tool calls than the question actually needs. Stop calling tools and answer as soon as you have enough to answer well.",
|
|
22
29
|
].join("\n");
|
|
23
30
|
async function runAgent(userMessage, history, onToolCall) {
|
|
31
|
+
const provider = process.env.FINCH_PROVIDER?.toLowerCase().trim();
|
|
24
32
|
const bankrKey = process.env.BANKR_API_KEY;
|
|
25
33
|
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
26
34
|
const openaiKey = process.env.OPENAI_API_KEY;
|
|
35
|
+
const grokKey = process.env.GROK_API_KEY;
|
|
36
|
+
// Explicit override - lets FINCH_PROVIDER=openai win even when BANKR_API_KEY
|
|
37
|
+
// is also set (e.g. as a persistent shell env var), same as llm.ts's callLLM.
|
|
38
|
+
if (provider === "bankr" && bankrKey)
|
|
39
|
+
return runBankrLoop(bankrKey, userMessage, history, onToolCall);
|
|
40
|
+
if (provider === "anthropic" && anthropicKey)
|
|
41
|
+
return runAnthropicLoop(anthropicKey, userMessage, history, onToolCall);
|
|
42
|
+
if (provider === "openai" && openaiKey)
|
|
43
|
+
return runOpenAILoop(openaiKey, userMessage, history, onToolCall);
|
|
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.
|
|
27
52
|
if (bankrKey)
|
|
28
53
|
return runBankrLoop(bankrKey, userMessage, history, onToolCall);
|
|
29
54
|
if (anthropicKey)
|
|
30
55
|
return runAnthropicLoop(anthropicKey, userMessage, history, onToolCall);
|
|
31
56
|
if (openaiKey)
|
|
32
57
|
return runOpenAILoop(openaiKey, userMessage, history, onToolCall);
|
|
58
|
+
if (grokKey)
|
|
59
|
+
return runGrokLoop(grokKey, userMessage, history, onToolCall);
|
|
33
60
|
// No direct key - proxy through Finch backend. Wallet auto-creates at ~/.finch/wallet.json
|
|
34
61
|
// on first use and signs requests transparently. No account or config needed.
|
|
35
62
|
try {
|
|
@@ -49,8 +76,14 @@ function toAnthropicTool(tool) {
|
|
|
49
76
|
input_schema: tool.inputSchema ?? { type: "object", properties: {} },
|
|
50
77
|
};
|
|
51
78
|
}
|
|
52
|
-
|
|
53
|
-
|
|
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) {
|
|
54
87
|
const tools = server_js_1.ALL_TOOLS.map(toAnthropicTool);
|
|
55
88
|
const toolCalls = [];
|
|
56
89
|
const messages = [
|
|
@@ -58,21 +91,7 @@ async function runAnthropicLoop(apiKey, userMessage, history, onToolCall) {
|
|
|
58
91
|
{ role: "user", content: userMessage },
|
|
59
92
|
];
|
|
60
93
|
for (let turn = 0; turn < 10; turn++) {
|
|
61
|
-
const
|
|
62
|
-
method: "POST",
|
|
63
|
-
headers: {
|
|
64
|
-
"Content-Type": "application/json",
|
|
65
|
-
"x-api-key": apiKey,
|
|
66
|
-
"anthropic-version": "2023-06-01",
|
|
67
|
-
},
|
|
68
|
-
body: JSON.stringify({ model, max_tokens: 2048, system: SYSTEM_PROMPT, tools, messages }),
|
|
69
|
-
signal: AbortSignal.timeout(90000),
|
|
70
|
-
});
|
|
71
|
-
if (!res.ok) {
|
|
72
|
-
const body = await res.text().catch(() => "");
|
|
73
|
-
throw new Error(`Anthropic ${res.status}: ${body.slice(0, 300)}`);
|
|
74
|
-
}
|
|
75
|
-
const data = await res.json();
|
|
94
|
+
const data = await sendTurn(messages, tools);
|
|
76
95
|
messages.push({ role: "assistant", content: data.content });
|
|
77
96
|
if (data.stop_reason !== "tool_use") {
|
|
78
97
|
const text = data.content
|
|
@@ -86,15 +105,27 @@ async function runAnthropicLoop(apiKey, userMessage, history, onToolCall) {
|
|
|
86
105
|
for (const block of data.content) {
|
|
87
106
|
if (block.type !== "tool_use")
|
|
88
107
|
continue;
|
|
89
|
-
onToolCall(block.name);
|
|
90
|
-
toolCalls.push({ name: block.name });
|
|
91
108
|
let resultText;
|
|
92
109
|
try {
|
|
93
110
|
const handler = server_js_1.HANDLER_MAP.get(block.name);
|
|
94
111
|
if (!handler)
|
|
95
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 });
|
|
96
119
|
const result = await handler(block.name, block.input ?? {});
|
|
97
|
-
|
|
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.";
|
|
98
129
|
}
|
|
99
130
|
catch (err) {
|
|
100
131
|
resultText = `Error: ${err.message}`;
|
|
@@ -103,56 +134,76 @@ async function runAnthropicLoop(apiKey, userMessage, history, onToolCall) {
|
|
|
103
134
|
}
|
|
104
135
|
messages.push({ role: "user", content: toolResults });
|
|
105
136
|
}
|
|
106
|
-
|
|
137
|
+
// Hit the turn cap without a final answer - rather than hand back nothing,
|
|
138
|
+
// force one more call with no tools available so the model has to
|
|
139
|
+
// synthesize a real answer from whatever it already gathered.
|
|
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) => {
|
|
162
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
163
|
+
method: "POST",
|
|
164
|
+
headers: { "Content-Type": "application/json", "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
|
|
165
|
+
body: JSON.stringify({ model, max_tokens: 4096, system: SYSTEM_PROMPT, messages: msgs }),
|
|
166
|
+
signal: AbortSignal.timeout(90000),
|
|
167
|
+
});
|
|
168
|
+
if (!res.ok)
|
|
169
|
+
return "";
|
|
170
|
+
const data = await res.json();
|
|
171
|
+
return (data.content ?? []).filter(b => b.type === "text").map(b => b.text).join("");
|
|
172
|
+
};
|
|
173
|
+
return runAnthropicStyleLoop(sendTurn, sendFinal, userMessage, history, onToolCall);
|
|
174
|
+
}
|
|
175
|
+
// Shared turn-cap fallback: rather than "Reached max tool iterations." with
|
|
176
|
+
// nothing useful, ask the model to synthesize a real answer from whatever
|
|
177
|
+
// conversation history (including every tool result so far) it already has,
|
|
178
|
+
// with no tools offered so it can't keep deferring.
|
|
179
|
+
async function finishWithoutTools(call, messages, toolCalls) {
|
|
180
|
+
try {
|
|
181
|
+
const closingMessages = [
|
|
182
|
+
...messages,
|
|
183
|
+
{ role: "user", content: "Stop calling tools. Summarize what you found above into a direct answer for the user right now." },
|
|
184
|
+
];
|
|
185
|
+
const text = await call(closingMessages);
|
|
186
|
+
if (text.trim())
|
|
187
|
+
return { text, toolCalls };
|
|
188
|
+
}
|
|
189
|
+
catch { /* fall through to the honest failure message below */ }
|
|
190
|
+
return {
|
|
191
|
+
text: "I gathered some information but ran out of turns before finishing the analysis. Try a narrower question, or ask me to continue from what I found.",
|
|
192
|
+
toolCalls,
|
|
193
|
+
};
|
|
107
194
|
}
|
|
108
195
|
// ── Convex-proxied Anthropic loop (session token only - platform covers LLM) ──
|
|
109
|
-
|
|
196
|
+
function runConvexProxiedLoop(userMessage, history, onToolCall) {
|
|
110
197
|
const model = process.env.FINCH_MODEL ?? process.env.ANTHROPIC_MODEL ?? "claude-haiku-4-5-20251001";
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
...history.map(h => ({ role: h.role, content: h.content })),
|
|
115
|
-
{ role: "user", content: userMessage },
|
|
116
|
-
];
|
|
117
|
-
for (let turn = 0; turn < 10; turn++) {
|
|
118
|
-
// callConvex handles wallet/session auth automatically; 90s timeout matches the proxy endpoint
|
|
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) => {
|
|
119
201
|
const data = await (0, convex_js_1.callConvex)("/llm/complete", "POST", {
|
|
120
|
-
model,
|
|
121
|
-
max_tokens: 2048,
|
|
122
|
-
system: SYSTEM_PROMPT,
|
|
123
|
-
tools,
|
|
124
|
-
messages,
|
|
202
|
+
model, max_tokens: 4096, system: SYSTEM_PROMPT, messages: msgs,
|
|
125
203
|
}, "llm_complete", 90000);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
.filter(b => b.type === "text")
|
|
130
|
-
.map(b => b.text)
|
|
131
|
-
.join("");
|
|
132
|
-
return { text, toolCalls };
|
|
133
|
-
}
|
|
134
|
-
const toolResults = [];
|
|
135
|
-
for (const block of data.content) {
|
|
136
|
-
if (block.type !== "tool_use")
|
|
137
|
-
continue;
|
|
138
|
-
onToolCall(block.name);
|
|
139
|
-
toolCalls.push({ name: block.name });
|
|
140
|
-
let resultText;
|
|
141
|
-
try {
|
|
142
|
-
const handler = server_js_1.HANDLER_MAP.get(block.name);
|
|
143
|
-
if (!handler)
|
|
144
|
-
throw new Error(`Unknown tool: ${block.name}`);
|
|
145
|
-
const result = await handler(block.name, block.input ?? {});
|
|
146
|
-
resultText = result?.content?.[0]?.text ?? "Done.";
|
|
147
|
-
}
|
|
148
|
-
catch (err) {
|
|
149
|
-
resultText = `Error: ${err.message}`;
|
|
150
|
-
}
|
|
151
|
-
toolResults.push({ type: "tool_result", tool_use_id: block.id, content: resultText });
|
|
152
|
-
}
|
|
153
|
-
messages.push({ role: "user", content: toolResults });
|
|
154
|
-
}
|
|
155
|
-
return { text: "Reached max tool iterations.", toolCalls };
|
|
204
|
+
return (data.content ?? []).filter((b) => b.type === "text").map((b) => b.text).join("");
|
|
205
|
+
};
|
|
206
|
+
return runAnthropicStyleLoop(sendTurn, sendFinal, userMessage, history, onToolCall);
|
|
156
207
|
}
|
|
157
208
|
// ── Bankr (OpenAI-compatible) agent loop ─────────────────────────────────────
|
|
158
209
|
function toBankrTool(tool) {
|
|
@@ -180,7 +231,7 @@ async function runOpenAICompatibleLoop(url, authHeaders, model, providerLabel, u
|
|
|
180
231
|
const res = await fetch(url, {
|
|
181
232
|
method: "POST",
|
|
182
233
|
headers: { "Content-Type": "application/json", ...authHeaders },
|
|
183
|
-
body: JSON.stringify({ model, messages, tools, max_tokens:
|
|
234
|
+
body: JSON.stringify({ model, messages, tools, max_tokens: 4096 }),
|
|
184
235
|
signal: AbortSignal.timeout(90000),
|
|
185
236
|
});
|
|
186
237
|
if (!res.ok) {
|
|
@@ -196,14 +247,16 @@ async function runOpenAICompatibleLoop(url, authHeaders, model, providerLabel, u
|
|
|
196
247
|
return { text: choice.content ?? "", toolCalls };
|
|
197
248
|
}
|
|
198
249
|
for (const call of choice.tool_calls) {
|
|
199
|
-
onToolCall(call.function.name);
|
|
200
|
-
toolCalls.push({ name: call.function.name });
|
|
201
250
|
let resultText;
|
|
202
251
|
try {
|
|
203
252
|
const args = JSON.parse(call.function.arguments ?? "{}");
|
|
204
253
|
const handler = server_js_1.HANDLER_MAP.get(call.function.name);
|
|
205
254
|
if (!handler)
|
|
206
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 });
|
|
207
260
|
const result = await handler(call.function.name, args);
|
|
208
261
|
resultText = result?.content?.[0]?.text ?? "Done.";
|
|
209
262
|
}
|
|
@@ -213,7 +266,18 @@ async function runOpenAICompatibleLoop(url, authHeaders, model, providerLabel, u
|
|
|
213
266
|
messages.push({ role: "tool", tool_call_id: call.id, content: resultText });
|
|
214
267
|
}
|
|
215
268
|
}
|
|
216
|
-
return
|
|
269
|
+
return finishWithoutTools(async (msgs) => {
|
|
270
|
+
const res = await fetch(url, {
|
|
271
|
+
method: "POST",
|
|
272
|
+
headers: { "Content-Type": "application/json", ...authHeaders },
|
|
273
|
+
body: JSON.stringify({ model, messages: msgs, max_tokens: 4096 }),
|
|
274
|
+
signal: AbortSignal.timeout(90000),
|
|
275
|
+
});
|
|
276
|
+
if (!res.ok)
|
|
277
|
+
return "";
|
|
278
|
+
const data = await res.json();
|
|
279
|
+
return data.choices?.[0]?.message?.content ?? "";
|
|
280
|
+
}, messages, toolCalls);
|
|
217
281
|
}
|
|
218
282
|
async function runBankrLoop(apiKey, userMessage, history, onToolCall) {
|
|
219
283
|
const model = process.env.FINCH_MODEL ?? process.env.BANKR_MODEL ?? "claude-haiku-4-5-20251001";
|
|
@@ -229,3 +293,9 @@ async function runOpenAILoop(apiKey, userMessage, history, onToolCall) {
|
|
|
229
293
|
const model = process.env.FINCH_MODEL ?? process.env.OPENAI_MODEL ?? "gpt-4o-mini";
|
|
230
294
|
return runOpenAICompatibleLoop(openAiChatUrl(), { Authorization: `Bearer ${apiKey}` }, model, "OpenAI", userMessage, history, onToolCall);
|
|
231
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/cli.js
CHANGED
|
@@ -44,6 +44,7 @@ const tool_filter_js_1 = require("./tool-filter.js");
|
|
|
44
44
|
const config_js_1 = require("./config.js");
|
|
45
45
|
const local_memory_js_1 = require("./local-memory.js");
|
|
46
46
|
const clink_input_js_1 = require("./clink-input.js");
|
|
47
|
+
const wallet_js_1 = require("./wallet.js");
|
|
47
48
|
const child_process = __importStar(require("child_process"));
|
|
48
49
|
(0, config_js_1.hydrateEnvFromConfig)();
|
|
49
50
|
// Derived tool counts - single source of truth, kept in sync with the
|
|
@@ -117,8 +118,8 @@ async function loginWithApiKey(rl) {
|
|
|
117
118
|
}
|
|
118
119
|
const email = data.email ?? "api-key-user";
|
|
119
120
|
const name = data.displayName ?? undefined;
|
|
120
|
-
(0, config_js_1.writeConfig)({ sessionToken: data.token, email, name });
|
|
121
|
-
printLoginSuccess({ email, name });
|
|
121
|
+
(0, config_js_1.writeConfig)({ sessionToken: data.token, email, name, walletAddress: data.walletAddress });
|
|
122
|
+
printLoginSuccess({ email, name, walletAddress: data.walletAddress });
|
|
122
123
|
}
|
|
123
124
|
async function loginFlow(loginRl) {
|
|
124
125
|
const rl = loginRl ?? readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -136,8 +137,8 @@ async function loginFlow(loginRl) {
|
|
|
136
137
|
if (res.ok && data.token) {
|
|
137
138
|
const email = data.email ?? "api-key-user";
|
|
138
139
|
const name = data.displayName ?? undefined;
|
|
139
|
-
(0, config_js_1.writeConfig)({ sessionToken: data.token, email, name });
|
|
140
|
-
printLoginSuccess({ email, name });
|
|
140
|
+
(0, config_js_1.writeConfig)({ sessionToken: data.token, email, name, walletAddress: data.walletAddress });
|
|
141
|
+
printLoginSuccess({ email, name, walletAddress: data.walletAddress });
|
|
141
142
|
rl.close();
|
|
142
143
|
return;
|
|
143
144
|
}
|
|
@@ -210,10 +211,16 @@ async function setupFlow() {
|
|
|
210
211
|
console.log(`\n ${C.dim}Skipped - using Finch proxy.${C.reset}\n`);
|
|
211
212
|
}
|
|
212
213
|
// ── Step 2: local memory ──────────────────────────────────────────────────
|
|
213
|
-
console.log(` ${C.cyan}${C.bold}Local memory${C.reset} ${C.dim}(self-hosted
|
|
214
|
-
console.log(` ${C.dim}
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
console.log(` ${C.cyan}${C.bold}Local memory${C.reset} ${C.dim}(self-hosted - your choice of backend)${C.reset}`);
|
|
215
|
+
console.log(` ${C.dim}[1] Simple file-based (recommended) - zero dependency, JSON on disk at ~/.finch/memory/, keyword search only.${C.reset}`);
|
|
216
|
+
console.log(` ${C.dim}[2] Supermemory server - real semantic/embedding search, but you run a separate process yourself.${C.reset}`);
|
|
217
|
+
console.log(` ${C.dim}[3] Skip - memory tools use the Finch-hosted proxy.${C.reset}\n`);
|
|
218
|
+
const memChoice = (0, clink_input_js_1.dedupClinkInput)((await ask(` Choose [1-3]: `)).trim());
|
|
219
|
+
if (memChoice === "1") {
|
|
220
|
+
(0, config_js_1.writeConfig)({ memoryBackend: "local-file" });
|
|
221
|
+
console.log(`\n ${C.green}✓${C.reset} Local file memory enabled at ~/.finch/memory/. No server, no key - memory tools run fully local.\n`);
|
|
222
|
+
}
|
|
223
|
+
else if (memChoice === "2") {
|
|
217
224
|
const url = "http://localhost:6767";
|
|
218
225
|
console.log(`\n ${C.dim}Checking ${url}...${C.reset}`);
|
|
219
226
|
let reachable = await isLocalMemoryReachableRaw(url);
|
|
@@ -389,16 +396,21 @@ const C = {
|
|
|
389
396
|
// ██║ ╚████║╚██████╔╝███████╗███████╗╚██████╗███████╗██║ ██║╚███╔███╔╝
|
|
390
397
|
// ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝
|
|
391
398
|
const LOGO_LINES = [
|
|
392
|
-
` ███╗ ██╗
|
|
393
|
-
` ████╗
|
|
394
|
-
` ██╔██╗
|
|
395
|
-
`
|
|
396
|
-
` ██║
|
|
397
|
-
` ╚═╝ ╚═══╝
|
|
399
|
+
` ███████╗ ██╗ ███╗ ██╗ ██████╗ ██╗ ██╗`,
|
|
400
|
+
` ██╔════╝ ██║ ████╗ ██║ ██╔════╝ ██║ ██║`,
|
|
401
|
+
` █████╗ ██║ ██╔██╗ ██║ ██║ ███████║`,
|
|
402
|
+
` ██╔══╝ ██║ ██║╚██╗██║ ██║ ██╔══██║`,
|
|
403
|
+
` ██║ ██║ ██║ ╚████║ ╚██████╗ ██║ ██║`,
|
|
404
|
+
` ╚═╝ ╚═╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═╝`,
|
|
398
405
|
];
|
|
399
406
|
const SEP = ` ${"─".repeat(70)}`;
|
|
400
407
|
function buildBanner() {
|
|
401
408
|
const cfg = (0, config_js_1.readConfig)();
|
|
409
|
+
// Every base_mcp_*/rh_mcp_* tool signs locally via getOrCreateWallet()
|
|
410
|
+
// (~/.finch/wallet.json) on BOTH Base and Robinhood Chain (RH reuses the
|
|
411
|
+
// same local address) - this CLI never touches the account's custodial
|
|
412
|
+
// webapp wallet. Showing that other address here would mismatch what
|
|
413
|
+
// every tool call actually reports and operates on.
|
|
402
414
|
const walletAddr = getLocalWalletAddress();
|
|
403
415
|
const provider = process.env.BANKR_API_KEY ? "Bankr"
|
|
404
416
|
: process.env.ANTHROPIC_API_KEY ? "Anthropic"
|
|
@@ -415,8 +427,9 @@ function buildBanner() {
|
|
|
415
427
|
const nameLine = displayName
|
|
416
428
|
? ` ${C.green}●${C.reset} ${displayName} ${displayEmail}`
|
|
417
429
|
: ` ${C.green}●${C.reset} ${displayEmail || `${C.green}Signed in${C.reset}`}`;
|
|
430
|
+
const walletNote = "Base + Robinhood Chain · local device wallet · keys never leave your machine";
|
|
418
431
|
const walletLine = walletAddr
|
|
419
|
-
? ` ${C.dim}Wallet${C.reset} ${C.cyan}${walletAddr.slice(0, 6)}...${walletAddr.slice(-4)}${C.reset} ${C.dim}·
|
|
432
|
+
? ` ${C.dim}Wallet${C.reset} ${C.cyan}${walletAddr.slice(0, 6)}...${walletAddr.slice(-4)}${C.reset} ${C.dim}· ${walletNote}${C.reset}`
|
|
420
433
|
: ` ${C.dim}Wallet not created yet · auto-creates on first DeFi call${C.reset}`;
|
|
421
434
|
const llmLine = ` ${C.dim}LLM ${C.reset}${C.green}${provider}${C.reset} ${C.dim}· ${TOTAL_TOOL_COUNT} tools active${C.reset}`;
|
|
422
435
|
authBlock = `\n${nameLine}\n${walletLine}\n${llmLine}`;
|
|
@@ -434,6 +447,10 @@ function buildBanner() {
|
|
|
434
447
|
}
|
|
435
448
|
// ── Post-login success block ──────────────────────────────────────────────────
|
|
436
449
|
function printLoginSuccess({ email, name }) {
|
|
450
|
+
// Always show the local per-device wallet: every base_mcp_*/rh_mcp_* tool
|
|
451
|
+
// signs with it on BOTH chains. The account's custodial webapp wallet is a
|
|
452
|
+
// separate address this CLI never touches - showing it here would mismatch
|
|
453
|
+
// what every subsequent tool call actually reports.
|
|
437
454
|
const walletAddr = getLocalWalletAddress();
|
|
438
455
|
const displayName = name ?? "";
|
|
439
456
|
console.log(`\n${SEP}`);
|
|
@@ -444,7 +461,8 @@ function printLoginSuccess({ email, name }) {
|
|
|
444
461
|
console.log(` ${C.green}✓${C.reset} ${C.green}${C.bold}Signed in${C.reset} ${C.dim}as ${email}${C.reset}`);
|
|
445
462
|
}
|
|
446
463
|
if (walletAddr) {
|
|
447
|
-
|
|
464
|
+
const note = "Base + Robinhood Chain · local device wallet";
|
|
465
|
+
console.log(` ${C.dim}Wallet${C.reset} ${C.cyan}${walletAddr.slice(0, 6)}...${walletAddr.slice(-4)}${C.reset} ${C.dim}· ${note}${C.reset}`);
|
|
448
466
|
}
|
|
449
467
|
else {
|
|
450
468
|
console.log(` ${C.dim}Wallet auto-creates on first DeFi tool call${C.reset}`);
|
|
@@ -527,7 +545,7 @@ async function main() {
|
|
|
527
545
|
});
|
|
528
546
|
const data = await res.json();
|
|
529
547
|
if (res.ok && data.token) {
|
|
530
|
-
(0, config_js_1.writeConfig)({ sessionToken: data.token, email: data.email ?? "api-key-user", name: data.displayName ?? undefined });
|
|
548
|
+
(0, config_js_1.writeConfig)({ sessionToken: data.token, email: data.email ?? "api-key-user", name: data.displayName ?? undefined, walletAddress: data.walletAddress });
|
|
531
549
|
console.log(`${C.green}✓${C.reset} ${C.dim}Signed in as ${data.email}${C.reset}`);
|
|
532
550
|
}
|
|
533
551
|
else {
|
|
@@ -539,6 +557,8 @@ async function main() {
|
|
|
539
557
|
}
|
|
540
558
|
}
|
|
541
559
|
process.stdout.write(buildBanner());
|
|
560
|
+
// Once, here, so it doesn't interrupt the first tool reply mid-conversation.
|
|
561
|
+
(0, wallet_js_1.warnIfNoPassphrase)();
|
|
542
562
|
// Check for updates in background - shows after banner, doesn't block prompt
|
|
543
563
|
checkForUpdate().catch(() => { });
|
|
544
564
|
const history = [];
|
|
@@ -998,10 +1018,17 @@ async function doctorFlow() {
|
|
|
998
1018
|
fix: `Optional. Create at https://github.com/settings/tokens (scopes: public_repo) and add to MCP env.`,
|
|
999
1019
|
});
|
|
1000
1020
|
}
|
|
1001
|
-
// 9. Local memory (self-hosted
|
|
1002
|
-
//
|
|
1021
|
+
// 9. Local memory (self-hosted) - optional, zero-cost alternative to the
|
|
1022
|
+
// Convex-proxied cloud memory backend. Two flavors: zero-dependency file
|
|
1023
|
+
// store, or a separately-run supermemory server.
|
|
1003
1024
|
const localMemCfg = (0, local_memory_js_1.getLocalMemoryConfig)();
|
|
1004
|
-
if (localMemCfg) {
|
|
1025
|
+
if (localMemCfg?.kind === "file") {
|
|
1026
|
+
checks.push({
|
|
1027
|
+
name: "Local memory", status: "✓",
|
|
1028
|
+
detail: `file-based at ~/.finch/memory - memory tools run fully local, zero cost, no server`,
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
else if (localMemCfg?.kind === "supermemory") {
|
|
1005
1032
|
const reachable = await (0, local_memory_js_1.isLocalMemoryReachable)(localMemCfg);
|
|
1006
1033
|
checks.push(reachable ? {
|
|
1007
1034
|
name: "Local memory", status: "✓",
|
|
@@ -1143,6 +1170,9 @@ else if (cmd === "logout") {
|
|
|
1143
1170
|
}
|
|
1144
1171
|
else if (cmd === "status") {
|
|
1145
1172
|
const cfg = (0, config_js_1.readConfig)();
|
|
1173
|
+
// Every base_mcp_*/rh_mcp_* tool signs locally on BOTH chains - the
|
|
1174
|
+
// account's custodial webapp wallet is a separate address this CLI never
|
|
1175
|
+
// touches, so always show the local one to match what tools report.
|
|
1146
1176
|
const walletAddr = getLocalWalletAddress();
|
|
1147
1177
|
const provider = process.env.BANKR_API_KEY ? "Bankr"
|
|
1148
1178
|
: process.env.ANTHROPIC_API_KEY ? "Anthropic"
|
|
@@ -1155,7 +1185,8 @@ else if (cmd === "status") {
|
|
|
1155
1185
|
const displayEmail = cfg.email ? `${C.dim}${cfg.email}${C.reset}` : "";
|
|
1156
1186
|
console.log(` ${C.green}●${C.reset} ${displayName}${displayEmail}`);
|
|
1157
1187
|
if (walletAddr) {
|
|
1158
|
-
|
|
1188
|
+
const note = "Base + Robinhood Chain · local device wallet";
|
|
1189
|
+
console.log(` ${C.dim}Wallet ${C.reset}${C.cyan}${walletAddr.slice(0, 6)}...${walletAddr.slice(-4)}${C.reset} ${C.dim}· ${note}${C.reset}`);
|
|
1159
1190
|
}
|
|
1160
1191
|
else {
|
|
1161
1192
|
console.log(` ${C.dim}Wallet not created yet${C.reset}`);
|
|
@@ -1169,12 +1200,38 @@ else if (cmd === "status") {
|
|
|
1169
1200
|
}
|
|
1170
1201
|
console.log(`${SEP_S}\n`);
|
|
1171
1202
|
}
|
|
1203
|
+
else if (cmd === "run") {
|
|
1204
|
+
const prompt = process.argv.slice(3).join(" ");
|
|
1205
|
+
if (!prompt) {
|
|
1206
|
+
console.error(` ${C.red}✗ usage: finch run "your prompt"${C.reset}\n`);
|
|
1207
|
+
process.exit(1);
|
|
1208
|
+
}
|
|
1209
|
+
(0, wallet_js_1.warnIfNoPassphrase)();
|
|
1210
|
+
(async () => {
|
|
1211
|
+
const history = [];
|
|
1212
|
+
const stop = spinner("thinking");
|
|
1213
|
+
try {
|
|
1214
|
+
const result = await (0, agent_loop_js_1.runAgent)(prompt, history, (toolName) => {
|
|
1215
|
+
stop();
|
|
1216
|
+
process.stdout.write(` ${C.dim}✦ ${toolName}${C.reset}\n`);
|
|
1217
|
+
});
|
|
1218
|
+
stop();
|
|
1219
|
+
console.log(`\n${result.text}\n`);
|
|
1220
|
+
}
|
|
1221
|
+
catch (err) {
|
|
1222
|
+
stop();
|
|
1223
|
+
console.error(` ${C.red}✗ run error: ${err.message || err}${C.reset}\n`);
|
|
1224
|
+
process.exit(1);
|
|
1225
|
+
}
|
|
1226
|
+
})();
|
|
1227
|
+
}
|
|
1172
1228
|
else if (cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
1173
1229
|
console.log(`
|
|
1174
1230
|
${C.cyan}${C.bold}finch${C.reset} ${C.dim}runtime layer for Agentic AI · terminal CLI${C.reset}
|
|
1175
1231
|
|
|
1176
1232
|
${C.cyan}Commands:${C.reset}
|
|
1177
1233
|
finch Start interactive AI terminal
|
|
1234
|
+
finch run "..." Run a single prompt and exit (scripting/CI-friendly)
|
|
1178
1235
|
finch install Auto-configure all detected MCP clients
|
|
1179
1236
|
finch login Sign in to unlock all tools
|
|
1180
1237
|
finch logout Sign out and clear saved token
|
package/dist/config.js
CHANGED
|
@@ -43,10 +43,10 @@ const fs = __importStar(require("fs"));
|
|
|
43
43
|
const os = __importStar(require("os"));
|
|
44
44
|
const path = __importStar(require("path"));
|
|
45
45
|
const CONFIG_DIR = path.join(os.homedir(), ".finch");
|
|
46
|
-
const LEGACY_DIR = path.join(os.homedir(), ".
|
|
46
|
+
const LEGACY_DIR = path.join(os.homedir(), ".finch");
|
|
47
47
|
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
48
48
|
const LEGACY_FILE = path.join(LEGACY_DIR, "config.json");
|
|
49
|
-
/** Prefer FINCH_* env, fall back to legacy
|
|
49
|
+
/** Prefer FINCH_* env, fall back to legacy FINCH_* during rebrand. */
|
|
50
50
|
function env(name, legacy) {
|
|
51
51
|
const primary = process.env[name];
|
|
52
52
|
if (primary !== undefined && primary !== "")
|
|
@@ -56,9 +56,9 @@ function env(name, legacy) {
|
|
|
56
56
|
if (v !== undefined && v !== "")
|
|
57
57
|
return v;
|
|
58
58
|
}
|
|
59
|
-
// Auto-map FINCH_X →
|
|
59
|
+
// Auto-map FINCH_X → FINCH_X when legacy not passed
|
|
60
60
|
if (name.startsWith("FINCH_")) {
|
|
61
|
-
const auto = "
|
|
61
|
+
const auto = "FINCH_" + name.slice("FINCH_".length);
|
|
62
62
|
const v = process.env[auto];
|
|
63
63
|
if (v !== undefined && v !== "")
|
|
64
64
|
return v;
|
|
@@ -70,7 +70,7 @@ function readConfig() {
|
|
|
70
70
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
71
71
|
return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
|
|
72
72
|
}
|
|
73
|
-
// migrate-read: still pick up old ~/.
|
|
73
|
+
// migrate-read: still pick up old ~/.finch/config.json
|
|
74
74
|
if (fs.existsSync(LEGACY_FILE)) {
|
|
75
75
|
return JSON.parse(fs.readFileSync(LEGACY_FILE, "utf8"));
|
|
76
76
|
}
|
|
@@ -89,7 +89,7 @@ function writeConfig(patch) {
|
|
|
89
89
|
}
|
|
90
90
|
function getSavedToken() {
|
|
91
91
|
// env var always wins over saved config
|
|
92
|
-
return env("FINCH_SESSION_TOKEN", "
|
|
92
|
+
return env("FINCH_SESSION_TOKEN", "FINCH_SESSION_TOKEN") ?? readConfig().sessionToken;
|
|
93
93
|
}
|
|
94
94
|
/** Accept finch_sk_* (new) and noel_sk_* (backend still issues these). */
|
|
95
95
|
function isApiKey(value) {
|
|
@@ -114,16 +114,16 @@ function hydrateEnvFromConfig() {
|
|
|
114
114
|
process.env.OPENAI_BASE_URL = cfg.openaiBaseUrl;
|
|
115
115
|
// Bridge legacy env into FINCH_* so the rest of the codebase can read one name.
|
|
116
116
|
const bridges = [
|
|
117
|
-
["FINCH_SESSION_TOKEN", "
|
|
118
|
-
["FINCH_API_KEY", "
|
|
119
|
-
["FINCH_CONVEX_URL", "
|
|
120
|
-
["FINCH_TOOLS", "
|
|
121
|
-
["FINCH_PROVIDER", "
|
|
122
|
-
["FINCH_MODEL", "
|
|
123
|
-
["FINCH_RPC_URL", "
|
|
124
|
-
["FINCH_BROADCAST_RPC", "
|
|
125
|
-
["FINCH_WALLET_PASSPHRASE", "
|
|
126
|
-
["FINCH_PAYMENT_HEADER", "
|
|
117
|
+
["FINCH_SESSION_TOKEN", "FINCH_SESSION_TOKEN"],
|
|
118
|
+
["FINCH_API_KEY", "FINCH_API_KEY"],
|
|
119
|
+
["FINCH_CONVEX_URL", "FINCH_CONVEX_URL"],
|
|
120
|
+
["FINCH_TOOLS", "FINCH_TOOLS"],
|
|
121
|
+
["FINCH_PROVIDER", "FINCH_PROVIDER"],
|
|
122
|
+
["FINCH_MODEL", "FINCH_MODEL"],
|
|
123
|
+
["FINCH_RPC_URL", "FINCH_RPC_URL"],
|
|
124
|
+
["FINCH_BROADCAST_RPC", "FINCH_BROADCAST_RPC"],
|
|
125
|
+
["FINCH_WALLET_PASSPHRASE", "FINCH_WALLET_PASSPHRASE"],
|
|
126
|
+
["FINCH_PAYMENT_HEADER", "FINCH_PAYMENT_HEADER"],
|
|
127
127
|
];
|
|
128
128
|
for (const [fin, leg] of bridges) {
|
|
129
129
|
if (!process.env[fin] && process.env[leg])
|