@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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +345 -0
  3. package/dist/_http-cache.js +96 -0
  4. package/dist/agent-loop.js +231 -0
  5. package/dist/annotations.js +113 -0
  6. package/dist/cli.js +1195 -0
  7. package/dist/clink-input.js +15 -0
  8. package/dist/config.js +132 -0
  9. package/dist/convex.js +151 -0
  10. package/dist/dex-pair.js +54 -0
  11. package/dist/enrichment-router.js +315 -0
  12. package/dist/index.js +256 -0
  13. package/dist/llm.js +323 -0
  14. package/dist/local-memory.js +102 -0
  15. package/dist/local-vault.js +454 -0
  16. package/dist/output-schemas.js +551 -0
  17. package/dist/prompts.js +111 -0
  18. package/dist/public-url.js +107 -0
  19. package/dist/resources.js +116 -0
  20. package/dist/server.js +300 -0
  21. package/dist/signal-gate.js +57 -0
  22. package/dist/token-decimals.js +26 -0
  23. package/dist/token-gate.js +88 -0
  24. package/dist/tool-filter.js +44 -0
  25. package/dist/tools/_solidity-scan.js +313 -0
  26. package/dist/tools/agents.js +729 -0
  27. package/dist/tools/automation.js +314 -0
  28. package/dist/tools/base-mcp.js +478 -0
  29. package/dist/tools/base.js +269 -0
  30. package/dist/tools/chronicle.js +268 -0
  31. package/dist/tools/coder.js +94 -0
  32. package/dist/tools/deep-research.js +1416 -0
  33. package/dist/tools/defi.js +291 -0
  34. package/dist/tools/equity.js +364 -0
  35. package/dist/tools/events.js +182 -0
  36. package/dist/tools/framework.js +150 -0
  37. package/dist/tools/github.js +514 -0
  38. package/dist/tools/insider.js +264 -0
  39. package/dist/tools/insight.js +634 -0
  40. package/dist/tools/market.js +555 -0
  41. package/dist/tools/memory.js +1046 -0
  42. package/dist/tools/miroshark.js +343 -0
  43. package/dist/tools/monitor.js +319 -0
  44. package/dist/tools/os.js +226 -0
  45. package/dist/tools/packets.js +296 -0
  46. package/dist/tools/research-chain.js +226 -0
  47. package/dist/tools/research-compare.js +280 -0
  48. package/dist/tools/research.js +188 -0
  49. package/dist/tools/rh-bridge.js +148 -0
  50. package/dist/tools/rh-mcp.js +1411 -0
  51. package/dist/tools/rh-orders.js +471 -0
  52. package/dist/tools/scanner.js +534 -0
  53. package/dist/tools/vault.js +764 -0
  54. package/dist/tools/wallet.js +200 -0
  55. package/dist/types.js +2 -0
  56. package/dist/wallet.js +184 -0
  57. package/package.json +87 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1195 @@
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 readline = __importStar(require("readline"));
38
+ const fs = __importStar(require("fs"));
39
+ const os = __importStar(require("os"));
40
+ const path = __importStar(require("path"));
41
+ const agent_loop_js_1 = require("./agent-loop.js");
42
+ const server_js_1 = require("./server.js");
43
+ const tool_filter_js_1 = require("./tool-filter.js");
44
+ const config_js_1 = require("./config.js");
45
+ const local_memory_js_1 = require("./local-memory.js");
46
+ const clink_input_js_1 = require("./clink-input.js");
47
+ const child_process = __importStar(require("child_process"));
48
+ (0, config_js_1.hydrateEnvFromConfig)();
49
+ // Derived tool counts - single source of truth, kept in sync with the
50
+ // actual registered tools. Updates here propagate to banner, login,
51
+ // and doctor without manual edits.
52
+ const TOTAL_TOOL_COUNT = server_js_1.ALL_TOOLS.length;
53
+ const CORE_TOOL_COUNT = (() => {
54
+ const prev = process.env.FINCH_TOOLS;
55
+ try {
56
+ process.env.FINCH_TOOLS = "core";
57
+ return (0, tool_filter_js_1.filterTools)(server_js_1.ALL_TOOLS).length;
58
+ }
59
+ finally {
60
+ if (prev === undefined)
61
+ delete process.env.FINCH_TOOLS;
62
+ else
63
+ process.env.FINCH_TOOLS = prev;
64
+ }
65
+ })();
66
+ const CONVEX_SITE = process.env.FINCH_CONVEX_URL ?? "https://befitting-porcupine-276.convex.site";
67
+ const PKG_VERSION = (() => {
68
+ try {
69
+ const raw = fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8");
70
+ return JSON.parse(raw).version ?? "unknown";
71
+ }
72
+ catch {
73
+ return "unknown";
74
+ }
75
+ })();
76
+ // Read wallet address from the encrypted JSON without decrypting -
77
+ // ethers stores the address in plaintext inside the keystore file.
78
+ function getLocalWalletAddress() {
79
+ try {
80
+ const walletPath = path.join(os.homedir(), ".finch", "wallet.json");
81
+ if (!fs.existsSync(walletPath))
82
+ return null;
83
+ const data = JSON.parse(fs.readFileSync(walletPath, "utf8"));
84
+ const addr = data.address;
85
+ if (!addr)
86
+ return null;
87
+ return addr.startsWith("0x") ? addr : `0x${addr}`;
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
93
+ async function loginWithApiKey(rl) {
94
+ const ask = (q) => new Promise(resolve => rl.question(q, resolve));
95
+ console.log(` ${C.dim}Generate an API key at app.finchagentic.com → Settings → API Keys${C.reset}`);
96
+ console.log(` ${C.dim}Or set env var: set FINCH_API_KEY=finch_sk_... (or noel_sk_...)${C.reset}`);
97
+ let apiKey = (await ask(` API key (finch_sk_... / noel_sk_...): `)).trim();
98
+ if (!apiKey)
99
+ return;
100
+ // Deduplicates doubled input from a known Clink v1.7.6 terminal bug (e.g.
101
+ // "finch_sk_xxfinch_sk_xx" → "finch_sk_xx"); also strips non-ASCII Clink can inject.
102
+ apiKey = (0, clink_input_js_1.dedupClinkInput)(apiKey).replace(/[^\x20-\x7E]/g, "").trim();
103
+ if (!(0, config_js_1.isApiKey)(apiKey)) {
104
+ console.log(`\n ${C.red}✗${C.reset} API key must start with "finch_sk_" or "noel_sk_". Got: "${apiKey.slice(0, 20)}..."\n`);
105
+ return;
106
+ }
107
+ process.stdout.write(` Authenticating...`);
108
+ const res = await fetch(`${CONVEX_SITE}/auth/apikey/login`, {
109
+ method: "POST",
110
+ headers: { "Content-Type": "application/json" },
111
+ body: JSON.stringify({ apiKey }),
112
+ });
113
+ const data = await res.json();
114
+ if (!res.ok || !data.token) {
115
+ console.log(`\n ${C.red}✗${C.reset} ${data.error ?? "Invalid API key"}\n`);
116
+ return;
117
+ }
118
+ const email = data.email ?? "api-key-user";
119
+ const name = data.displayName ?? undefined;
120
+ (0, config_js_1.writeConfig)({ sessionToken: data.token, email, name });
121
+ printLoginSuccess({ email, name });
122
+ }
123
+ async function loginFlow(loginRl) {
124
+ const rl = loginRl ?? readline.createInterface({ input: process.stdin, output: process.stdout });
125
+ // Check env var first — skip prompt entirely
126
+ const envKey = process.env.FINCH_API_KEY;
127
+ if (envKey && (0, config_js_1.isApiKey)(envKey)) {
128
+ console.log(`\n ${C.dim}Found FINCH_API_KEY in environment — authenticating...${C.reset}`);
129
+ process.stdout.write(` Authenticating...`);
130
+ const res = await fetch(`${CONVEX_SITE}/auth/apikey/login`, {
131
+ method: "POST",
132
+ headers: { "Content-Type": "application/json" },
133
+ body: JSON.stringify({ apiKey: envKey }),
134
+ });
135
+ const data = await res.json();
136
+ if (res.ok && data.token) {
137
+ const email = data.email ?? "api-key-user";
138
+ const name = data.displayName ?? undefined;
139
+ (0, config_js_1.writeConfig)({ sessionToken: data.token, email, name });
140
+ printLoginSuccess({ email, name });
141
+ rl.close();
142
+ return;
143
+ }
144
+ console.log(`\n ${C.red}✗${C.reset} Env var FINCH_API_KEY is invalid. Falling back to manual login.\n`);
145
+ }
146
+ console.log(`\n ${C.cyan}${C.bold}Sign in to Finch${C.reset}\n`);
147
+ await loginWithApiKey(rl);
148
+ rl.close();
149
+ }
150
+ // ── Setup wizard: BYOK LLM provider + local memory ────────────────────────────
151
+ const PROVIDER_CHOICES = {
152
+ "1": { configKey: "bankrApiKey", envVar: "BANKR_API_KEY", label: "Bankr" },
153
+ "2": { configKey: "anthropicApiKey", envVar: "ANTHROPIC_API_KEY", label: "Anthropic" },
154
+ "3": { configKey: "openaiApiKey", envVar: "OPENAI_API_KEY", label: "OpenAI" },
155
+ };
156
+ const LOCAL_MEMORY_INSTALL_CMD = ["npx", ["-y", "supermemory", "local"]];
157
+ const LOCAL_MEMORY_POLL_MS = 2000;
158
+ const LOCAL_MEMORY_POLL_MAX_MS = 45000;
159
+ const LOCAL_MEMORY_KEY_RE = /\b(sm_[A-Za-z0-9_-]{8,})\b/;
160
+ async function setupFlow() {
161
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
162
+ const ask = (q) => new Promise(resolve => rl.question(q, resolve));
163
+ console.log(`\n ${C.cyan}${C.bold}finch setup${C.reset}`);
164
+ console.log(` ${C.dim}Bring your own LLM key and/or run memory fully local - zero cost, zero lock-in.${C.reset}\n`);
165
+ // ── Step 1: LLM provider ──────────────────────────────────────────────────
166
+ // Say plainly that skipping is fine. Tools return data and structure for the
167
+ // calling model; only `finch run` and scheduled agents need a key of
168
+ // their own, because nothing else is there to do the thinking.
169
+ console.log(` ${C.dim}Optional. Every MCP tool works without a key - your client's model does the reasoning.${C.reset}`);
170
+ console.log(` ${C.dim}A key is what lets finch think on its own: \`finch run\` and scheduled agents.${C.reset}\n`);
171
+ console.log(` ${C.dim}[1] Bankr [2] Anthropic [3] OpenAI [4] Custom endpoint (self-hosted/VPS) [5] Skip${C.reset}\n`);
172
+ const providerChoice = (0, clink_input_js_1.dedupClinkInput)((await ask(` Choose [1-5]: `)).trim());
173
+ const provider = PROVIDER_CHOICES[providerChoice];
174
+ // Tracks whatever key got configured in this step, regardless of which
175
+ // branch set it, so step 2 can forward it to the local-memory subprocess
176
+ // env uniformly instead of only handling the named-provider case.
177
+ let chosenProviderKey;
178
+ let chosenProviderEnvVar;
179
+ if (provider) {
180
+ const key = (0, clink_input_js_1.dedupClinkInput)((await ask(` ${provider.label} API key: `)).trim()).replace(/[^\x20-\x7E]/g, "").trim();
181
+ if (key) {
182
+ (0, config_js_1.writeConfig)({ [provider.configKey]: key });
183
+ process.env[provider.envVar] = key;
184
+ chosenProviderKey = key;
185
+ chosenProviderEnvVar = provider.envVar;
186
+ console.log(`\n ${C.green}✓${C.reset} Saved. ${provider.label} will be used for LLM calls in the interactive shell and MCP server.`);
187
+ console.log(` ${C.dim}(Stored in ~/.finch/config.json, mode 0600 - same protection as your session token.)${C.reset}\n`);
188
+ }
189
+ else {
190
+ console.log(`\n ${C.dim}No key entered - skipped.${C.reset}\n`);
191
+ }
192
+ }
193
+ else if (providerChoice === "4") {
194
+ console.log(` ${C.dim}Any OpenAI Chat Completions-compatible endpoint works (LiteLLM, vLLM, Ollama, OpenRouter, your own VPS gateway).${C.reset}`);
195
+ const baseUrl = (0, clink_input_js_1.dedupClinkInput)((await ask(` Base URL (e.g. https://your-vps:8000/v1): `)).trim()).replace(/[^\x20-\x7E]/g, "").trim().replace(/\/+$/, "");
196
+ if (baseUrl) {
197
+ const key = (0, clink_input_js_1.dedupClinkInput)((await ask(` API key (leave blank if your endpoint doesn't need one): `)).trim()).replace(/[^\x20-\x7E]/g, "").trim();
198
+ (0, config_js_1.writeConfig)({ openaiBaseUrl: baseUrl, openaiApiKey: key || "local" });
199
+ process.env.OPENAI_BASE_URL = baseUrl;
200
+ process.env.OPENAI_API_KEY = key || "local";
201
+ chosenProviderKey = key || "local";
202
+ chosenProviderEnvVar = "OPENAI_API_KEY";
203
+ console.log(`\n ${C.green}✓${C.reset} Saved. Requests route to ${baseUrl} using the OpenAI protocol.\n`);
204
+ }
205
+ else {
206
+ console.log(`\n ${C.dim}No URL entered - skipped.${C.reset}\n`);
207
+ }
208
+ }
209
+ else {
210
+ console.log(`\n ${C.dim}Skipped - using Finch proxy.${C.reset}\n`);
211
+ }
212
+ // ── 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") {
217
+ const url = "http://localhost:6767";
218
+ console.log(`\n ${C.dim}Checking ${url}...${C.reset}`);
219
+ let reachable = await isLocalMemoryReachableRaw(url);
220
+ let installerError;
221
+ if (!reachable) {
222
+ console.log(` ${C.dim}Not running - launching \`npx -y supermemory local\` in the background...${C.reset}`);
223
+ const providerEnv = chosenProviderEnvVar && chosenProviderKey ? { [chosenProviderEnvVar]: chosenProviderKey } : {};
224
+ const result = await launchLocalMemoryServer(url, providerEnv);
225
+ reachable = result.reachable;
226
+ installerError = result.installerError;
227
+ }
228
+ if (reachable) {
229
+ const key = await readLocalMemoryKey();
230
+ if (key) {
231
+ (0, config_js_1.writeConfig)({ memoryBackend: "local", supermemoryUrl: url, supermemoryApiKey: key });
232
+ console.log(`\n ${C.green}✓${C.reset} Local memory enabled at ${url}. Memory tools now run fully local - zero cost, private to this machine.\n`);
233
+ }
234
+ else {
235
+ console.log(`\n ${C.yellow}⚠${C.reset} Server is running but its API key couldn't be auto-detected.`);
236
+ const manualKey = (0, clink_input_js_1.dedupClinkInput)((await ask(` Paste the key printed by the server (starts with sm_), or press Enter to skip: `)).trim());
237
+ if (manualKey) {
238
+ (0, config_js_1.writeConfig)({ memoryBackend: "local", supermemoryUrl: url, supermemoryApiKey: manualKey });
239
+ console.log(`\n ${C.green}✓${C.reset} Local memory enabled at ${url}.\n`);
240
+ }
241
+ else {
242
+ console.log(`\n ${C.dim}Skipped - memory tools will keep using the Finch proxy. Run \`finch setup\` again once you have the key.${C.reset}\n`);
243
+ }
244
+ }
245
+ }
246
+ else if (installerError) {
247
+ console.log(`\n ${C.yellow}⚠${C.reset} Local install failed: ${installerError}`);
248
+ console.log(` ${C.dim}Memory tools keep using the Finch proxy until this is resolved. Run \`finch setup\` again after fixing it.${C.reset}\n`);
249
+ }
250
+ else {
251
+ console.log(`\n ${C.yellow}⚠${C.reset} Couldn't reach a local supermemory server after ${LOCAL_MEMORY_POLL_MAX_MS / 1000}s.`);
252
+ console.log(` ${C.dim}Install it yourself in another terminal: ${C.cyan}npx -y supermemory local${C.reset}`);
253
+ console.log(` ${C.dim}Then run \`finch setup\` again - memory tools keep using the Finch proxy until then.${C.reset}\n`);
254
+ }
255
+ }
256
+ else {
257
+ console.log(`\n ${C.dim}Skipped - memory tools use the Finch-hosted proxy.${C.reset}\n`);
258
+ }
259
+ // ── Step 3: local vault ───────────────────────────────────────────────────
260
+ // Unlike local memory, this needs no server - it's a plain on-disk store, so
261
+ // enabling it is a config flip. Data lives under ~/.finch/vault/.
262
+ console.log(` ${C.cyan}${C.bold}Local vault${C.reset} ${C.dim}(versioned artifacts on your own disk - ~/.finch/vault/)${C.reset}`);
263
+ console.log(` ${C.dim}No account, no Convex, no cost. vault_save/read/search/history/diff run fully local.${C.reset}\n`);
264
+ const enableVault = (0, clink_input_js_1.dedupClinkInput)((await ask(` Enable local vault? [y/N]: `)).trim()).toLowerCase();
265
+ if (enableVault === "y" || enableVault === "yes") {
266
+ (0, config_js_1.writeConfig)({ vaultBackend: "local" });
267
+ console.log(`\n ${C.green}✓${C.reset} Local vault enabled. All vault tools now store on this machine - you own the data, no account needed.\n`);
268
+ }
269
+ else {
270
+ console.log(`\n ${C.dim}Skipped - vault tools use the Finch-hosted store (needs \`finch login\`).${C.reset}\n`);
271
+ }
272
+ rl.close();
273
+ }
274
+ async function isLocalMemoryReachableRaw(url) {
275
+ try {
276
+ const res = await fetch(url, { signal: AbortSignal.timeout(1500) });
277
+ return res.status < 500;
278
+ }
279
+ catch {
280
+ return false;
281
+ }
282
+ }
283
+ // Spawns the local supermemory server detached so it survives after this CLI
284
+ // process exits (same lifecycle model as a local daemon, e.g. Ollama).
285
+ // Captures stdout briefly to look for a printed sm_... key (the binary's
286
+ // first-boot wizard prints one), then stops watching it - the process itself
287
+ // keeps running in the background regardless of whether we caught the key.
288
+ let capturedLocalMemoryKey;
289
+ // Known installer failure signatures worth surfacing verbatim instead of a
290
+ // generic "couldn't reach" message. Confirmed on Windows: the installer
291
+ // shells out via WSL, and fails immediately (before ever listening on the
292
+ // port) if WSL isn't installed/registered - waiting out the full poll
293
+ // window for that case just wastes the user's time.
294
+ const KNOWN_INSTALLER_FAILURES = [
295
+ { re: /unsupported OS/i, hint: "The installer doesn't support this shell/OS directly (seen from Git Bash/MINGW on Windows)." },
296
+ { re: /REGDB_E_CLASSNOTREG|Class not registered/i, hint: "The installer needs WSL (Windows Subsystem for Linux) to run its install script, and WSL isn't set up on this machine. Install it with `wsl --install` (needs a restart), then try again." },
297
+ ];
298
+ async function launchLocalMemoryServer(url, providerEnv) {
299
+ const [cmd, args] = LOCAL_MEMORY_INSTALL_CMD;
300
+ let installerError;
301
+ // Authoritative "did the process die" signal - independent of whether the
302
+ // regex hints below happen to match. A spawn failure (e.g. npx not found)
303
+ // emits an async 'error' event rather than throwing synchronously, so it
304
+ // is NOT caught by the try/catch around spawn() itself; without this
305
+ // listener it becomes an uncaught exception that crashes the whole CLI.
306
+ let exited = false;
307
+ // Accumulated across all chunks (not tested per-chunk) so a known error
308
+ // string split across two writes still matches.
309
+ let outputBuffer = "";
310
+ try {
311
+ const child = child_process.spawn(cmd, [...args], {
312
+ detached: true,
313
+ stdio: ["ignore", "pipe", "pipe"],
314
+ env: { ...process.env, ...providerEnv },
315
+ });
316
+ child.on("error", (err) => {
317
+ exited = true;
318
+ if (!installerError)
319
+ installerError = `Couldn't launch \`${cmd}\`: ${err.message}`;
320
+ });
321
+ child.on("exit", () => { exited = true; });
322
+ const onData = (chunk) => {
323
+ outputBuffer += chunk.toString("utf8");
324
+ const keyMatch = outputBuffer.match(LOCAL_MEMORY_KEY_RE);
325
+ if (keyMatch)
326
+ capturedLocalMemoryKey = keyMatch[1];
327
+ if (!installerError) {
328
+ const known = KNOWN_INSTALLER_FAILURES.find((k) => k.re.test(outputBuffer));
329
+ if (known)
330
+ installerError = known.hint;
331
+ }
332
+ };
333
+ child.stdout?.on("data", onData);
334
+ child.stderr?.on("data", onData);
335
+ child.unref();
336
+ }
337
+ catch (err) {
338
+ return { reachable: false, installerError: `Couldn't launch \`${cmd}\`: ${err.message}` };
339
+ }
340
+ const deadline = Date.now() + LOCAL_MEMORY_POLL_MAX_MS;
341
+ while (Date.now() < deadline) {
342
+ if (await isLocalMemoryReachableRaw(url))
343
+ return { reachable: true };
344
+ // Fail fast once the process has died (confirmed via 'error'/'exit') or
345
+ // a known error signature was recognized - polling further won't help.
346
+ if (installerError || exited)
347
+ return { reachable: false, installerError };
348
+ await new Promise((r) => setTimeout(r, LOCAL_MEMORY_POLL_MS));
349
+ }
350
+ return { reachable: false, installerError };
351
+ }
352
+ async function readLocalMemoryKey() {
353
+ if (capturedLocalMemoryKey)
354
+ return capturedLocalMemoryKey;
355
+ // Fall back to the documented on-disk location ("everything lives in
356
+ // ./.supermemory") - exact path may need adjusting once verified against
357
+ // the real binary; this covers the most likely spot.
358
+ try {
359
+ const candidate = path.join(os.homedir(), ".supermemory", "config.json");
360
+ if (fs.existsSync(candidate)) {
361
+ const data = JSON.parse(fs.readFileSync(candidate, "utf8"));
362
+ const key = data?.apiKey ?? data?.api_key ?? data?.key;
363
+ if (typeof key === "string" && LOCAL_MEMORY_KEY_RE.test(key))
364
+ return key;
365
+ }
366
+ }
367
+ catch { /* best-effort only */ }
368
+ return undefined;
369
+ }
370
+ // ── ANSI ─────────────────────────────────────────────────────────────────────
371
+ const C = {
372
+ reset: "\x1b[0m",
373
+ bold: "\x1b[1m",
374
+ dim: "\x1b[2m",
375
+ green: "\x1b[32m",
376
+ cyan: "\x1b[36m",
377
+ violet: "\x1b[35m",
378
+ red: "\x1b[31m",
379
+ yellow: "\x1b[33m",
380
+ white: "\x1b[97m",
381
+ bg: "\x1b[48;5;17m",
382
+ };
383
+ // ── Banner ────────────────────────────────────────────────────────────────────
384
+ //
385
+ // ███╗ ██╗ ██████╗ ███████╗██╗ ██████╗██╗ █████╗ ██╗ ██╗
386
+ // ████╗ ██║██╔═══██╗██╔════╝██║ ██╔════╝██║ ██╔══██╗██║ ██║
387
+ // ██╔██╗ ██║██║ ██║█████╗ ██║ ██║ ██║ ███████║██║ █╗ ██║
388
+ // ██║╚██╗██║██║ ██║██╔══╝ ██║ ██║ ██║ ██╔══██║██║███╗██║
389
+ // ██║ ╚████║╚██████╔╝███████╗███████╗╚██████╗███████╗██║ ██║╚███╔███╔╝
390
+ // ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝
391
+ const LOGO_LINES = [
392
+ ` ███╗ ██╗ ██████╗ ███████╗██╗ ██████╗██╗ █████╗ ██╗ ██╗`,
393
+ ` ████╗ ██║██╔═══██╗██╔════╝██║ ██╔════╝██║ ██╔══██╗██║ ██║`,
394
+ ` ██╔██╗ ██║██║ ██║█████╗ ██║ ██║ ██║ ███████║██║ █╗ ██║`,
395
+ ` ██║╚██╗██║██║ ██║██╔══╝ ██║ ██║ ██║ ██╔══██║██║███╗██║`,
396
+ ` ██║ ╚████║╚██████╔╝███████╗███████╗╚██████╗███████╗██║ ██║╚███╔███╔╝`,
397
+ ` ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝`,
398
+ ];
399
+ const SEP = ` ${"─".repeat(70)}`;
400
+ function buildBanner() {
401
+ const cfg = (0, config_js_1.readConfig)();
402
+ const walletAddr = getLocalWalletAddress();
403
+ const provider = process.env.BANKR_API_KEY ? "Bankr"
404
+ : process.env.ANTHROPIC_API_KEY ? "Anthropic"
405
+ : process.env.OPENAI_API_KEY ? "OpenAI"
406
+ : "Finch proxy";
407
+ const logo = LOGO_LINES.map(l => `${C.cyan}${C.bold}${l}${C.reset}`).join("\n");
408
+ // ── meta row ──
409
+ const meta = `\n${SEP}\n ${C.dim}v${PKG_VERSION} · ${TOTAL_TOOL_COUNT} tools · finchagentic.com${C.reset}\n${SEP}`;
410
+ // ── auth block ──
411
+ let authBlock;
412
+ if (cfg.sessionToken) {
413
+ const displayName = cfg.name ? `${C.white}${C.bold}${cfg.name}${C.reset}` : "";
414
+ const displayEmail = cfg.email ? `${C.dim}${cfg.email}${C.reset}` : "";
415
+ const nameLine = displayName
416
+ ? ` ${C.green}●${C.reset} ${displayName} ${displayEmail}`
417
+ : ` ${C.green}●${C.reset} ${displayEmail || `${C.green}Signed in${C.reset}`}`;
418
+ 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}`
420
+ : ` ${C.dim}Wallet not created yet · auto-creates on first DeFi call${C.reset}`;
421
+ const llmLine = ` ${C.dim}LLM ${C.reset}${C.green}${provider}${C.reset} ${C.dim}· ${TOTAL_TOOL_COUNT} tools active${C.reset}`;
422
+ authBlock = `\n${nameLine}\n${walletLine}\n${llmLine}`;
423
+ }
424
+ else {
425
+ authBlock = [
426
+ ``,
427
+ ` ${C.yellow}○${C.reset} ${C.yellow}Not signed in${C.reset} ${C.dim}- tools that need your account will fail${C.reset}`,
428
+ ` ${C.dim}Run ${C.reset}${C.cyan}/login${C.reset}${C.dim} to unlock all ${TOTAL_TOOL_COUNT} tools${C.reset}`,
429
+ ` ${C.dim}LLM ${provider} · basic tools still work${C.reset}`,
430
+ ].join("\n");
431
+ }
432
+ const hint = `\n${SEP}\n ${C.dim}Type anything to chat · /help · Ctrl+C to exit${C.reset}\n`;
433
+ return `\n${logo}\n${meta}\n${authBlock}\n${hint}`;
434
+ }
435
+ // ── Post-login success block ──────────────────────────────────────────────────
436
+ function printLoginSuccess({ email, name }) {
437
+ const walletAddr = getLocalWalletAddress();
438
+ const displayName = name ?? "";
439
+ console.log(`\n${SEP}`);
440
+ if (displayName) {
441
+ console.log(` ${C.green}✓${C.reset} ${C.white}${C.bold}${displayName}${C.reset} ${C.dim}${email}${C.reset}`);
442
+ }
443
+ else {
444
+ console.log(` ${C.green}✓${C.reset} ${C.green}${C.bold}Signed in${C.reset} ${C.dim}as ${email}${C.reset}`);
445
+ }
446
+ 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}`);
448
+ }
449
+ else {
450
+ console.log(` ${C.dim}Wallet auto-creates on first DeFi tool call${C.reset}`);
451
+ }
452
+ console.log(` ${C.dim}Token saved to ~/.finch/config.json${C.reset}`);
453
+ console.log(` ${C.dim}All ${TOTAL_TOOL_COUNT} tools unlocked${C.reset}`);
454
+ console.log(`${SEP}\n`);
455
+ }
456
+ // ── Spinner ───────────────────────────────────────────────────────────────────
457
+ function spinner(label) {
458
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
459
+ let i = 0;
460
+ const iv = setInterval(() => {
461
+ process.stdout.write(`\r ${C.dim}${frames[i % frames.length]} ${label}${C.reset} `);
462
+ i++;
463
+ }, 80);
464
+ return () => {
465
+ clearInterval(iv);
466
+ process.stdout.write("\r" + " ".repeat(label.length + 12) + "\r");
467
+ };
468
+ }
469
+ // ── Help ─────────────────────────────────────────────────────────────────────
470
+ function printHelp() {
471
+ const cfg = (0, config_js_1.readConfig)();
472
+ const authLine = cfg.email
473
+ ? ` ${C.dim}Signed in as ${C.reset}${C.green}${cfg.email}${C.reset}`
474
+ : ` ${C.yellow}⚠${C.reset} ${C.dim}Not signed in - run /login to unlock all tools${C.reset}`;
475
+ console.log(`
476
+ ${C.cyan}Commands:${C.reset}
477
+ /login Sign in to unlock all ${TOTAL_TOOL_COUNT} tools
478
+ /logout Sign out and clear saved token
479
+ /clear Clear conversation history
480
+ /tools List all available tools
481
+ /quit Exit
482
+
483
+ ${authLine}
484
+
485
+ ${C.dim}Examples:
486
+ remember that I prefer concise answers
487
+ search the web for recent AI news
488
+ save a note to my vault
489
+ research "top AI agent frameworks in 2025"
490
+ spawn an agent to monitor competitor releases weekly${C.reset}
491
+ `);
492
+ }
493
+ // ── Version check ─────────────────────────────────────────────────────────────
494
+ async function checkForUpdate() {
495
+ try {
496
+ const res = await fetch("https://registry.npmjs.org/@finchagentic/mcp/latest", {
497
+ signal: AbortSignal.timeout(5000),
498
+ });
499
+ if (!res.ok)
500
+ return;
501
+ const data = await res.json();
502
+ const latest = data.version;
503
+ if (!latest || latest === PKG_VERSION)
504
+ return;
505
+ const sep = ` ${"─".repeat(58)}`;
506
+ console.log(`\n${sep}`);
507
+ console.log(` ${C.yellow}⚠${C.reset} Update available: ${C.yellow}v${PKG_VERSION}${C.reset} → ${C.cyan}v${latest}${C.reset}`);
508
+ console.log(` ${C.dim}npm install -g @finchagentic/mcp@${latest}${C.reset} ${C.dim}or restart your MCP client${C.reset}`);
509
+ console.log(`${sep}\n`);
510
+ }
511
+ catch {
512
+ // silently ignore
513
+ }
514
+ }
515
+ // ── Main ─────────────────────────────────────────────────────────────────────
516
+ async function main() {
517
+ // Auto-login from env var if set and not already logged in
518
+ const cfg = (0, config_js_1.readConfig)();
519
+ const envKey = process.env.FINCH_API_KEY;
520
+ if (!cfg.sessionToken && envKey && (0, config_js_1.isApiKey)(envKey)) {
521
+ process.stdout.write(` ${C.dim}Auto-login from FINCH_API_KEY...${C.reset} `);
522
+ try {
523
+ const res = await fetch(`${CONVEX_SITE}/auth/apikey/login`, {
524
+ method: "POST",
525
+ headers: { "Content-Type": "application/json" },
526
+ body: JSON.stringify({ apiKey: envKey }),
527
+ });
528
+ const data = await res.json();
529
+ if (res.ok && data.token) {
530
+ (0, config_js_1.writeConfig)({ sessionToken: data.token, email: data.email ?? "api-key-user", name: data.displayName ?? undefined });
531
+ console.log(`${C.green}✓${C.reset} ${C.dim}Signed in as ${data.email}${C.reset}`);
532
+ }
533
+ else {
534
+ console.log(`${C.red}✗${C.reset} ${C.dim}Invalid API key in env var${C.reset}`);
535
+ }
536
+ }
537
+ catch (e) {
538
+ console.log(`${C.red}✗${C.reset} ${C.dim}Login failed: ${e.message}${C.reset}`);
539
+ }
540
+ }
541
+ process.stdout.write(buildBanner());
542
+ // Check for updates in background - shows after banner, doesn't block prompt
543
+ checkForUpdate().catch(() => { });
544
+ const history = [];
545
+ const rl = readline.createInterface({
546
+ input: process.stdin,
547
+ output: process.stdout,
548
+ prompt: `${C.green}>${C.reset} `,
549
+ });
550
+ rl.prompt();
551
+ rl.on("line", async (raw) => {
552
+ const line = raw.trim();
553
+ if (!line) {
554
+ rl.prompt();
555
+ return;
556
+ }
557
+ // Built-in commands
558
+ if (line === "/quit" || line === "/exit") {
559
+ console.log(`\n ${C.dim}Goodbye.${C.reset}\n`);
560
+ process.exit(0);
561
+ }
562
+ if (line === "/clear") {
563
+ history.length = 0;
564
+ console.log(` ${C.dim}History cleared.${C.reset}\n`);
565
+ rl.prompt();
566
+ return;
567
+ }
568
+ if (line === "/help") {
569
+ printHelp();
570
+ rl.prompt();
571
+ return;
572
+ }
573
+ if (line === "/login") {
574
+ rl.pause();
575
+ // Create fresh readline for login — reusing rl causes input conflicts
576
+ const loginRl = readline.createInterface({ input: process.stdin, output: process.stdout });
577
+ await loginFlow(loginRl);
578
+ loginRl.close();
579
+ rl.resume();
580
+ rl.prompt();
581
+ return;
582
+ }
583
+ if (line === "/logout") {
584
+ (0, config_js_1.writeConfig)({ sessionToken: undefined, email: undefined });
585
+ console.log(` ${C.dim}Logged out. Token cleared from ~/.finch/config.json${C.reset}\n`);
586
+ rl.prompt();
587
+ return;
588
+ }
589
+ if (line === "/tools") {
590
+ console.log(`\n ${C.cyan}${server_js_1.ALL_TOOLS.length} tools:${C.reset}`);
591
+ for (const t of server_js_1.ALL_TOOLS) {
592
+ console.log(` ${C.dim}·${C.reset} ${t.name} ${C.dim}${(t.description ?? "").slice(0, 60)}${C.reset}`);
593
+ }
594
+ console.log();
595
+ rl.prompt();
596
+ return;
597
+ }
598
+ // Agent call
599
+ const stop = spinner("thinking");
600
+ try {
601
+ const result = await (0, agent_loop_js_1.runAgent)(line, history, (toolName) => {
602
+ stop();
603
+ process.stdout.write(` ${C.dim}✦ ${toolName}${C.reset}\n`);
604
+ });
605
+ stop();
606
+ // Update conversation history (keep last 20 turns)
607
+ history.push({ role: "user", content: line });
608
+ history.push({ role: "assistant", content: result.text });
609
+ while (history.length > 20)
610
+ history.splice(0, 2);
611
+ // Output
612
+ console.log();
613
+ const lines = result.text.split("\n");
614
+ for (const l of lines) {
615
+ console.log(` ${l}`);
616
+ }
617
+ console.log();
618
+ }
619
+ catch (err) {
620
+ stop();
621
+ console.log(`\n ${C.red}✗${C.reset} ${err.message}\n`);
622
+ }
623
+ rl.prompt();
624
+ });
625
+ rl.on("close", () => {
626
+ console.log(`\n ${C.dim}Goodbye.${C.reset}\n`);
627
+ process.exit(0);
628
+ });
629
+ }
630
+ function resolveClients() {
631
+ const home = os.homedir();
632
+ const plat = os.platform();
633
+ const appdata = process.env.APPDATA ?? path.join(home, "AppData", "Roaming");
634
+ const defs = [
635
+ {
636
+ name: "Claude Desktop", serversKey: "mcpServers", entryStyle: "standard",
637
+ paths: {
638
+ darwin: path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
639
+ win32: path.join(appdata, "Claude", "claude_desktop_config.json"),
640
+ linux: path.join(home, ".config", "Claude", "claude_desktop_config.json"),
641
+ },
642
+ },
643
+ {
644
+ name: "Cursor", serversKey: "mcpServers", entryStyle: "standard",
645
+ paths: {
646
+ darwin: path.join(home, ".cursor", "mcp.json"),
647
+ win32: path.join(home, ".cursor", "mcp.json"),
648
+ linux: path.join(home, ".cursor", "mcp.json"),
649
+ },
650
+ },
651
+ {
652
+ name: "Windsurf", serversKey: "mcpServers", entryStyle: "standard",
653
+ paths: {
654
+ darwin: path.join(home, ".codeium", "windsurf", "mcp_config.json"),
655
+ win32: path.join(home, ".codeium", "windsurf", "mcp_config.json"),
656
+ linux: path.join(home, ".codeium", "windsurf", "mcp_config.json"),
657
+ },
658
+ },
659
+ // VS Code: user-level mcp.json uses the top-level `servers` key (NOT
660
+ // mcpServers) and each entry needs `"type": "stdio"`.
661
+ {
662
+ name: "VS Code", serversKey: "servers", entryStyle: "vscode",
663
+ paths: {
664
+ darwin: path.join(home, "Library", "Application Support", "Code", "User", "mcp.json"),
665
+ win32: path.join(appdata, "Code", "User", "mcp.json"),
666
+ linux: path.join(home, ".config", "Code", "User", "mcp.json"),
667
+ },
668
+ },
669
+ {
670
+ name: "VS Code Insiders", serversKey: "servers", entryStyle: "vscode",
671
+ paths: {
672
+ darwin: path.join(home, "Library", "Application Support", "Code - Insiders", "User", "mcp.json"),
673
+ win32: path.join(appdata, "Code - Insiders", "User", "mcp.json"),
674
+ linux: path.join(home, ".config", "Code - Insiders", "User", "mcp.json"),
675
+ },
676
+ },
677
+ // Zed: MCP servers live in settings.json under `context_servers`, and each
678
+ // entry needs `"source": "custom"`. (Not a separate mcp.json file.)
679
+ {
680
+ name: "Zed", serversKey: "context_servers", entryStyle: "zed",
681
+ paths: {
682
+ darwin: path.join(home, ".config", "zed", "settings.json"),
683
+ win32: path.join(appdata, "Zed", "settings.json"),
684
+ linux: path.join(home, ".config", "zed", "settings.json"),
685
+ },
686
+ },
687
+ ];
688
+ return defs
689
+ .map((d) => {
690
+ const configPath = d.paths[plat] ?? d.paths.linux;
691
+ return { name: d.name, configPath, serversKey: d.serversKey, entryStyle: d.entryStyle };
692
+ })
693
+ .filter((c) => {
694
+ // Include if the config file exists OR the parent directory exists (app installed but not yet configured)
695
+ return fs.existsSync(c.configPath) || fs.existsSync(path.dirname(c.configPath));
696
+ });
697
+ }
698
+ // `npx -y @finchagentic/mcp@version` alone fails with "could not determine
699
+ // executable to run" - the package ships two bins (finch, finch-mcp)
700
+ // and neither matches the unscoped package name npx resolves by default.
701
+ // -p ... finch-mcp names the actual MCP-protocol server explicitly
702
+ // (not `finch`, which is the human-interactive REPL and would break the
703
+ // JSON-RPC handshake if a host spawned it instead). Pinned to the currently
704
+ // installed version, never @latest - matches the security boundary
705
+ // documented in the README (wallet/credential access needs a stable,
706
+ // reviewable version, not a moving target).
707
+ function buildEntry(style) {
708
+ const base = {
709
+ command: "npx",
710
+ args: ["-y", "-p", `@finchagentic/mcp@${PKG_VERSION}`, "finch-mcp"],
711
+ env: {},
712
+ };
713
+ if (style === "vscode")
714
+ return { type: "stdio", ...base };
715
+ if (style === "zed")
716
+ return { source: "custom", ...base };
717
+ return base;
718
+ }
719
+ // Returns "skipped" (distinct from "error") when an existing config file can't
720
+ // be parsed - overwriting it would destroy the user's real config (VS Code
721
+ // mcp.json and Zed settings.json are JSONC and routinely contain comments,
722
+ // which JSON.parse rejects). In that case we leave the file untouched and the
723
+ // caller prints a manual-add snippet instead.
724
+ function installIntoConfig(client) {
725
+ try {
726
+ let json = {};
727
+ if (fs.existsSync(client.configPath)) {
728
+ const raw = fs.readFileSync(client.configPath, "utf8");
729
+ try {
730
+ json = JSON.parse(raw);
731
+ }
732
+ catch {
733
+ return "skipped"; // don't clobber an unparseable (likely JSONC) file
734
+ }
735
+ }
736
+ else {
737
+ // Ensure parent dir exists
738
+ fs.mkdirSync(path.dirname(client.configPath), { recursive: true });
739
+ }
740
+ const key = client.serversKey;
741
+ if (!json[key] || typeof json[key] !== "object")
742
+ json[key] = {};
743
+ const existed = !!json[key].finch;
744
+ json[key].finch = buildEntry(client.entryStyle);
745
+ fs.writeFileSync(client.configPath, JSON.stringify(json, null, 2), "utf8");
746
+ return existed ? "updated" : "added";
747
+ }
748
+ catch {
749
+ return "error";
750
+ }
751
+ }
752
+ async function installFlow() {
753
+ console.log(`\n ${C.cyan}${C.bold}finch install${C.reset}\n`);
754
+ console.log(` ${C.dim}Scanning for MCP-compatible apps...${C.reset}\n`);
755
+ const clients = resolveClients();
756
+ if (clients.length === 0) {
757
+ console.log(` ${C.yellow}No MCP-compatible apps found.${C.reset}`);
758
+ console.log(` ${C.dim}Install Claude Desktop, Cursor, or Windsurf, then run this again.${C.reset}\n`);
759
+ console.log(` ${C.dim}Or add manually to your app's MCP config:${C.reset}`);
760
+ console.log(` ${C.dim} "finch": { "command": "npx", "args": ["-y", "-p", "@finchagentic/mcp@${PKG_VERSION}", "finch-mcp"] }${C.reset}\n`);
761
+ return;
762
+ }
763
+ let installed = 0;
764
+ const toRestart = [];
765
+ const manual = [];
766
+ for (const client of clients) {
767
+ const result = installIntoConfig(client);
768
+ const short = client.configPath.replace(os.homedir(), "~");
769
+ if (result === "added") {
770
+ console.log(` ${C.green}✓${C.reset} ${C.bold}${client.name}${C.reset} ${C.dim}→ added${C.reset}`);
771
+ console.log(` ${C.dim}${short}${C.reset}`);
772
+ installed++;
773
+ toRestart.push(client.name);
774
+ }
775
+ else if (result === "updated") {
776
+ console.log(` ${C.green}↑${C.reset} ${C.bold}${client.name}${C.reset} ${C.dim}→ updated (re-pinned to v${PKG_VERSION})${C.reset}`);
777
+ console.log(` ${C.dim}${short}${C.reset}`);
778
+ installed++;
779
+ toRestart.push(client.name);
780
+ }
781
+ else if (result === "skipped") {
782
+ // Existing config couldn't be parsed (likely has comments) - never
783
+ // overwrite it. Show the user exactly what to paste under which key.
784
+ console.log(` ${C.yellow}⚠${C.reset} ${C.bold}${client.name}${C.reset} ${C.dim}→ has an existing config we won't overwrite${C.reset}`);
785
+ console.log(` ${C.dim}${short}${C.reset}`);
786
+ manual.push(` ${C.dim}${client.name}: add under ${C.reset}${C.cyan}"${client.serversKey}"${C.reset}${C.dim}:${C.reset}\n ${C.dim}${JSON.stringify({ finch: buildEntry(client.entryStyle) })}${C.reset}`);
787
+ }
788
+ else {
789
+ console.log(` ${C.yellow}✗${C.reset} ${C.bold}${client.name}${C.reset} ${C.dim}→ write failed (check permissions)${C.reset}`);
790
+ }
791
+ }
792
+ if (manual.length) {
793
+ console.log(`\n ${C.dim}Add these by hand (we didn't touch your existing config):${C.reset}`);
794
+ for (const m of manual)
795
+ console.log(m);
796
+ }
797
+ console.log(`\n ${C.dim}${"─".repeat(52)}${C.reset}\n`);
798
+ if (installed === 0) {
799
+ console.log(` ${C.yellow}Nothing was installed. Check file permissions.${C.reset}\n`);
800
+ return;
801
+ }
802
+ console.log(` ${C.green}${C.bold}✓ Finch installed in ${installed} app${installed === 1 ? "" : "s"}.${C.reset}\n`);
803
+ if (toRestart.length > 0) {
804
+ console.log(` ${C.dim}Restart to activate: ${toRestart.join(" · ")}${C.reset}`);
805
+ }
806
+ // Tool palette: the server exposes the CORE subset by default to keep the
807
+ // client's context lean. Everything else stays callable by name, and power
808
+ // users can flip on the full surface. Surfaced here so the count a user sees
809
+ // in their client (CORE) doesn't silently contradict the "all tools" framing.
810
+ console.log(`\n ${C.dim}Your client will see the ${CORE_TOOL_COUNT} core tools by default. ` +
811
+ `Add ${C.reset}${C.cyan}"env": { "FINCH_TOOLS": "all" }${C.reset}${C.dim} to the finch entry for all ${TOTAL_TOOL_COUNT}.${C.reset}`);
812
+ const cfg = (0, config_js_1.readConfig)();
813
+ if (!cfg.sessionToken) {
814
+ console.log(`\n ${C.dim}Next step - sign in to unlock all tools:${C.reset}`);
815
+ console.log(` ${C.cyan} finch login${C.reset}\n`);
816
+ }
817
+ else {
818
+ console.log(`\n ${C.dim}Already signed in as ${cfg.email ?? "user"}.${C.reset}`);
819
+ console.log(` ${C.dim}Open your MCP client and start using Finch.${C.reset}\n`);
820
+ }
821
+ }
822
+ async function doctorFlow() {
823
+ console.log(`\n ${C.cyan}${C.bold}finch doctor${C.reset} ${C.dim}v${PKG_VERSION}${C.reset}\n`);
824
+ console.log(` ${C.dim}Running diagnostic - this takes ~5 seconds.${C.reset}\n`);
825
+ const checks = [];
826
+ const cfg = (0, config_js_1.readConfig)();
827
+ const authHeader = cfg.sessionToken ? { Authorization: `Bearer ${cfg.sessionToken}` } : {};
828
+ // 1. LLM provider
829
+ const provider = process.env.BANKR_API_KEY ? "bankr"
830
+ : process.env.ANTHROPIC_API_KEY ? "anthropic"
831
+ : process.env.OPENAI_API_KEY ? "openai"
832
+ : "finch-proxy";
833
+ // Tools never call an LLM of their own - they return data and structure for
834
+ // the calling model. A key is what makes *finch itself* a host: the CLI
835
+ // agent loop and scheduled agents, which have no client model to borrow.
836
+ checks.push({
837
+ name: "LLM provider",
838
+ status: "✓",
839
+ detail: provider === "finch-proxy"
840
+ ? `none set · all ${TOTAL_TOOL_COUNT} tools work without one`
841
+ : `${provider} (direct) · used by \`finch run\` and scheduled agents`,
842
+ fix: provider === "finch-proxy"
843
+ ? `Only needed for \`finch run\` and scheduled agents — \`finch setup\` adds one`
844
+ : undefined,
845
+ });
846
+ // 2. Backend reachable
847
+ try {
848
+ const t0 = Date.now();
849
+ const res = await fetch(`${CONVEX_SITE}/memory/profile`, {
850
+ headers: authHeader,
851
+ signal: AbortSignal.timeout(8000),
852
+ });
853
+ const latency = Date.now() - t0;
854
+ if (res.status === 401) {
855
+ checks.push({
856
+ name: "Backend reachable", status: "⚠",
857
+ detail: `${CONVEX_SITE} → 401 (not signed in)`,
858
+ fix: `Run \`finch login\` to authenticate.`,
859
+ });
860
+ }
861
+ else if (res.ok) {
862
+ checks.push({
863
+ name: "Backend reachable", status: "✓",
864
+ detail: `${CONVEX_SITE} → ${res.status} (${latency}ms)`,
865
+ });
866
+ }
867
+ else {
868
+ checks.push({
869
+ name: "Backend reachable", status: "✗",
870
+ detail: `${CONVEX_SITE} → ${res.status}`,
871
+ fix: `Check FINCH_CONVEX_URL env var, or wait if the service is down.`,
872
+ });
873
+ }
874
+ }
875
+ catch (err) {
876
+ checks.push({
877
+ name: "Backend reachable", status: "✗",
878
+ detail: `${CONVEX_SITE} → ${err.message}`,
879
+ fix: `Network issue or wrong URL. Default: https://befitting-porcupine-276.convex.site`,
880
+ });
881
+ }
882
+ // 3. Auth state
883
+ if (cfg.sessionToken) {
884
+ checks.push({
885
+ name: "Authentication",
886
+ status: "✓",
887
+ detail: cfg.email
888
+ ? `Signed in as ${cfg.email}`
889
+ : `Session token present`,
890
+ });
891
+ }
892
+ else {
893
+ checks.push({
894
+ name: "Authentication",
895
+ status: "⚠",
896
+ detail: `No session token - running with local wallet signature only`,
897
+ fix: `Run \`finch login\` for persistent identity across MCP clients.`,
898
+ });
899
+ }
900
+ // 4. Local wallet
901
+ const walletPath = path.join(os.homedir(), ".finch", "wallet.json");
902
+ if (fs.existsSync(walletPath)) {
903
+ try {
904
+ const wallet = JSON.parse(fs.readFileSync(walletPath, "utf8"));
905
+ const addr = wallet.address ? (wallet.address.startsWith("0x") ? wallet.address : `0x${wallet.address}`) : "unknown";
906
+ checks.push({
907
+ name: "Local wallet", status: "✓",
908
+ detail: `${addr.slice(0, 6)}...${addr.slice(-4)} at ~/.finch/wallet.json`,
909
+ });
910
+ }
911
+ catch {
912
+ checks.push({
913
+ name: "Local wallet", status: "⚠",
914
+ detail: `wallet.json present but unreadable`,
915
+ fix: `Delete ~/.finch/wallet.json and re-run finch - new wallet auto-creates.`,
916
+ });
917
+ }
918
+ }
919
+ else {
920
+ checks.push({
921
+ name: "Local wallet", status: "⚠",
922
+ detail: `Not created yet`,
923
+ fix: `Auto-creates on first DeFi tool call (base_mcp_balance, etc).`,
924
+ });
925
+ }
926
+ // 5. Profile entries
927
+ if (cfg.sessionToken) {
928
+ try {
929
+ const res = await fetch(`${CONVEX_SITE}/vault/profile-context?maxChars=100`, {
930
+ headers: authHeader,
931
+ signal: AbortSignal.timeout(6000),
932
+ });
933
+ const data = await res.json();
934
+ if (data?.hasProfile) {
935
+ checks.push({
936
+ name: "Profile context", status: "✓",
937
+ detail: `Profile entries found - Claude auto-loads your context across sessions`,
938
+ });
939
+ }
940
+ else {
941
+ checks.push({
942
+ name: "Profile context", status: "⚠",
943
+ detail: `No profile entries yet`,
944
+ fix: `Run: vault_save type=memory key=profile/business content="<who you are, what you build>"`,
945
+ });
946
+ }
947
+ }
948
+ catch {
949
+ checks.push({
950
+ name: "Profile context", status: "⚠",
951
+ detail: `Could not fetch (backend issue)`,
952
+ });
953
+ }
954
+ }
955
+ // 6. Tool palette mode
956
+ const toolMode = process.env.FINCH_TOOLS ?? "core";
957
+ checks.push({
958
+ name: "Tool palette",
959
+ status: "✓",
960
+ detail: `Mode: ${toolMode}${toolMode === "core" ? ` (default - ${CORE_TOOL_COUNT} essential tools)` : toolMode === "all" ? ` (power user - ${TOTAL_TOOL_COUNT} tools)` : ` (custom subset)`}`,
961
+ fix: toolMode === "core"
962
+ ? `Set FINCH_TOOLS=all to expose all ${TOTAL_TOOL_COUNT} tools (raises LLM context cost).`
963
+ : undefined,
964
+ });
965
+ // 7. MEV-protect broadcast (optional belt-and-suspenders for swaps)
966
+ if (process.env.FINCH_BROADCAST_RPC) {
967
+ const host = (() => {
968
+ try {
969
+ return new URL(process.env.FINCH_BROADCAST_RPC).host;
970
+ }
971
+ catch {
972
+ return "custom";
973
+ }
974
+ })();
975
+ checks.push({
976
+ name: "MEV-protect", status: "✓",
977
+ detail: `Broadcasts routed through ${host} (private/MEV-protected)`,
978
+ });
979
+ }
980
+ else {
981
+ checks.push({
982
+ name: "MEV-protect", status: "⚠",
983
+ detail: `Standard Base RPC (sequencer is centralized, MEV is naturally low)`,
984
+ fix: `Optional: set FINCH_BROADCAST_RPC=<private-relay-url> for belt-and-suspenders routing.`,
985
+ });
986
+ }
987
+ // 8. GITHUB_TOKEN (optional but affects github_search_code)
988
+ if (process.env.GITHUB_TOKEN) {
989
+ checks.push({
990
+ name: "GitHub token", status: "✓",
991
+ detail: `GITHUB_TOKEN set - github_search_code + private repos work`,
992
+ });
993
+ }
994
+ else {
995
+ checks.push({
996
+ name: "GitHub token", status: "⚠",
997
+ detail: `No GITHUB_TOKEN - github_search_code disabled, other tools rate-limited to 60/hr`,
998
+ fix: `Optional. Create at https://github.com/settings/tokens (scopes: public_repo) and add to MCP env.`,
999
+ });
1000
+ }
1001
+ // 9. Local memory (self-hosted supermemory) - optional, zero-cost alternative
1002
+ // to the Convex-proxied cloud memory backend.
1003
+ const localMemCfg = (0, local_memory_js_1.getLocalMemoryConfig)();
1004
+ if (localMemCfg) {
1005
+ const reachable = await (0, local_memory_js_1.isLocalMemoryReachable)(localMemCfg);
1006
+ checks.push(reachable ? {
1007
+ name: "Local memory", status: "✓",
1008
+ detail: `supermemory reachable at ${localMemCfg.url} - memory tools run fully local, zero cost`,
1009
+ } : {
1010
+ name: "Local memory", status: "⚠",
1011
+ detail: `memoryBackend is "local" but ${localMemCfg.url} isn't reachable`,
1012
+ fix: `Start the local server (see \`npx supermemory local\`), or run \`finch setup\` again.`,
1013
+ });
1014
+ }
1015
+ else {
1016
+ checks.push({
1017
+ name: "Local memory", status: "⚠",
1018
+ detail: `Not configured - memory tools use the Finch-hosted proxy`,
1019
+ fix: `Run \`finch setup\` to switch to a free, self-hosted local memory backend.`,
1020
+ });
1021
+ }
1022
+ // 10. Local vault (user-owned, on-disk) - the account-free alternative to the
1023
+ // Convex-hosted vault.
1024
+ if (cfg.vaultBackend === "local") {
1025
+ const vaultDir = path.join(os.homedir(), ".finch", "vault");
1026
+ let entryCount = 0;
1027
+ try {
1028
+ const idx = JSON.parse(fs.readFileSync(path.join(vaultDir, "index.json"), "utf8"));
1029
+ entryCount = Object.keys(idx.entries ?? {}).length;
1030
+ }
1031
+ catch { /* no index yet */ }
1032
+ checks.push({
1033
+ name: "Local vault", status: "✓",
1034
+ detail: `on-disk at ~/.finch/vault (${entryCount} entr${entryCount === 1 ? "y" : "ies"}) - you own the data, no account needed`,
1035
+ });
1036
+ }
1037
+ else {
1038
+ checks.push({
1039
+ name: "Local vault", status: "⚠",
1040
+ detail: `Not configured - vault tools use the Finch-hosted store (needs login)`,
1041
+ fix: `Run \`finch setup\` to store vault artifacts locally, no account required.`,
1042
+ });
1043
+ }
1044
+ // ── Render ────────────────────────────────────────────────────────────────
1045
+ const colorFor = (s) => s === "✓" ? C.green : s === "⚠" ? C.yellow : C.red;
1046
+ const widest = Math.max(...checks.map((c) => c.name.length));
1047
+ for (const c of checks) {
1048
+ const pad = c.name.padEnd(widest);
1049
+ console.log(` ${colorFor(c.status)}${c.status}${C.reset} ${C.cyan}${pad}${C.reset} ${c.detail}`);
1050
+ if (c.fix)
1051
+ console.log(` ${" ".repeat(widest)} ${C.dim}→ ${c.fix}${C.reset}`);
1052
+ }
1053
+ const counts = { "✓": 0, "⚠": 0, "✗": 0 };
1054
+ checks.forEach((c) => counts[c.status]++);
1055
+ console.log("");
1056
+ console.log(` ${C.dim}Summary:${C.reset} ${C.green}${counts["✓"]} ok${C.reset} · ${C.yellow}${counts["⚠"]} warning${C.reset} · ${C.red}${counts["✗"]} critical${C.reset}`);
1057
+ console.log("");
1058
+ if (counts["✗"] > 0) {
1059
+ console.log(` ${C.red}Critical issues found - fix the lines above before relying on finch.${C.reset}\n`);
1060
+ process.exit(1);
1061
+ }
1062
+ else if (counts["⚠"] > 0) {
1063
+ console.log(` ${C.dim}Warnings are non-blocking - finch works but could be smoother.${C.reset}\n`);
1064
+ }
1065
+ else {
1066
+ console.log(` ${C.green}All systems healthy.${C.reset}\n`);
1067
+ }
1068
+ }
1069
+ // ── vault - inspect the local, user-owned vault ──────────────────────────────
1070
+ function vaultFlow() {
1071
+ const cfg = (0, config_js_1.readConfig)();
1072
+ const dir = path.join(os.homedir(), ".finch", "vault");
1073
+ const shortDir = dir.replace(os.homedir(), "~");
1074
+ const SEP_V = ` ${"─".repeat(52)}`;
1075
+ console.log(`\n ${C.cyan}${C.bold}finch vault${C.reset}\n`);
1076
+ if (cfg.vaultBackend !== "local") {
1077
+ console.log(` ${C.yellow}○${C.reset} Local vault is ${C.yellow}off${C.reset} - vault tools use the hosted store (needs \`finch login\`).`);
1078
+ console.log(` ${C.dim}Turn it on:${C.reset} ${C.cyan}finch setup${C.reset} ${C.dim}(answer "y" to local vault) - then your data lives here, on your machine.${C.reset}\n`);
1079
+ return;
1080
+ }
1081
+ let entries = 0, versions = 0, creds = 0;
1082
+ try {
1083
+ const idx = JSON.parse(fs.readFileSync(path.join(dir, "index.json"), "utf8"));
1084
+ const e = idx.entries ?? {};
1085
+ entries = Object.keys(e).length;
1086
+ versions = Object.values(e).reduce((n, x) => n + (x?.versions?.length ?? 0), 0);
1087
+ }
1088
+ catch { /* no index yet */ }
1089
+ try {
1090
+ creds = Object.keys(JSON.parse(fs.readFileSync(path.join(dir, "credentials.json"), "utf8"))).length;
1091
+ }
1092
+ catch { /* none */ }
1093
+ console.log(` ${C.green}●${C.reset} Local vault is ${C.green}on${C.reset} - your data, on your machine, no account.`);
1094
+ console.log(`${SEP_V}`);
1095
+ console.log(` ${C.dim}Location ${C.reset}${shortDir}`);
1096
+ console.log(` ${C.dim}Contents ${C.reset}${entries} entr${entries === 1 ? "y" : "ies"} ${C.dim}·${C.reset} ${versions} version${versions === 1 ? "" : "s"} ${C.dim}·${C.reset} ${creds} credential${creds === 1 ? "" : "s"} ${C.dim}(encrypted)${C.reset}`);
1097
+ console.log(`${SEP_V}`);
1098
+ console.log(` ${C.dim}It's a plain folder - back it up or sync it however you like:${C.reset}`);
1099
+ console.log(` ${C.cyan}cp -r ${shortDir} ~/backup${C.reset} ${C.dim}# copy${C.reset}`);
1100
+ console.log(` ${C.cyan}git -C ${shortDir} init${C.reset} ${C.dim}# version-control it${C.reset}`);
1101
+ console.log(` ${C.dim}Nothing leaves this machine unless you move it.${C.reset}\n`);
1102
+ }
1103
+ // ── Entry point ───────────────────────────────────────────────────────────────
1104
+ const cmd = process.argv[2];
1105
+ if (cmd === "install") {
1106
+ installFlow().catch((err) => {
1107
+ console.error(` ${C.red}✗ install error: ${err.message}${C.reset}`);
1108
+ process.exit(1);
1109
+ });
1110
+ }
1111
+ else if (cmd === "login") {
1112
+ // loginFlow() already checks FINCH_API_KEY first and falls back to the
1113
+ // interactive prompt - no need to duplicate that check here.
1114
+ loginFlow().catch((err) => {
1115
+ console.error(` ${C.red}✗ login error: ${err.message}${C.reset}`);
1116
+ process.exit(1);
1117
+ });
1118
+ }
1119
+ else if (cmd === "doctor") {
1120
+ doctorFlow().catch((err) => {
1121
+ console.error(` ${C.red}✗ doctor error: ${err.message}${C.reset}`);
1122
+ process.exit(1);
1123
+ });
1124
+ }
1125
+ else if (cmd === "setup") {
1126
+ setupFlow().catch((err) => {
1127
+ console.error(` ${C.red}✗ setup error: ${err.message}${C.reset}`);
1128
+ process.exit(1);
1129
+ });
1130
+ }
1131
+ else if (cmd === "vault") {
1132
+ vaultFlow();
1133
+ }
1134
+ else if (cmd === "logout") {
1135
+ const cfg = (0, config_js_1.readConfig)();
1136
+ if (!cfg.sessionToken) {
1137
+ console.log(`\n ${C.dim}Already signed out.${C.reset}\n`);
1138
+ }
1139
+ else {
1140
+ (0, config_js_1.writeConfig)({ sessionToken: undefined, email: undefined });
1141
+ console.log(`\n ${C.green}✓${C.reset} Signed out${cfg.email ? ` (${cfg.email})` : ""}. Token cleared from ~/.finch/config.json\n`);
1142
+ }
1143
+ }
1144
+ else if (cmd === "status") {
1145
+ const cfg = (0, config_js_1.readConfig)();
1146
+ const walletAddr = getLocalWalletAddress();
1147
+ const provider = process.env.BANKR_API_KEY ? "Bankr"
1148
+ : process.env.ANTHROPIC_API_KEY ? "Anthropic"
1149
+ : process.env.OPENAI_API_KEY ? "OpenAI"
1150
+ : "Finch proxy";
1151
+ const SEP_S = ` ${"─".repeat(52)}`;
1152
+ console.log(`\n${SEP_S}`);
1153
+ if (cfg.sessionToken) {
1154
+ const displayName = cfg.name ? `${C.white}${C.bold}${cfg.name}${C.reset} ` : "";
1155
+ const displayEmail = cfg.email ? `${C.dim}${cfg.email}${C.reset}` : "";
1156
+ console.log(` ${C.green}●${C.reset} ${displayName}${displayEmail}`);
1157
+ 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}`);
1159
+ }
1160
+ else {
1161
+ console.log(` ${C.dim}Wallet not created yet${C.reset}`);
1162
+ }
1163
+ console.log(` ${C.dim}LLM ${C.reset}${C.green}${provider}${C.reset} ${C.dim}· ${TOTAL_TOOL_COUNT} tools · v${PKG_VERSION}${C.reset}`);
1164
+ }
1165
+ else {
1166
+ console.log(` ${C.yellow}○${C.reset} ${C.yellow}Not signed in${C.reset}`);
1167
+ console.log(` ${C.dim}→ run \`finch login\` to unlock all ${TOTAL_TOOL_COUNT} tools${C.reset}`);
1168
+ console.log(` ${C.dim}LLM ${provider} · v${PKG_VERSION}${C.reset}`);
1169
+ }
1170
+ console.log(`${SEP_S}\n`);
1171
+ }
1172
+ else if (cmd === "help" || cmd === "--help" || cmd === "-h") {
1173
+ console.log(`
1174
+ ${C.cyan}${C.bold}finch${C.reset} ${C.dim}runtime layer for Agentic AI · terminal CLI${C.reset}
1175
+
1176
+ ${C.cyan}Commands:${C.reset}
1177
+ finch Start interactive AI terminal
1178
+ finch install Auto-configure all detected MCP clients
1179
+ finch login Sign in to unlock all tools
1180
+ finch logout Sign out and clear saved token
1181
+ finch status Show auth state and version (quick check)
1182
+ finch doctor Run a full health check + suggest fixes
1183
+ finch setup Configure your own LLM key and/or local memory + vault
1184
+ finch vault Show your local vault (location, contents, backup)
1185
+ finch help Show this help
1186
+
1187
+ ${C.dim}Claude Code / Cursor / Windsurf / Codex / Aeon / Antigravity / Zed — anywhere MCP runs.${C.reset}
1188
+ `);
1189
+ }
1190
+ else {
1191
+ main().catch(err => {
1192
+ console.error(`Fatal: ${err.message}`);
1193
+ process.exit(1);
1194
+ });
1195
+ }