@finchagentic/mcp 4.7.1 → 4.7.3

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