@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/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 supermemory - github.com/supermemoryai/supermemory)${C.reset}`);
214
- console.log(` ${C.dim}Runs on your machine, MIT licensed, free. Replaces the Finch-hosted memory proxy.${C.reset}\n`);
215
- const enableLocal = (0, clink_input_js_1.dedupClinkInput)((await ask(` Enable local memory? [y/N]: `)).trim()).toLowerCase();
216
- if (enableLocal === "y" || enableLocal === "yes") {
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}· Base mainnet · keys never leave your machine${C.reset}`
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
- console.log(` ${C.dim}Wallet${C.reset} ${C.cyan}${walletAddr.slice(0, 6)}...${walletAddr.slice(-4)}${C.reset} ${C.dim}· Base mainnet${C.reset}`);
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 supermemory) - optional, zero-cost alternative
1002
- // to the Convex-proxied cloud memory backend.
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
- console.log(` ${C.dim}Wallet ${C.reset}${C.cyan}${walletAddr.slice(0, 6)}...${walletAddr.slice(-4)}${C.reset} ${C.dim}· Base mainnet${C.reset}`);
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(), ".noelclaw");
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 NOELCLAW_* during rebrand. */
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 → NOELCLAW_X when legacy not passed
59
+ // Auto-map FINCH_X → FINCH_X when legacy not passed
60
60
  if (name.startsWith("FINCH_")) {
61
- const auto = "NOELCLAW_" + name.slice("FINCH_".length);
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 ~/.noelclaw/config.json
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", "NOELCLAW_SESSION_TOKEN") ?? readConfig().sessionToken;
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", "NOELCLAW_SESSION_TOKEN"],
118
- ["FINCH_API_KEY", "NOELCLAW_API_KEY"],
119
- ["FINCH_CONVEX_URL", "NOELCLAW_CONVEX_URL"],
120
- ["FINCH_TOOLS", "NOELCLAW_TOOLS"],
121
- ["FINCH_PROVIDER", "NOELCLAW_PROVIDER"],
122
- ["FINCH_MODEL", "NOELCLAW_MODEL"],
123
- ["FINCH_RPC_URL", "NOELCLAW_RPC_URL"],
124
- ["FINCH_BROADCAST_RPC", "NOELCLAW_BROADCAST_RPC"],
125
- ["FINCH_WALLET_PASSPHRASE", "NOELCLAW_WALLET_PASSPHRASE"],
126
- ["FINCH_PAYMENT_HEADER", "NOELCLAW_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
- // 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 ?? "";
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
+ }
@@ -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 memory
15
- // (memoryBackend: "local") and a key is present. Callers should still treat
16
- // a null return as "use Convex" - this never throws.
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 !== "local" || !cfg.supermemoryApiKey)
21
- return null;
22
- return { url: cfg.supermemoryUrl ?? DEFAULT_URL, apiKey: cfg.supermemoryApiKey };
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, so this
96
- // approximates via a capped wildcard search - accurate up to `SAMPLE_LIMIT`,
97
- // reported as a floor ("200+") beyond that rather than a false exact count.
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, "list_resources");
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, "read_resource");
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
  }
@@ -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. Without a key, callLLM falls
42
- // through to the finch-proxy path which may also be unconfigured for
43
- // this user - every agent_update would otherwise pay an 8s timeout for
44
- // nothing. Cheap, correct, and keeps update latency low.
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;