@finchagentic/mcp 4.0.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 +21 -0
- package/README.md +345 -0
- package/dist/_http-cache.js +96 -0
- package/dist/agent-loop.js +231 -0
- package/dist/annotations.js +113 -0
- package/dist/cli.js +1195 -0
- package/dist/clink-input.js +15 -0
- package/dist/config.js +132 -0
- package/dist/convex.js +151 -0
- package/dist/dex-pair.js +54 -0
- package/dist/enrichment-router.js +315 -0
- package/dist/index.js +256 -0
- package/dist/llm.js +323 -0
- package/dist/local-memory.js +102 -0
- package/dist/local-vault.js +454 -0
- package/dist/output-schemas.js +551 -0
- package/dist/prompts.js +111 -0
- package/dist/public-url.js +107 -0
- package/dist/resources.js +116 -0
- package/dist/server.js +300 -0
- package/dist/signal-gate.js +57 -0
- package/dist/token-decimals.js +26 -0
- package/dist/token-gate.js +88 -0
- package/dist/tool-filter.js +44 -0
- package/dist/tools/_solidity-scan.js +313 -0
- package/dist/tools/agents.js +729 -0
- package/dist/tools/automation.js +314 -0
- package/dist/tools/base-mcp.js +478 -0
- package/dist/tools/base.js +269 -0
- package/dist/tools/chronicle.js +268 -0
- package/dist/tools/coder.js +94 -0
- package/dist/tools/deep-research.js +1416 -0
- package/dist/tools/defi.js +291 -0
- package/dist/tools/equity.js +364 -0
- package/dist/tools/events.js +182 -0
- package/dist/tools/framework.js +150 -0
- package/dist/tools/github.js +514 -0
- package/dist/tools/insider.js +264 -0
- package/dist/tools/insight.js +634 -0
- package/dist/tools/market.js +555 -0
- package/dist/tools/memory.js +1046 -0
- package/dist/tools/miroshark.js +343 -0
- package/dist/tools/monitor.js +319 -0
- package/dist/tools/os.js +226 -0
- package/dist/tools/packets.js +296 -0
- package/dist/tools/research-chain.js +226 -0
- package/dist/tools/research-compare.js +280 -0
- package/dist/tools/research.js +188 -0
- package/dist/tools/rh-bridge.js +148 -0
- package/dist/tools/rh-mcp.js +1411 -0
- package/dist/tools/rh-orders.js +471 -0
- package/dist/tools/scanner.js +534 -0
- package/dist/tools/vault.js +764 -0
- package/dist/tools/wallet.js +200 -0
- package/dist/types.js +2 -0
- package/dist/wallet.js +184 -0
- package/package.json +87 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const server_js_1 = require("./server.js");
|
|
38
|
+
const tool_filter_js_1 = require("./tool-filter.js");
|
|
39
|
+
const wallet_js_1 = require("./wallet.js");
|
|
40
|
+
const config_js_1 = require("./config.js");
|
|
41
|
+
const clink_input_js_1 = require("./clink-input.js");
|
|
42
|
+
const readline = __importStar(require("readline"));
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
// Always read the version from package.json so banner + boot strings stay in
|
|
46
|
+
// sync after every npm publish - no more hand-edits in three places.
|
|
47
|
+
const PKG_VERSION = (() => {
|
|
48
|
+
try {
|
|
49
|
+
// dist/index.js -> ../package.json (CJS so __dirname is available)
|
|
50
|
+
const raw = fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8");
|
|
51
|
+
return JSON.parse(raw).version ?? "unknown";
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return "unknown";
|
|
55
|
+
}
|
|
56
|
+
})();
|
|
57
|
+
const CONVEX_SITE = process.env.FINCH_CONVEX_URL ?? "https://befitting-porcupine-276.convex.site";
|
|
58
|
+
// ── ANSI helpers ──────────────────────────────────────────────────────────────
|
|
59
|
+
const C = {
|
|
60
|
+
cyan: "\x1b[36m",
|
|
61
|
+
dim: "\x1b[90m",
|
|
62
|
+
white: "\x1b[97m",
|
|
63
|
+
green: "\x1b[32m",
|
|
64
|
+
yellow: "\x1b[33m",
|
|
65
|
+
red: "\x1b[31m",
|
|
66
|
+
reset: "\x1b[0m",
|
|
67
|
+
bold: "\x1b[1m",
|
|
68
|
+
};
|
|
69
|
+
const BANNER = `
|
|
70
|
+
${C.cyan}
|
|
71
|
+
███╗ ██╗ ██████╗ ███████╗██╗ ██████╗██╗ █████╗ ██╗ ██╗
|
|
72
|
+
████╗ ██║██╔═══██╗██╔════╝██║ ██╔════╝██║ ██╔══██╗██║ ██║
|
|
73
|
+
██╔██╗ ██║██║ ██║█████╗ ██║ ██║ ██║ ███████║██║ █╗ ██║
|
|
74
|
+
██║╚██╗██║██║ ██║██╔══╝ ██║ ██║ ██║ ██╔══██║██║███╗██║
|
|
75
|
+
██║ ╚████║╚██████╔╝███████╗███████╗╚██████╗███████╗██║ ██║╚███╔███╔╝
|
|
76
|
+
╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝
|
|
77
|
+
${C.reset}`;
|
|
78
|
+
function line(label, value, color = C.cyan) {
|
|
79
|
+
const pad = " ".repeat(Math.max(0, 12 - label.length));
|
|
80
|
+
process.stderr.write(` ${color}▸ ${label}${C.reset}${pad}${value}\n`);
|
|
81
|
+
}
|
|
82
|
+
function divider() {
|
|
83
|
+
process.stderr.write(` ${C.dim}${"─".repeat(58)}${C.reset}\n`);
|
|
84
|
+
}
|
|
85
|
+
async function checkForUpdate(current) {
|
|
86
|
+
try {
|
|
87
|
+
const res = await fetch("https://registry.npmjs.org/@finchagentic/mcp/latest", {
|
|
88
|
+
signal: AbortSignal.timeout(5000),
|
|
89
|
+
});
|
|
90
|
+
if (!res.ok)
|
|
91
|
+
return;
|
|
92
|
+
const data = await res.json();
|
|
93
|
+
const latest = data.version;
|
|
94
|
+
if (!latest || latest === current)
|
|
95
|
+
return;
|
|
96
|
+
// Show update notice to stderr - visible in Claude Desktop logs and terminal.
|
|
97
|
+
const sep = ` ${"─".repeat(58)}`;
|
|
98
|
+
process.stderr.write(`\n${sep}\n` +
|
|
99
|
+
` ${C.yellow}⚠${C.reset} Update available: ${C.yellow}v${current}${C.reset} → ${C.cyan}v${latest}${C.reset}\n` +
|
|
100
|
+
` ${C.dim}npm install -g @finchagentic/mcp@${latest}${C.reset} ${C.dim}or restart your MCP client${C.reset}\n` +
|
|
101
|
+
`${sep}\n\n`);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Non-blocking - silently ignore network errors
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function main() {
|
|
108
|
+
(0, config_js_1.hydrateEnvFromConfig)();
|
|
109
|
+
process.stderr.write(BANNER);
|
|
110
|
+
const CAT_RULES = [
|
|
111
|
+
{ label: "Market", match: n => /^(get_market_data|get_token_data|compare_tokens|market_overview|token_history)$/.test(n) },
|
|
112
|
+
{ label: "Insight", match: n => /^(ask_finch|market_thesis|trade_plan)$/.test(n) },
|
|
113
|
+
{ label: "DeFi", match: n => n === "get_defi_yields" },
|
|
114
|
+
{ label: "Base MCP", match: n => n.startsWith("base_mcp_") },
|
|
115
|
+
{ label: "RH MCP", match: n => n.startsWith("rh_") },
|
|
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
|
+
{ label: "Vault", match: n => n.startsWith("vault_") },
|
|
119
|
+
{ label: "Wallet", match: n => /^(get_wallet_address|get_wallet_balance|wallet_sign_message)$/.test(n) },
|
|
120
|
+
{ label: "MiroShark", match: n => n.startsWith("miroshark_") },
|
|
121
|
+
{ 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" },
|
|
123
|
+
{ label: "Coder", match: n => n === "audit_contract" },
|
|
124
|
+
{ label: "Base", match: n => /^(query_vaults|list_markets|prepare_deposit|chain_stats)$/.test(n) },
|
|
125
|
+
{ label: "Memory", match: n => n.startsWith("memory_") },
|
|
126
|
+
{ label: "OS", match: n => /^(finch_status|finch_diagnostics|finch_shell_chat)$/.test(n) },
|
|
127
|
+
{ 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) },
|
|
129
|
+
{ label: "GitHub", match: n => n.startsWith("github_") },
|
|
130
|
+
{ label: "Chronicle", match: n => n.startsWith("chronicle_") },
|
|
131
|
+
{ label: "Packets", match: n => n.startsWith("packet_") },
|
|
132
|
+
];
|
|
133
|
+
const categories = CAT_RULES
|
|
134
|
+
.map(rule => {
|
|
135
|
+
const names = server_js_1.ALL_TOOLS.map(t => t.name).filter(rule.match);
|
|
136
|
+
return { label: rule.label, count: names.length, tools: names.join(" · ") };
|
|
137
|
+
})
|
|
138
|
+
.filter(c => c.count > 0);
|
|
139
|
+
const total = server_js_1.ALL_TOOLS.length;
|
|
140
|
+
// What the connected client actually sees - the LIST response is filtered by
|
|
141
|
+
// FINCH_TOOLS (default "core"). Showing this next to the registered total
|
|
142
|
+
// stops the banner from claiming 121 while the client only sees the core set.
|
|
143
|
+
const exposed = (0, tool_filter_js_1.filterTools)(server_js_1.ALL_TOOLS).length;
|
|
144
|
+
const toolMode = process.env.FINCH_TOOLS ?? "core";
|
|
145
|
+
divider();
|
|
146
|
+
process.stderr.write(`\n`);
|
|
147
|
+
// Tools don't run inference - the connected client's model does. A provider
|
|
148
|
+
// is shown only because the CLI agent loop and scheduled agents use one; its
|
|
149
|
+
// absence is the normal, fully-working state for an MCP client.
|
|
150
|
+
const model = process.env.FINCH_MODEL ?? "claude-haiku-4-5-20251001";
|
|
151
|
+
const aiMode = process.env.BANKR_API_KEY
|
|
152
|
+
? `Bankr ${C.dim}${model}${C.reset}`
|
|
153
|
+
: process.env.ANTHROPIC_API_KEY
|
|
154
|
+
? `Anthropic ${C.dim}${model}${C.reset}`
|
|
155
|
+
: process.env.OPENAI_API_KEY
|
|
156
|
+
? `OpenAI ${C.dim}${model}${C.reset}`
|
|
157
|
+
: `your client's model ${C.dim}no key needed${C.reset}`;
|
|
158
|
+
line("version", `v${PKG_VERSION}`);
|
|
159
|
+
line("ai", aiMode);
|
|
160
|
+
const toolDetail = exposed === total
|
|
161
|
+
? `${C.white}${C.bold}${total} tools exposed${C.reset} ${C.dim}across ${categories.length} categories${C.reset}`
|
|
162
|
+
: `${C.white}${C.bold}${exposed} tools exposed${C.reset} ${C.dim}(${toolMode} of ${total} registered · set FINCH_TOOLS=all for every tool)${C.reset}`;
|
|
163
|
+
line("tools", toolDetail);
|
|
164
|
+
process.stderr.write(`\n`);
|
|
165
|
+
divider();
|
|
166
|
+
process.stderr.write(`\n`);
|
|
167
|
+
// ── Categories grid ────────────────────────────────────────────────────────
|
|
168
|
+
for (const cat of categories) {
|
|
169
|
+
const countStr = `${cat.count}`.padStart(2);
|
|
170
|
+
process.stderr.write(` ${C.dim}│${C.reset} ${C.cyan}${cat.label.padEnd(11)}${C.reset} ${C.dim}${countStr}x${C.reset} ${C.dim}${cat.tools}${C.reset}\n`);
|
|
171
|
+
}
|
|
172
|
+
process.stderr.write(`\n`);
|
|
173
|
+
divider();
|
|
174
|
+
// ── Wallet + start ─────────────────────────────────────────────────────────
|
|
175
|
+
await (0, server_js_1.startServer)();
|
|
176
|
+
const hasAuth = !!(0, config_js_1.getSavedToken)();
|
|
177
|
+
try {
|
|
178
|
+
const wallet = await (0, wallet_js_1.getOrCreateWallet)();
|
|
179
|
+
process.stderr.write(`\n`);
|
|
180
|
+
line("wallet", wallet.address);
|
|
181
|
+
if (hasAuth) {
|
|
182
|
+
line("auth", `${C.green}signed in${C.reset} ${C.dim}all ${server_js_1.ALL_TOOLS.length} tools unlocked${C.reset}`, C.green);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
line("auth", `${C.yellow}not signed in${C.reset} ${C.dim}run 'finch login' to unlock premium tools${C.reset}`, C.yellow);
|
|
186
|
+
}
|
|
187
|
+
line("status", `${C.green}ready${C.reset} ${C.dim}waiting for MCP client...${C.reset}`, C.green);
|
|
188
|
+
process.stderr.write(`\n`);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
process.stderr.write(`\n`);
|
|
192
|
+
line("wallet", `${C.yellow}not configured${C.reset} ${C.dim}run 'finch login' to set up${C.reset}`, C.yellow);
|
|
193
|
+
line("status", `${C.green}ready${C.reset} ${C.dim}wallet tools require setup${C.reset}`, C.green);
|
|
194
|
+
process.stderr.write(`\n`);
|
|
195
|
+
}
|
|
196
|
+
// Check for updates async - fires 3s after startup so it doesn't delay boot
|
|
197
|
+
setTimeout(() => { checkForUpdate(PKG_VERSION).catch(() => { }); }, 3000);
|
|
198
|
+
}
|
|
199
|
+
// Interactive sign-in for the `finch-mcp login` path. Kept identical to the
|
|
200
|
+
// canonical `finch login` (cli.ts): both accept a Finch API key
|
|
201
|
+
// (finch_sk_...) and exchange it for a session token at /auth/apikey/login.
|
|
202
|
+
// Earlier this asked for a raw "session token" against /auth/me - a different
|
|
203
|
+
// credential users never actually generate, so the two bins disagreed on what
|
|
204
|
+
// "login" meant.
|
|
205
|
+
async function loginFlow() {
|
|
206
|
+
process.stderr.write(`\n ${C.cyan}▸ finch login${C.reset}\n\n`);
|
|
207
|
+
process.stderr.write(` Generate an API key at ${C.cyan}app.finchagentic.com${C.reset} → Settings → API Keys\n\n`);
|
|
208
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
209
|
+
let apiKey = await new Promise((resolve) => {
|
|
210
|
+
rl.question(` API key (finch_sk_... / noel_sk_...): `, (answer) => {
|
|
211
|
+
rl.close();
|
|
212
|
+
resolve(answer.trim());
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
// Deduplicate doubled input from a known Clink terminal bug + strip non-ASCII.
|
|
216
|
+
apiKey = (0, clink_input_js_1.dedupClinkInput)(apiKey).replace(/[^\x20-\x7E]/g, "").trim();
|
|
217
|
+
if (!(0, config_js_1.isApiKey)(apiKey)) {
|
|
218
|
+
process.stderr.write(`\n ${C.yellow}✗ Invalid key — should start with finch_sk_ or noel_sk_${C.reset}\n\n`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const res = await fetch(`${CONVEX_SITE}/auth/apikey/login`, {
|
|
223
|
+
method: "POST",
|
|
224
|
+
headers: { "Content-Type": "application/json" },
|
|
225
|
+
body: JSON.stringify({ apiKey }),
|
|
226
|
+
signal: AbortSignal.timeout(8000),
|
|
227
|
+
});
|
|
228
|
+
const data = await res.json();
|
|
229
|
+
if (!res.ok || !data.token) {
|
|
230
|
+
process.stderr.write(`\n ${C.yellow}✗ ${data.error ?? "Invalid API key"} — check it at app.finchagentic.com${C.reset}\n\n`);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
(0, config_js_1.writeConfig)({ sessionToken: data.token, email: data.email, name: data.displayName ?? undefined });
|
|
234
|
+
process.stderr.write(`\n ${C.green}✓ Signed in as ${data.email ?? data.displayName ?? "user"}${C.reset}\n`);
|
|
235
|
+
process.stderr.write(` ${C.dim}Token saved to ~/.finch/config.json${C.reset}\n`);
|
|
236
|
+
process.stderr.write(` ${C.dim}All ${server_js_1.ALL_TOOLS.length} tools now unlocked.${C.reset}\n\n`);
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
process.stderr.write(`\n ${C.red}✗ Login failed: ${err.message}${C.reset}\n\n`);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
process.exit(0);
|
|
243
|
+
}
|
|
244
|
+
const cmd = process.argv[2];
|
|
245
|
+
if (cmd === "login") {
|
|
246
|
+
loginFlow().catch((err) => {
|
|
247
|
+
process.stderr.write(`[finch] login error: ${err}\n`);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
main().catch((err) => {
|
|
253
|
+
process.stderr.write(`[finch] fatal: ${err}\n`);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
});
|
|
256
|
+
}
|
package/dist/llm.js
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hasDirectLLMKey = hasDirectLLMKey;
|
|
4
|
+
exports.grokLiveSearchHits = grokLiveSearchHits;
|
|
5
|
+
exports.isGrokActive = isGrokActive;
|
|
6
|
+
exports.callLLM = callLLM;
|
|
7
|
+
const wallet_js_1 = require("./wallet.js");
|
|
8
|
+
const config_js_1 = require("./config.js");
|
|
9
|
+
const ANTHROPIC_URL = "https://api.anthropic.com/v1/messages";
|
|
10
|
+
const BANKR_URL = "https://llm.bankr.bot/v1/chat/completions";
|
|
11
|
+
const GROK_URL = "https://api.x.ai/v1/chat/completions";
|
|
12
|
+
const OPENAI_URL = "https://api.openai.com/v1/chat/completions";
|
|
13
|
+
const CONVEX_SITE = process.env.FINCH_CONVEX_URL ?? "https://befitting-porcupine-276.convex.site";
|
|
14
|
+
/**
|
|
15
|
+
* Returns true if Grok is the currently active LLM provider, based on env.
|
|
16
|
+
* Useful for tools that want to conditionally enable Grok-specific features
|
|
17
|
+
* like Live Search.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* True if any BYOK provider key (Bankr/Anthropic/OpenAI/Grok) is set. Callers
|
|
21
|
+
* that must never fall through to callViaConvex() - e.g. local-memory mode,
|
|
22
|
+
* which promises zero Convex involvement - should check this first and fail
|
|
23
|
+
* clearly instead of letting callLLM() silently proxy through Finch.
|
|
24
|
+
*/
|
|
25
|
+
function hasDirectLLMKey() {
|
|
26
|
+
return !!(process.env.BANKR_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY || process.env.GROK_API_KEY);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Grok Live Search used purely as a RETRIEVAL layer.
|
|
30
|
+
*
|
|
31
|
+
* X/news content is the one thing an MCP client genuinely cannot reach on its
|
|
32
|
+
* own, which makes fetching it real tool work. Interpreting it is not — so this
|
|
33
|
+
* asks Grok to return the found items verbatim and hands them upward as sources.
|
|
34
|
+
* The caller's model decides what any of it means.
|
|
35
|
+
*
|
|
36
|
+
* Returns [] on any failure: live search is an augmentation, never a hard
|
|
37
|
+
* dependency, and research must still work without a Grok key.
|
|
38
|
+
*/
|
|
39
|
+
async function grokLiveSearchHits(query, sources = ["x", "news", "web"], days, maxResults = 10) {
|
|
40
|
+
const apiKey = process.env.GROK_API_KEY;
|
|
41
|
+
if (!apiKey)
|
|
42
|
+
return [];
|
|
43
|
+
const body = {
|
|
44
|
+
model: process.env.FINCH_GROK_MODEL ?? "grok-4.3",
|
|
45
|
+
max_tokens: 2000,
|
|
46
|
+
messages: [
|
|
47
|
+
{
|
|
48
|
+
role: "system",
|
|
49
|
+
content: "You are a retrieval tool, not an analyst. Return what you find verbatim. " +
|
|
50
|
+
"Never summarise, interpret, rank by opinion, or add commentary.",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
role: "user",
|
|
54
|
+
content: `Search for: ${query}\n\n` +
|
|
55
|
+
`Return up to ${maxResults} of the most relevant recent items, one per line, ` +
|
|
56
|
+
`in exactly this format:\n\n` +
|
|
57
|
+
`URL :: verbatim quote or post text (max 400 chars)\n\n` +
|
|
58
|
+
`Rules: quote the source's own words. Do not paraphrase. Do not add analysis, ` +
|
|
59
|
+
`conclusions, or your own framing. If an item has no URL, use "-" for the URL.`,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
search_parameters: {
|
|
63
|
+
mode: "on",
|
|
64
|
+
sources: sources.map((type) => ({ type })),
|
|
65
|
+
max_search_results: maxResults,
|
|
66
|
+
...(days ? { from_date: new Date(Date.now() - days * 86400000).toISOString().slice(0, 10) } : {}),
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
try {
|
|
70
|
+
const res = await fetch(GROK_URL, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
73
|
+
body: JSON.stringify(body),
|
|
74
|
+
signal: AbortSignal.timeout(45000),
|
|
75
|
+
});
|
|
76
|
+
if (!res.ok)
|
|
77
|
+
return [];
|
|
78
|
+
const data = (await res.json());
|
|
79
|
+
const content = data.choices?.[0]?.message?.content ?? "";
|
|
80
|
+
const hits = [];
|
|
81
|
+
for (const line of content.split("\n")) {
|
|
82
|
+
const idx = line.indexOf("::");
|
|
83
|
+
if (idx === -1)
|
|
84
|
+
continue;
|
|
85
|
+
const url = line.slice(0, idx).trim().replace(/^[-*\d.\s]+/, "");
|
|
86
|
+
const excerpt = line.slice(idx + 2).trim();
|
|
87
|
+
if (excerpt.length < 20)
|
|
88
|
+
continue;
|
|
89
|
+
hits.push({ url: /^https?:\/\//.test(url) ? url : "", excerpt });
|
|
90
|
+
}
|
|
91
|
+
// If the model ignored the format, fall back to bare citations so the
|
|
92
|
+
// caller still receives the URLs rather than nothing.
|
|
93
|
+
if (hits.length === 0 && data.citations?.length) {
|
|
94
|
+
return data.citations.slice(0, maxResults).map((url) => ({ url, excerpt: "" }));
|
|
95
|
+
}
|
|
96
|
+
return hits.slice(0, maxResults);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function isGrokActive() {
|
|
103
|
+
const provider = process.env.FINCH_PROVIDER?.toLowerCase().trim();
|
|
104
|
+
if (provider === "grok")
|
|
105
|
+
return !!process.env.GROK_API_KEY;
|
|
106
|
+
if (provider === "bankr" || provider === "anthropic" || provider === "openai")
|
|
107
|
+
return false;
|
|
108
|
+
// Auto-priority - Grok is only active if it's the only key present
|
|
109
|
+
if (process.env.BANKR_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY)
|
|
110
|
+
return false;
|
|
111
|
+
return !!process.env.GROK_API_KEY;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Call the best available LLM.
|
|
115
|
+
*
|
|
116
|
+
* Provider auto-priority (when FINCH_PROVIDER unset):
|
|
117
|
+
* BANKR_API_KEY → ANTHROPIC_API_KEY → OPENAI_API_KEY → GROK_API_KEY → Convex backend
|
|
118
|
+
*
|
|
119
|
+
* Force a provider via FINCH_PROVIDER: "bankr" | "anthropic" | "openai" | "grok"
|
|
120
|
+
*
|
|
121
|
+
* Model selection (first wins):
|
|
122
|
+
* FINCH_MODEL → {provider}_MODEL → provider default
|
|
123
|
+
*/
|
|
124
|
+
async function callLLM(systemPrompt, userPrompt, maxTokens = 1024, history = [], timeoutMs = 60000, options = {}) {
|
|
125
|
+
const provider = process.env.FINCH_PROVIDER?.toLowerCase().trim();
|
|
126
|
+
const bankrKey = process.env.BANKR_API_KEY;
|
|
127
|
+
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
128
|
+
const openaiKey = process.env.OPENAI_API_KEY;
|
|
129
|
+
const grokKey = process.env.GROK_API_KEY;
|
|
130
|
+
// Explicit provider override - user picked one
|
|
131
|
+
if (provider === "grok" && grokKey)
|
|
132
|
+
return callGrok(grokKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, options.liveSearch, options.model);
|
|
133
|
+
if (provider === "anthropic" && anthropicKey)
|
|
134
|
+
return callAnthropic(anthropicKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, options.model);
|
|
135
|
+
if (provider === "openai" && openaiKey)
|
|
136
|
+
return callOpenAI(openaiKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, options.model);
|
|
137
|
+
if (provider === "bankr" && bankrKey)
|
|
138
|
+
return callBankr(bankrKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, options.model);
|
|
139
|
+
// Auto priority
|
|
140
|
+
if (bankrKey)
|
|
141
|
+
return callBankr(bankrKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, options.model);
|
|
142
|
+
if (anthropicKey)
|
|
143
|
+
return callAnthropic(anthropicKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, options.model);
|
|
144
|
+
if (openaiKey)
|
|
145
|
+
return callOpenAI(openaiKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, options.model);
|
|
146
|
+
if (grokKey)
|
|
147
|
+
return callGrok(grokKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, options.liveSearch, options.model);
|
|
148
|
+
// Fallback: route through Convex backend - owner covers cost
|
|
149
|
+
return callViaConvex(systemPrompt, userPrompt, history, timeoutMs);
|
|
150
|
+
}
|
|
151
|
+
async function callViaConvex(systemPrompt, userPrompt, history, timeoutMs) {
|
|
152
|
+
const fullQuestion = systemPrompt
|
|
153
|
+
? `[System: ${systemPrompt}]\n\n${userPrompt}`
|
|
154
|
+
: userPrompt;
|
|
155
|
+
const headers = { "Content-Type": "application/json" };
|
|
156
|
+
const apiKey = process.env.FINCH_API_KEY;
|
|
157
|
+
let sessionToken = process.env.FINCH_SESSION_TOKEN;
|
|
158
|
+
// Read from config file — this is the resolved session token after login
|
|
159
|
+
try {
|
|
160
|
+
const cfg = (0, config_js_1.readConfig)();
|
|
161
|
+
if (cfg.sessionToken)
|
|
162
|
+
sessionToken = cfg.sessionToken;
|
|
163
|
+
}
|
|
164
|
+
catch { /* ignore */ }
|
|
165
|
+
// Prefer session token (resolved by backend) over API key for /mcp/chat
|
|
166
|
+
if (sessionToken) {
|
|
167
|
+
headers["Authorization"] = `Bearer ${sessionToken}`;
|
|
168
|
+
}
|
|
169
|
+
else if (apiKey) {
|
|
170
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
try {
|
|
174
|
+
const { address, signature, timestamp } = await (0, wallet_js_1.signRequest)("ask_finch");
|
|
175
|
+
headers["X-Wallet-Address"] = address;
|
|
176
|
+
headers["X-Wallet-Signature"] = signature;
|
|
177
|
+
headers["X-Wallet-Timestamp"] = timestamp;
|
|
178
|
+
}
|
|
179
|
+
catch { /* proceed without wallet auth */ }
|
|
180
|
+
}
|
|
181
|
+
const res = await fetch(`${CONVEX_SITE}/mcp/chat`, {
|
|
182
|
+
method: "POST",
|
|
183
|
+
headers,
|
|
184
|
+
body: JSON.stringify({ question: fullQuestion, messages: history }),
|
|
185
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
186
|
+
});
|
|
187
|
+
if (!res.ok) {
|
|
188
|
+
const body = await res.text().catch(() => "");
|
|
189
|
+
throw new Error(`LLM error ${res.status}: ${body.slice(0, 200)}`);
|
|
190
|
+
}
|
|
191
|
+
const data = await res.json();
|
|
192
|
+
return data.answer ?? "";
|
|
193
|
+
}
|
|
194
|
+
async function callAnthropic(apiKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, modelOverride) {
|
|
195
|
+
const messages = [...history, { role: "user", content: userPrompt }];
|
|
196
|
+
const model = modelOverride ?? process.env.FINCH_MODEL ?? process.env.ANTHROPIC_MODEL ?? "claude-haiku-4-5-20251001";
|
|
197
|
+
const res = await fetch(ANTHROPIC_URL, {
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers: {
|
|
200
|
+
"Content-Type": "application/json",
|
|
201
|
+
"x-api-key": apiKey,
|
|
202
|
+
"anthropic-version": "2023-06-01",
|
|
203
|
+
},
|
|
204
|
+
body: JSON.stringify({ model, max_tokens: maxTokens, system: systemPrompt, messages }),
|
|
205
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
206
|
+
});
|
|
207
|
+
if (!res.ok) {
|
|
208
|
+
const body = await res.text().catch(() => "");
|
|
209
|
+
throw new Error(`Anthropic error ${res.status}: ${body.slice(0, 200)}`);
|
|
210
|
+
}
|
|
211
|
+
const data = await res.json();
|
|
212
|
+
return data.content?.find(b => b.type === "text")?.text ?? "";
|
|
213
|
+
}
|
|
214
|
+
async function callBankr(apiKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, modelOverride) {
|
|
215
|
+
const model = modelOverride ?? process.env.FINCH_MODEL ?? process.env.BANKR_MODEL ?? "claude-haiku-4-5-20251001";
|
|
216
|
+
const res = await fetch(BANKR_URL, {
|
|
217
|
+
method: "POST",
|
|
218
|
+
headers: { "Content-Type": "application/json", "X-API-Key": apiKey },
|
|
219
|
+
body: JSON.stringify({
|
|
220
|
+
model,
|
|
221
|
+
messages: [
|
|
222
|
+
{ role: "system", content: systemPrompt },
|
|
223
|
+
...history,
|
|
224
|
+
{ role: "user", content: userPrompt },
|
|
225
|
+
],
|
|
226
|
+
max_tokens: maxTokens,
|
|
227
|
+
}),
|
|
228
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
229
|
+
});
|
|
230
|
+
if (!res.ok) {
|
|
231
|
+
const body = await res.text().catch(() => "");
|
|
232
|
+
throw new Error(`Bankr error ${res.status}: ${body.slice(0, 200)}`);
|
|
233
|
+
}
|
|
234
|
+
const data = await res.json();
|
|
235
|
+
return data.choices?.[0]?.message?.content ?? "";
|
|
236
|
+
}
|
|
237
|
+
// OPENAI_BASE_URL lets this point at any OpenAI Chat Completions-compatible
|
|
238
|
+
// endpoint instead of api.openai.com - self-hosted gateways (LiteLLM, vLLM,
|
|
239
|
+
// Ollama, LocalAI) and aggregators (OpenRouter) all speak this same API
|
|
240
|
+
// shape, so no separate provider code is needed for them.
|
|
241
|
+
function openAiChatUrl() {
|
|
242
|
+
const base = process.env.OPENAI_BASE_URL?.replace(/\/+$/, "");
|
|
243
|
+
return base ? `${base}/chat/completions` : OPENAI_URL;
|
|
244
|
+
}
|
|
245
|
+
async function callOpenAI(apiKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, modelOverride) {
|
|
246
|
+
const model = modelOverride ?? process.env.FINCH_MODEL ?? process.env.OPENAI_MODEL ?? "gpt-4o-mini";
|
|
247
|
+
const res = await fetch(openAiChatUrl(), {
|
|
248
|
+
method: "POST",
|
|
249
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
250
|
+
body: JSON.stringify({
|
|
251
|
+
model,
|
|
252
|
+
messages: [
|
|
253
|
+
{ role: "system", content: systemPrompt },
|
|
254
|
+
...history,
|
|
255
|
+
{ role: "user", content: userPrompt },
|
|
256
|
+
],
|
|
257
|
+
max_tokens: maxTokens,
|
|
258
|
+
}),
|
|
259
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
260
|
+
});
|
|
261
|
+
if (!res.ok) {
|
|
262
|
+
const body = await res.text().catch(() => "");
|
|
263
|
+
throw new Error(`OpenAI error ${res.status}: ${body.slice(0, 200)}`);
|
|
264
|
+
}
|
|
265
|
+
const data = await res.json();
|
|
266
|
+
return data.choices?.[0]?.message?.content ?? "";
|
|
267
|
+
}
|
|
268
|
+
async function callGrok(apiKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, liveSearch, modelOverride) {
|
|
269
|
+
const model = modelOverride ?? process.env.FINCH_MODEL ?? process.env.GROK_MODEL ?? "grok-4-fast-reasoning";
|
|
270
|
+
const body = {
|
|
271
|
+
model,
|
|
272
|
+
messages: [
|
|
273
|
+
{ role: "system", content: systemPrompt },
|
|
274
|
+
...history,
|
|
275
|
+
{ role: "user", content: userPrompt },
|
|
276
|
+
],
|
|
277
|
+
max_tokens: maxTokens,
|
|
278
|
+
stream: false,
|
|
279
|
+
};
|
|
280
|
+
// xAI Live Search - pulls real-time results from web/X/news/RSS during inference.
|
|
281
|
+
// Docs: https://docs.x.ai/docs/guides/live-search
|
|
282
|
+
// Note: as of late 2026 xAI deprecated this in favor of Agent Tools API
|
|
283
|
+
// (returns 410 Gone). We detect that and retry without search_parameters
|
|
284
|
+
// so synthesis still succeeds - just without the real-time augmentation.
|
|
285
|
+
if (liveSearch) {
|
|
286
|
+
body.search_parameters = {
|
|
287
|
+
mode: liveSearch.mode,
|
|
288
|
+
sources: liveSearch.sources.map((type) => ({ type })),
|
|
289
|
+
max_search_results: liveSearch.maxResults ?? 10,
|
|
290
|
+
...(liveSearch.fromDate ? { from_date: liveSearch.fromDate } : {}),
|
|
291
|
+
...(liveSearch.toDate ? { to_date: liveSearch.toDate } : {}),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
let res = await fetch(GROK_URL, {
|
|
295
|
+
method: "POST",
|
|
296
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
|
|
297
|
+
body: JSON.stringify(body),
|
|
298
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
299
|
+
});
|
|
300
|
+
// Live Search deprecated → strip search_parameters and retry
|
|
301
|
+
if (res.status === 410 && liveSearch) {
|
|
302
|
+
delete body.search_parameters;
|
|
303
|
+
res = await fetch(GROK_URL, {
|
|
304
|
+
method: "POST",
|
|
305
|
+
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
|
|
306
|
+
body: JSON.stringify(body),
|
|
307
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
if (!res.ok) {
|
|
311
|
+
const errBody = await res.text().catch(() => "");
|
|
312
|
+
throw new Error(`Grok error ${res.status}: ${errBody.slice(0, 200)}`);
|
|
313
|
+
}
|
|
314
|
+
const data = await res.json();
|
|
315
|
+
const content = data.choices?.[0]?.message?.content ?? "";
|
|
316
|
+
// When Live Search ran (and was not deprecated), append the citations as a
|
|
317
|
+
// parsable block at the bottom - downstream consumers (deep_research) can
|
|
318
|
+
// read these and merge into the final source list.
|
|
319
|
+
if (liveSearch && data.citations && data.citations.length > 0) {
|
|
320
|
+
return `${content}\n\n<!--GROK_LIVE_CITATIONS\n${data.citations.join("\n")}\nGROK_LIVE_CITATIONS-->`;
|
|
321
|
+
}
|
|
322
|
+
return content;
|
|
323
|
+
}
|