@finchagentic/mcp 4.0.0 → 4.1.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 +73 -6
- package/dist/cli.js +78 -21
- package/dist/config.js +16 -16
- package/dist/llm.js +12 -48
- package/dist/local-memory-file.js +134 -0
- package/dist/local-memory.js +29 -9
- package/dist/resources.js +2 -2
- package/dist/tools/agents.js +4 -4
- package/dist/tools/base-mcp.js +4 -4
- package/dist/tools/chronicle.js +4 -4
- package/dist/tools/deep-research.js +10 -5
- package/dist/tools/defi.js +26 -5
- package/dist/tools/insight.js +23 -27
- package/dist/tools/memory.js +33 -29
- package/dist/tools/monitor.js +1 -1
- package/dist/tools/os.js +10 -5
- package/dist/tools/packets.js +3 -3
- package/dist/tools/research.js +2 -2
- package/dist/tools/rh-mcp.js +12 -0
- package/dist/tools/vault.js +23 -21
- package/dist/wallet.js +93 -16
- package/package.json +8 -20
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])
|
package/dist/llm.js
CHANGED
|
@@ -4,13 +4,10 @@ exports.hasDirectLLMKey = hasDirectLLMKey;
|
|
|
4
4
|
exports.grokLiveSearchHits = grokLiveSearchHits;
|
|
5
5
|
exports.isGrokActive = isGrokActive;
|
|
6
6
|
exports.callLLM = callLLM;
|
|
7
|
-
const wallet_js_1 = require("./wallet.js");
|
|
8
|
-
const config_js_1 = require("./config.js");
|
|
9
7
|
const ANTHROPIC_URL = "https://api.anthropic.com/v1/messages";
|
|
10
8
|
const BANKR_URL = "https://llm.bankr.bot/v1/chat/completions";
|
|
11
9
|
const GROK_URL = "https://api.x.ai/v1/chat/completions";
|
|
12
10
|
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
11
|
/**
|
|
15
12
|
* Returns true if Grok is the currently active LLM provider, based on env.
|
|
16
13
|
* Useful for tools that want to conditionally enable Grok-specific features
|
|
@@ -145,51 +142,18 @@ async function callLLM(systemPrompt, userPrompt, maxTokens = 1024, history = [],
|
|
|
145
142
|
return callOpenAI(openaiKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, options.model);
|
|
146
143
|
if (grokKey)
|
|
147
144
|
return callGrok(grokKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, options.liveSearch, options.model);
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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 ?? "";
|
|
145
|
+
// No provider key configured - BYOK is required. This tool needs its own
|
|
146
|
+
// multi-step reasoning (planning, synthesis, critique) that an MCP host's
|
|
147
|
+
// model cannot do on the tool's behalf, so there is no free path: bring
|
|
148
|
+
// your own key or don't use tools that need one (ask_finch, deep_research,
|
|
149
|
+
// market_thesis, trade_plan, scheduled agent runs). Fails clearly instead
|
|
150
|
+
// of silently billing the Finch deployment owner for every anonymous
|
|
151
|
+
// install of this package.
|
|
152
|
+
throw new Error("No LLM provider configured. This tool needs its own key to do multi-step " +
|
|
153
|
+
"reasoning server-side - set one of BANKR_API_KEY, ANTHROPIC_API_KEY, " +
|
|
154
|
+
"OPENAI_API_KEY, or GROK_API_KEY as an environment variable (see the " +
|
|
155
|
+
"Configuration section of the README), then retry. Most other Finch tools " +
|
|
156
|
+
"don't need this - only ones that do their own internal LLM reasoning do.");
|
|
193
157
|
}
|
|
194
158
|
async function callAnthropic(apiKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs, modelOverride) {
|
|
195
159
|
const messages = [...history, { role: "user", content: userPrompt }];
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getLocalMemoryFileConfig = getLocalMemoryFileConfig;
|
|
37
|
+
exports.fileMemoryAdd = fileMemoryAdd;
|
|
38
|
+
exports.fileMemorySearch = fileMemorySearch;
|
|
39
|
+
exports.fileMemoryList = fileMemoryList;
|
|
40
|
+
exports.fileMemoryDelete = fileMemoryDelete;
|
|
41
|
+
exports.fileMemoryProfile = fileMemoryProfile;
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const os = __importStar(require("os"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const crypto = __importStar(require("crypto"));
|
|
46
|
+
function getLocalMemoryFileConfig() {
|
|
47
|
+
return { dir: path.join(os.homedir(), ".finch", "memory") };
|
|
48
|
+
}
|
|
49
|
+
function indexPath(cfg) {
|
|
50
|
+
return path.join(cfg.dir, "index.json");
|
|
51
|
+
}
|
|
52
|
+
function readIndex(cfg) {
|
|
53
|
+
try {
|
|
54
|
+
const raw = fs.readFileSync(indexPath(cfg), "utf8");
|
|
55
|
+
const parsed = JSON.parse(raw);
|
|
56
|
+
return { memories: parsed.memories ?? [] };
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return { memories: [] };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Atomic write: temp file + rename, matching local-vault.ts's pattern so a
|
|
63
|
+
// crash mid-write can't corrupt the index every memory depends on.
|
|
64
|
+
function writeIndex(cfg, idx) {
|
|
65
|
+
fs.mkdirSync(cfg.dir, { recursive: true });
|
|
66
|
+
const tmp = indexPath(cfg) + `.tmp-${process.pid}`;
|
|
67
|
+
fs.writeFileSync(tmp, JSON.stringify(idx, null, 2), "utf8");
|
|
68
|
+
fs.renameSync(tmp, indexPath(cfg));
|
|
69
|
+
}
|
|
70
|
+
function toResult(m) {
|
|
71
|
+
return {
|
|
72
|
+
id: m.id,
|
|
73
|
+
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
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function fileMemoryAdd(cfg, content, metadata, sourceUrl) {
|
|
78
|
+
const idx = readIndex(cfg);
|
|
79
|
+
const id = crypto.randomBytes(8).toString("hex");
|
|
80
|
+
const row = {
|
|
81
|
+
id,
|
|
82
|
+
content,
|
|
83
|
+
title: metadata.title,
|
|
84
|
+
tags: metadata.tags,
|
|
85
|
+
source: metadata.source,
|
|
86
|
+
sourceUrl,
|
|
87
|
+
contentHash: metadata.contentHash ?? "",
|
|
88
|
+
pinned: metadata.pinned,
|
|
89
|
+
addedAt: metadata.addedAt ?? Date.now(),
|
|
90
|
+
};
|
|
91
|
+
idx.memories.push(row);
|
|
92
|
+
writeIndex(cfg, idx);
|
|
93
|
+
return { id };
|
|
94
|
+
}
|
|
95
|
+
function fileMemorySearch(cfg, query, limit) {
|
|
96
|
+
const idx = readIndex(cfg);
|
|
97
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
98
|
+
const scored = [];
|
|
99
|
+
for (const m of idx.memories) {
|
|
100
|
+
const hay = `${m.title ?? ""}\n${(m.tags ?? []).join(" ")}\n${m.content}`.toLowerCase();
|
|
101
|
+
let score = 0;
|
|
102
|
+
for (const t of terms) {
|
|
103
|
+
if ((m.title ?? "").toLowerCase().includes(t))
|
|
104
|
+
score += 3;
|
|
105
|
+
if ((m.tags ?? []).some((tag) => tag.toLowerCase().includes(t)))
|
|
106
|
+
score += 2;
|
|
107
|
+
score += Math.min(hay.split(t).length - 1, 5);
|
|
108
|
+
}
|
|
109
|
+
if (score > 0)
|
|
110
|
+
scored.push({ m, score });
|
|
111
|
+
}
|
|
112
|
+
scored.sort((a, b) => b.score - a.score);
|
|
113
|
+
const top = scored.slice(0, limit);
|
|
114
|
+
const maxScore = top[0]?.score || 1;
|
|
115
|
+
return top.map(({ m, score }) => ({ ...toResult(m), score: score / maxScore }));
|
|
116
|
+
}
|
|
117
|
+
function fileMemoryList(cfg, limit, tag) {
|
|
118
|
+
const idx = readIndex(cfg);
|
|
119
|
+
let rows = [...idx.memories].sort((a, b) => b.addedAt - a.addedAt);
|
|
120
|
+
if (tag)
|
|
121
|
+
rows = rows.filter((m) => (m.tags ?? []).includes(tag));
|
|
122
|
+
return rows.slice(0, limit).map(toResult);
|
|
123
|
+
}
|
|
124
|
+
function fileMemoryDelete(cfg, id) {
|
|
125
|
+
const idx = readIndex(cfg);
|
|
126
|
+
const next = idx.memories.filter((m) => m.id !== id);
|
|
127
|
+
if (next.length === idx.memories.length)
|
|
128
|
+
throw new Error(`Memory not found: ${id}`);
|
|
129
|
+
writeIndex(cfg, { memories: next });
|
|
130
|
+
}
|
|
131
|
+
function fileMemoryProfile(cfg) {
|
|
132
|
+
const idx = readIndex(cfg);
|
|
133
|
+
return { total: idx.memories.length, status: "ok", space: "local" };
|
|
134
|
+
}
|
package/dist/local-memory.js
CHANGED
|
@@ -8,18 +8,22 @@ exports.localMemoryList = localMemoryList;
|
|
|
8
8
|
exports.localMemoryDelete = localMemoryDelete;
|
|
9
9
|
exports.localMemoryProfile = localMemoryProfile;
|
|
10
10
|
const config_js_1 = require("./config.js");
|
|
11
|
+
const local_memory_file_js_1 = require("./local-memory-file.js");
|
|
11
12
|
const DEFAULT_URL = "http://localhost:6767";
|
|
12
13
|
const REACHABILITY_TIMEOUT_MS = 1500;
|
|
13
14
|
const CALL_TIMEOUT_MS = 15000;
|
|
14
|
-
// Returns config only when the user has explicitly opted into local
|
|
15
|
-
//
|
|
16
|
-
//
|
|
15
|
+
// Returns config only when the user has explicitly opted into a local
|
|
16
|
+
// backend. Callers should still treat a null return as "use Convex" - this
|
|
17
|
+
// never throws.
|
|
17
18
|
function getLocalMemoryConfig() {
|
|
18
19
|
try {
|
|
19
20
|
const cfg = (0, config_js_1.readConfig)();
|
|
20
|
-
if (cfg.memoryBackend
|
|
21
|
-
return
|
|
22
|
-
|
|
21
|
+
if (cfg.memoryBackend === "local-file")
|
|
22
|
+
return { kind: "file", ...(0, local_memory_file_js_1.getLocalMemoryFileConfig)() };
|
|
23
|
+
if (cfg.memoryBackend === "local" && cfg.supermemoryApiKey) {
|
|
24
|
+
return { kind: "supermemory", url: cfg.supermemoryUrl ?? DEFAULT_URL, apiKey: cfg.supermemoryApiKey };
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
23
27
|
}
|
|
24
28
|
catch {
|
|
25
29
|
return null;
|
|
@@ -28,7 +32,10 @@ function getLocalMemoryConfig() {
|
|
|
28
32
|
// Requires a genuine 2xx, not just "something answered" - a stale/wrong
|
|
29
33
|
// supermemoryApiKey returns 401 (server is up, but every real memory call
|
|
30
34
|
// will fail the same way), and that must show as unhealthy, not healthy.
|
|
35
|
+
// The file backend is always "reachable" - it's a directory, not a server.
|
|
31
36
|
async function isLocalMemoryReachable(cfg) {
|
|
37
|
+
if (cfg.kind === "file")
|
|
38
|
+
return true;
|
|
32
39
|
try {
|
|
33
40
|
const res = await fetch(`${cfg.url}/v3/search`, {
|
|
34
41
|
method: "POST",
|
|
@@ -43,6 +50,8 @@ async function isLocalMemoryReachable(cfg) {
|
|
|
43
50
|
}
|
|
44
51
|
}
|
|
45
52
|
async function localMemoryAdd(cfg, content, metadata, sourceUrl) {
|
|
53
|
+
if (cfg.kind === "file")
|
|
54
|
+
return (0, local_memory_file_js_1.fileMemoryAdd)(cfg, content, metadata, sourceUrl);
|
|
46
55
|
const res = await fetch(`${cfg.url}/v3/documents`, {
|
|
47
56
|
method: "POST",
|
|
48
57
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.apiKey}` },
|
|
@@ -55,6 +64,8 @@ async function localMemoryAdd(cfg, content, metadata, sourceUrl) {
|
|
|
55
64
|
return { id: data.id ?? data.documentId ?? "saved" };
|
|
56
65
|
}
|
|
57
66
|
async function localMemorySearch(cfg, query, limit) {
|
|
67
|
+
if (cfg.kind === "file")
|
|
68
|
+
return (0, local_memory_file_js_1.fileMemorySearch)(cfg, query, limit);
|
|
58
69
|
const res = await fetch(`${cfg.url}/v3/search`, {
|
|
59
70
|
method: "POST",
|
|
60
71
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${cfg.apiKey}` },
|
|
@@ -76,6 +87,8 @@ async function localMemorySearch(cfg, query, limit) {
|
|
|
76
87
|
// server, so callers pass "*" and post-filter by tag client-side, matching
|
|
77
88
|
// how the Convex-side /memory/list already does its own tag post-filter.
|
|
78
89
|
async function localMemoryList(cfg, limit, tag) {
|
|
90
|
+
if (cfg.kind === "file")
|
|
91
|
+
return (0, local_memory_file_js_1.fileMemoryList)(cfg, limit, tag);
|
|
79
92
|
const rows = await localMemorySearch(cfg, "*", Math.max(limit * (tag ? 3 : 1), limit));
|
|
80
93
|
const filtered = tag ? rows.filter((r) => Array.isArray(r.metadata?.tags) && r.metadata.tags.includes(tag)) : rows;
|
|
81
94
|
return filtered.slice(0, limit);
|
|
@@ -84,6 +97,10 @@ async function localMemoryList(cfg, limit, tag) {
|
|
|
84
97
|
// time this was written - throws a distinct error so callers can surface a
|
|
85
98
|
// clear "not supported locally yet" message instead of a generic failure.
|
|
86
99
|
async function localMemoryDelete(cfg, id) {
|
|
100
|
+
if (cfg.kind === "file") {
|
|
101
|
+
(0, local_memory_file_js_1.fileMemoryDelete)(cfg, id);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
87
104
|
const res = await fetch(`${cfg.url}/v3/documents/${encodeURIComponent(id)}`, {
|
|
88
105
|
method: "DELETE",
|
|
89
106
|
headers: { Authorization: `Bearer ${cfg.apiKey}` },
|
|
@@ -92,11 +109,14 @@ async function localMemoryDelete(cfg, id) {
|
|
|
92
109
|
if (!res.ok)
|
|
93
110
|
throw new Error(`local supermemory delete failed: HTTP ${res.status}`);
|
|
94
111
|
}
|
|
95
|
-
// No dedicated count endpoint is documented for the local server,
|
|
96
|
-
// approximates via a capped wildcard search - accurate up to
|
|
97
|
-
// reported as a floor ("200+") beyond that rather
|
|
112
|
+
// No dedicated count endpoint is documented for the local supermemory server,
|
|
113
|
+
// so that path approximates via a capped wildcard search - accurate up to
|
|
114
|
+
// `PROFILE_SAMPLE_LIMIT`, reported as a floor ("200+") beyond that rather
|
|
115
|
+
// than a false exact count. The file backend counts exactly.
|
|
98
116
|
const PROFILE_SAMPLE_LIMIT = 200;
|
|
99
117
|
async function localMemoryProfile(cfg) {
|
|
118
|
+
if (cfg.kind === "file")
|
|
119
|
+
return { ...(0, local_memory_file_js_1.fileMemoryProfile)(cfg), approximate: false };
|
|
100
120
|
const rows = await localMemorySearch(cfg, "*", PROFILE_SAMPLE_LIMIT);
|
|
101
121
|
return { total: rows.length, status: "ok", space: "local", approximate: rows.length >= PROFILE_SAMPLE_LIMIT };
|
|
102
122
|
}
|
package/dist/resources.js
CHANGED
|
@@ -50,7 +50,7 @@ async function listVaultResources(cursor) {
|
|
|
50
50
|
try {
|
|
51
51
|
const offset = decodeCursor(cursor);
|
|
52
52
|
// Over-fetch by 1 so we can tell if there's another page without a count call.
|
|
53
|
-
const data = await (0, convex_js_1.callConvex)(`/vault/list?limit=${PAGE_SIZE + 1}&offset=${offset}`, "GET", undefined, "
|
|
53
|
+
const data = await (0, convex_js_1.callConvex)(`/vault/list?limit=${PAGE_SIZE + 1}&offset=${offset}`, "GET", undefined, "vault_list");
|
|
54
54
|
const allEntries = data.entries ?? data.results ?? [];
|
|
55
55
|
const hasMore = allEntries.length > PAGE_SIZE;
|
|
56
56
|
const entries = hasMore ? allEntries.slice(0, PAGE_SIZE) : allEntries;
|
|
@@ -77,7 +77,7 @@ async function readVaultResource(uri) {
|
|
|
77
77
|
throw new Error(`Unknown resource URI: ${uri}`);
|
|
78
78
|
}
|
|
79
79
|
const key = decodeURIComponent(uri.slice(URI_PREFIX.length));
|
|
80
|
-
const data = await (0, convex_js_1.callConvex)(`/vault/entry?key=${encodeURIComponent(key)}`, "GET", undefined, "
|
|
80
|
+
const data = await (0, convex_js_1.callConvex)(`/vault/entry?key=${encodeURIComponent(key)}`, "GET", undefined, "vault_read");
|
|
81
81
|
if (data.error) {
|
|
82
82
|
throw new Error(`Vault entry not found: ${key}`);
|
|
83
83
|
}
|
package/dist/tools/agents.js
CHANGED
|
@@ -38,10 +38,10 @@ const LEARNING_SYSTEM_PROMPT = [
|
|
|
38
38
|
"- Do not output explanations, prefixes, or quotation marks. Just the sentence or NONE.",
|
|
39
39
|
].join("\n");
|
|
40
40
|
async function extractLearning(goal, priorUpdates, priorLearnings, latest) {
|
|
41
|
-
// Fast skip when no LLM key is configured
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
41
|
+
// Fast skip when no LLM key is configured - callLLM() now throws
|
|
42
|
+
// immediately rather than proxying through Finch (BYOK is required), and
|
|
43
|
+
// the catch below swallows that into a silent skip. This early return just
|
|
44
|
+
// avoids building the prompt for a call we already know will fail.
|
|
45
45
|
const hasLLM = !!(process.env.BANKR_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.GROK_API_KEY);
|
|
46
46
|
if (!hasLLM && !process.env.FINCH_SESSION_TOKEN) {
|
|
47
47
|
return null;
|