@finchagentic/mcp 4.2.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,13 +29,13 @@ Always pin the version. Never use `@latest`.
29
29
 
30
30
  ```bash
31
31
  # One-command installer (detects common MCP clients)
32
- npx -y -p @finchagentic/mcp@4.2.0 finch install
32
+ npx -y -p @finchagentic/mcp@4.5.0 finch install
33
33
  ```
34
34
 
35
35
  ### Claude Code
36
36
 
37
37
  ```bash
38
- claude mcp add finch -s user -- npx -y -p @finchagentic/mcp@4.2.0 finch-mcp
38
+ claude mcp add finch -s user -- npx -y -p @finchagentic/mcp@4.5.0 finch-mcp
39
39
  ```
40
40
 
41
41
  ### Cursor / Windsurf / Claude Desktop
@@ -45,7 +45,7 @@ claude mcp add finch -s user -- npx -y -p @finchagentic/mcp@4.2.0 finch-mcp
45
45
  "mcpServers": {
46
46
  "finch": {
47
47
  "command": "npx",
48
- "args": ["-y", "-p", "@finchagentic/mcp@4.2.0", "finch-mcp"]
48
+ "args": ["-y", "-p", "@finchagentic/mcp@4.5.0", "finch-mcp"]
49
49
  }
50
50
  }
51
51
  }
@@ -59,7 +59,7 @@ claude mcp add finch -s user -- npx -y -p @finchagentic/mcp@4.2.0 finch-mcp
59
59
  "finch": {
60
60
  "type": "stdio",
61
61
  "command": "npx",
62
- "args": ["-y", "-p", "@finchagentic/mcp@4.2.0", "finch-mcp"]
62
+ "args": ["-y", "-p", "@finchagentic/mcp@4.5.0", "finch-mcp"]
63
63
  }
64
64
  }
65
65
  }
@@ -79,7 +79,7 @@ claude mcp add finch -s user -- npx -y -p @finchagentic/mcp@4.2.0 finch-mcp
79
79
 
80
80
  </details>
81
81
 
82
- No LLM API key is required to start — 108 of 113 tools are plain reads/writes/on-chain calls that your MCP client's own model already drives; only 5 (`ask_finch`, `deep_research`, and scheduled agent learning) do their own multi-step reasoning server-side and need a key (see [Configuration](#configuration)). Tools load on first use.
82
+ No LLM API key is required to start — 111 of 116 tools are plain reads/writes/on-chain calls that your MCP client's own model already drives; only 5 (`ask_finch`, `deep_research`, and scheduled agent learning) do their own multi-step reasoning server-side and need a key (see [Configuration](#configuration)). Tools load on first use.
83
83
 
84
84
  ## Quick start
85
85
 
@@ -87,6 +87,7 @@ No LLM API key is required to start — 108 of 113 tools are plain reads/writes/
87
87
  finch doctor # health check
88
88
  finch setup # local vault / memory / providers
89
89
  finch vault # inspect local vault
90
+ finch orders # schedule Robinhood Chain DCA / TP-SL order ticks
90
91
  ```
91
92
 
92
93
  Try in your MCP client:
@@ -99,15 +100,19 @@ save this thesis to vault
99
100
 
100
101
  ## What you get
101
102
 
102
- **113 tools** across four pillars:
103
+ **116 tools** across four pillars:
103
104
 
104
105
  | Pillar | What it does |
105
106
  |--------|----------------|
106
107
  | **Memory** | Full-text searchable memory + versioned vault + chronicle |
107
- | **Agents** | Spawn, recall, update named agents |
108
- | **Workflows** | Automations, monitors, packets, deep research |
108
+ | **Agents** | Spawn, recall, update named agents — `agent_recall` also pulls related memory/vault context matching the agent's goal, not just its own logged updates |
109
+ | **Workflows** | Automations, monitors, packets, deep research (auto-saves reports + auto-links related past research) |
109
110
  | **Execution** | Base DeFi, Robinhood Chain, market data, web, GitHub |
110
111
 
112
+ Coding and research sessions persist the same way: `deep_research` auto-saves its report to vault and links it to related past reports; `code_session_save` does the same for coding/debugging sessions (`vault_save type=code`, versioned per project, auto-linked). Both exist so the next session — yours or another agent's — starts with real context instead of cold.
113
+
114
+ `vault_save` and `agent_spawn` also take an optional `workspaceProject` - the same named Projects a user organizes their Agents/vault content into on the webapp's Agents page. Pass a name and it's matched case-insensitively or created automatically (`list_projects` to browse what exists first). Hosted vault only - local-vault mode has no project concept.
115
+
111
116
  Default palette is `core` (lighter context). Full set:
112
117
 
113
118
  ```json
@@ -119,7 +124,7 @@ Default palette is `core` (lighter context). Full set:
119
124
  Finch is the runtime. **Your LLM is the brain. Your data stays yours.**
120
125
 
121
126
  ```bash
122
- npx -y -p @finchagentic/mcp@4.2.0 finch setup
127
+ npx -y -p @finchagentic/mcp@4.5.0 finch setup
123
128
  # enable local vault (and optional local memory)
124
129
  ```
125
130
 
@@ -146,12 +151,12 @@ Scheduled/cloud features still need an account. Core memory, vault, and public-d
146
151
  | `GITHUB_TOKEN` | For `github_search_code` |
147
152
  | `ALCHEMY_API_KEY` | Faster Base RPC (optional) |
148
153
 
149
- **Cost model:** almost everything is free to run — the other 108 tools are plain API/RPC calls, and your MCP client's own model (Claude, GPT, whatever's driving the chat) does all the tool-selection reasoning at no cost to Finch. The 5 exceptions above need their own key because their reasoning happens *inside* the tool call, invisible to your client, and can't be delegated to it. Set exactly one of the four env vars and every tool that needs it will use it automatically.
154
+ **Cost model:** almost everything is free to run — the other 110 tools are plain API/RPC calls, and your MCP client's own model (Claude, GPT, whatever's driving the chat) does all the tool-selection reasoning at no cost to Finch. The 5 exceptions above need their own key because their reasoning happens *inside* the tool call, invisible to your client, and can't be delegated to it. Set exactly one of the four env vars and every tool that needs it will use it automatically.
150
155
 
151
156
  Guided setup:
152
157
 
153
158
  ```bash
154
- npx -y -p @finchagentic/mcp@4.2.0 finch setup
159
+ npx -y -p @finchagentic/mcp@4.5.0 finch setup
155
160
  ```
156
161
 
157
162
  ## Security
@@ -160,7 +165,7 @@ npx -y -p @finchagentic/mcp@4.2.0 finch setup
160
165
  |:-:|----------|------|
161
166
  | 1 | Prompt injection | External content is data only — never instructions |
162
167
  | 2 | Mainnet confirm | Estimate → preview → confirm → execute |
163
- | 3 | Pinned install | Always `@finchagentic/mcp@4.2.0`, never `@latest` |
168
+ | 3 | Pinned install | Always `@finchagentic/mcp@4.5.0`, never `@latest` |
164
169
  | 4 | Credential vault | Never paste secrets into prompts or third-party tools |
165
170
  | 5 | Data disclosure | Know what leaves the machine (LLM, Firecrawl, GitHub, chain RPCs) |
166
171
  | 6 | Server monitors | Scheduled jobs need explicit confirmation |
@@ -26,7 +26,7 @@ const WRITE_IDEMPOTENT = new Set([
26
26
  const WRITE = new Set([
27
27
  "agent_spawn",
28
28
  "chronicle_add",
29
- "create_automation",
29
+ "code_session_save",
30
30
  "memory_add",
31
31
  "memory_extract",
32
32
  "memory_consolidate",
@@ -65,6 +65,13 @@ const DESTRUCTIVE = new Set([
65
65
  "rh_mcp_swap",
66
66
  "rh_dca_create", // arms recurring real buys
67
67
  "rh_bracket_create", // arms real TP/SL sells
68
+ // a swap/send automation arms unattended, REPEATING real fund movement
69
+ // (the backend's 1-minute cron evaluator fires it) - same risk class as
70
+ // rh_dca_create/rh_bracket_create above, not a plain additive write. An
71
+ // alert-only automation doesn't move funds, but the tool can't tell which
72
+ // kind it's about to create until AFTER the backend parses rawInput, so
73
+ // it's classified by its worst case, same reasoning as rh_orders_tick.
74
+ "create_automation",
68
75
  "rh_orders_tick", // preview by default, but can execute:true and move funds
69
76
  // executors that run other (possibly fund-moving) tools
70
77
  "run_automation",
@@ -73,6 +80,7 @@ const DESTRUCTIVE = new Set([
73
80
  // for a fixed period; unstake moves it (plus rewards) back
74
81
  "stake_finch",
75
82
  "unstake_finch",
83
+ "claim_vested_rewards", // treasury -> custodial wallet USDG transfer
76
84
  ]);
77
85
  function annotationsFor(name) {
78
86
  if (DESTRUCTIVE.has(name)) {
package/dist/cli.js CHANGED
@@ -1127,6 +1127,138 @@ function vaultFlow() {
1127
1127
  console.log(` ${C.cyan}git -C ${shortDir} init${C.reset} ${C.dim}# version-control it${C.reset}`);
1128
1128
  console.log(` ${C.dim}Nothing leaves this machine unless you move it.${C.reset}\n`);
1129
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
+ }
1130
1262
  // ── Entry point ───────────────────────────────────────────────────────────────
1131
1263
  const cmd = process.argv[2];
1132
1264
  if (cmd === "install") {
@@ -1158,6 +1290,12 @@ else if (cmd === "setup") {
1158
1290
  else if (cmd === "vault") {
1159
1291
  vaultFlow();
1160
1292
  }
1293
+ else if (cmd === "orders") {
1294
+ ordersFlow().catch((err) => {
1295
+ console.error(` ${C.red}✗ orders error: ${err.message}${C.reset}`);
1296
+ process.exit(1);
1297
+ });
1298
+ }
1161
1299
  else if (cmd === "logout") {
1162
1300
  const cfg = (0, config_js_1.readConfig)();
1163
1301
  if (!cfg.sessionToken) {
@@ -1239,6 +1377,7 @@ else if (cmd === "help" || cmd === "--help" || cmd === "-h") {
1239
1377
  finch doctor Run a full health check + suggest fixes
1240
1378
  finch setup Configure your own LLM key and/or local memory + vault
1241
1379
  finch vault Show your local vault (location, contents, backup)
1380
+ finch orders Run/schedule Robinhood Chain DCA & TP-SL order ticks
1242
1381
  finch help Show this help
1243
1382
 
1244
1383
  ${C.dim}Claude Code / Cursor / Windsurf / Codex / Aeon / Antigravity / Zed — anywhere MCP runs.${C.reset}
package/dist/convex.js CHANGED
@@ -28,7 +28,17 @@ async function attemptConvex(url, method, headers, body, timeoutMs = 30000) {
28
28
  signal: AbortSignal.timeout(timeoutMs),
29
29
  });
30
30
  }
31
- async function callConvex(path, method, body, toolName = "unknown", timeoutMs = 30000) {
31
+ async function callConvex(path, method, body, toolName = "unknown", timeoutMs = 30000,
32
+ // Fund-moving mutations (stake, unstake, claim rewards, a real - non-
33
+ // dryRun - automation trigger) are NOT idempotent server-side: there is
34
+ // no request-id dedup on the backend, so silently retrying a POST whose
35
+ // response was merely lost (a network blip or client-side timeout AFTER
36
+ // the server already processed it) can double-execute a real transfer.
37
+ // Pass true here for any such call - it trades the convenience of an
38
+ // automatic retry for never risking a duplicate execution; the caller
39
+ // sees a clear "may have already gone through" error instead and can
40
+ // check status before deciding to retry by hand.
41
+ noRetry = false) {
32
42
  const url = `${exports.CONVEX_SITE}${path}`;
33
43
  const headers = { "Content-Type": "application/json" };
34
44
  const apiKey = process.env.FINCH_API_KEY;
@@ -66,7 +76,8 @@ async function callConvex(path, method, body, toolName = "unknown", timeoutMs =
66
76
  if (process.env.BANKR_API_KEY)
67
77
  headers["X-User-Bankr-Key"] = process.env.BANKR_API_KEY;
68
78
  let lastError = null;
69
- for (let attempt = 0; attempt < RETRY_DELAYS.length; attempt++) {
79
+ const attempts = noRetry ? 1 : RETRY_DELAYS.length;
80
+ for (let attempt = 0; attempt < attempts; attempt++) {
70
81
  if (attempt > 0) {
71
82
  await new Promise((r) => setTimeout(r, RETRY_DELAYS[attempt - 1]));
72
83
  }
@@ -75,7 +86,14 @@ async function callConvex(path, method, body, toolName = "unknown", timeoutMs =
75
86
  res = await attemptConvex(url, method, headers, body, timeoutMs);
76
87
  }
77
88
  catch (err) {
78
- lastError = err;
89
+ // Ambiguous: the request may never have reached the server, or it may
90
+ // have been received and even processed before the connection died -
91
+ // there's no way to tell from here. Safe to retry for read-only/
92
+ // idempotent calls; for a fund-moving one, retrying risks resending a
93
+ // mutation that already landed.
94
+ lastError = noRetry
95
+ ? new Error(`${err?.message ?? err} - request may or may not have completed (no response received). Check status before retrying manually.`)
96
+ : err;
79
97
  continue;
80
98
  }
81
99
  if (res.status === 402) {
@@ -89,7 +107,7 @@ async function callConvex(path, method, body, toolName = "unknown", timeoutMs =
89
107
  `Hint: ${b.hint || 'Add FINCH_SESSION_TOKEN=… to the env block in your MCP config'}\n\n` +
90
108
  `${b.alternative ? `Alternative: ${b.alternative}` : ""}`);
91
109
  }
92
- if (RETRY_STATUSES.has(res.status) && attempt < RETRY_DELAYS.length) {
110
+ if (RETRY_STATUSES.has(res.status) && attempt < attempts - 1) {
93
111
  // Capture the actual body so a deterministic error (e.g. "unknown
94
112
  // token") that happens to come back on a 500 still surfaces its real
95
113
  // message if retries exhaust - previously this discarded the body
@@ -99,6 +117,15 @@ async function callConvex(path, method, body, toolName = "unknown", timeoutMs =
99
117
  lastError = new Error(`Finch API error ${res.status}: ${bodyText.slice(0, 300) || "(no body)"}`);
100
118
  continue;
101
119
  }
120
+ // noRetry + a retryable status on the one and only attempt: the server
121
+ // definitely received this one (unlike the network-exception case
122
+ // above), so it may have partially or fully processed it before
123
+ // erroring - same "check before retrying" caution, worded for the case
124
+ // where we know a response did come back.
125
+ if (RETRY_STATUSES.has(res.status) && noRetry) {
126
+ const bodyText = await res.text().catch(() => "");
127
+ throw new Error(`Finch API error ${res.status}: ${bodyText.slice(0, 300) || "(no body)"} - the server received this request and may have processed it before erroring. Check status before retrying manually.`);
128
+ }
102
129
  if (!res.ok)
103
130
  throw new Error(`Finch API error: ${res.status} ${await res.text()}`);
104
131
  return res.json();
package/dist/index.js CHANGED
@@ -66,14 +66,17 @@ const C = {
66
66
  reset: "\x1b[0m",
67
67
  bold: "\x1b[1m",
68
68
  };
69
+ // Was still spelling out the pre-rebrand "NOELCLAW" wordmark in ASCII art -
70
+ // leftover from before the Finch rebrand, never caught because nothing
71
+ // visually diffs a banner string.
69
72
  const BANNER = `
70
73
  ${C.cyan}
71
- ███╗ ██╗ ██████╗ ███████╗██╗ ██████╗██╗ █████╗ ██╗ ██╗
72
- ████╗ ██║██╔═══██╗██╔════╝██║ ██╔════╝██║ ██╔══██╗██║ ██║
73
- ██╔██╗ ██║██║ ██║█████╗ ██║ ██║ ██║ ███████║██║ █╗ ██║
74
- ██║╚██╗██║██║ ██║██╔══╝ ██║ ██║ ██║ ██╔══██║██║███╗██║
75
- ██║ ╚████║╚██████╔╝███████╗███████╗╚██████╗███████╗██║ ██║╚███╔███╔╝
76
- ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝
74
+ ███████╗██╗███╗ ██╗ ██████╗██╗ ██╗
75
+ ██╔════╝██║████╗ ██║██╔════╝██║ ██║
76
+ █████╗ ██║██╔██╗ ██║██║ ███████║
77
+ ██╔══╝ ██║██║╚██╗██║██║ ██╔══██║
78
+ ██║ ██║██║ ╚████║╚██████╗██║ ██║
79
+ ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═════╝╚═╝ ╚═╝
77
80
  ${C.reset}`;
78
81
  function line(label, value, color = C.cyan) {
79
82
  const pad = " ".repeat(Math.max(0, 12 - label.length));
@@ -114,9 +117,9 @@ async function main() {
114
117
  { label: "Base MCP", match: n => n.startsWith("base_mcp_") },
115
118
  { label: "RH MCP", match: n => n.startsWith("rh_") },
116
119
  { label: "Automation", match: n => /^(create_automation|list_automations|pause_automation|delete_automation|get_automation_runs|run_automation)$/.test(n) },
117
- { label: "Vault", match: n => n.startsWith("vault_") },
120
+ { label: "Vault", match: n => n.startsWith("vault_") || n === "code_session_save" || n === "list_projects" },
118
121
  { label: "Wallet", match: n => /^(get_wallet_address|get_wallet_balance|wallet_sign_message)$/.test(n) },
119
- { label: "Staking", match: n => /^(stake_finch|unstake_finch|stake_finch_status|stake_auto_restake)$/.test(n) },
122
+ { label: "Staking", match: n => /^(stake_finch|unstake_finch|stake_finch_status|stake_auto_restake|claim_vested_rewards)$/.test(n) },
120
123
  { label: "MiroShark", match: n => n.startsWith("miroshark_") },
121
124
  { label: "Scanner", match: n => /^(scan_market|score_token|check_token)$/.test(n) },
122
125
  { label: "Agents", match: n => n.startsWith("agent_") },
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isLocalVaultActive = isLocalVaultActive;
4
+ exports.resolveProjectId = resolveProjectId;
5
+ const convex_js_1 = require("./convex.js");
6
+ const local_vault_js_1 = require("./local-vault.js");
7
+ // MCP has no persistent "active project" UI state the way the webapp Sidebar
8
+ // switcher does - a caller (vault_save, agent_spawn) names the project it
9
+ // wants by string on each call instead. Resolution + auto-create both happen
10
+ // server-side (POST /projects/resolve, see convex/http.ts +
11
+ // convex/projects.ts's resolveOrCreateProjectForUser) so the match/dedup
12
+ // logic exists in exactly one place, shared with the webapp's own project
13
+ // picker.
14
+ //
15
+ // Local-vault mode (vaultBackend: "local") has no project concept at all -
16
+ // it's a flat, unauthenticated file store on the user's own machine with no
17
+ // server to resolve a name against. Callers should check
18
+ // isLocalVaultActive() first and skip resolution entirely rather than call
19
+ // this and get a confusing network-style failure.
20
+ function isLocalVaultActive() {
21
+ return !!(0, local_vault_js_1.getLocalVaultConfig)();
22
+ }
23
+ async function resolveProjectId(name) {
24
+ const trimmed = name.trim();
25
+ if (!trimmed)
26
+ return null;
27
+ try {
28
+ return await (0, convex_js_1.callConvex)("/projects/resolve", "POST", { name: trimmed }, "vault_save");
29
+ }
30
+ catch {
31
+ // Resolution is best-effort - a save should never fail just because the
32
+ // project lookup did. Falls back to unassigned, same as the webapp's
33
+ // agent_spawn membership check when a stale/bad id doesn't match.
34
+ return null;
35
+ }
36
+ }
package/dist/server.js CHANGED
@@ -115,7 +115,7 @@ exports.ALL_TOOLS = [
115
115
  // SWARM_TOOLS removed v3.19 - multi-agent research is now built into
116
116
  // deep_research (depth=standard|deep). Handler fully removed v3.21.
117
117
  // FRAMEWORK_TOOLS removed - list_playbooks/run_playbook/get_finch_ledger backend routes never existed, see tools/ import comment above.
118
- ...vault_js_1.VAULT_TOOLS, // 15 - save, read, list, search, history, diff, export, pin, unpublish, tag, delete, link, related, store_credential, get_credential
118
+ ...vault_js_1.VAULT_TOOLS, // 17 - save, code_session_save, read, list, search, history, diff, export, pin, unpublish, tag, delete, link, related, store_credential, get_credential, list_projects
119
119
  ...wallet_js_1.WALLET_TOOLS, // 3 - get_wallet_address, get_wallet_balance, wallet_sign_message
120
120
  ...miroshark_js_1.MIROSHARK_TOOLS, // 3 - simulate, status, stop
121
121
  ...agents_js_1.AGENT_TOOLS, // 4 - agent_spawn, agent_recall, agent_update, agent_ledger (vault-backed, all working). list_agents/hire_agent and the autonomous-schedule tools (agent_identity/agent_schedule/agent_unschedule/agent_pause/agent_resume/agent_runs) removed - their backend routes were never implemented, see tools/agents.ts
@@ -139,14 +139,22 @@ exports.ALL_TOOLS = [
139
139
  ...github_js_1.GITHUB_TOOLS, // 8 - list_repos, list_prs, get_pr, list_issues, get_issue, get_file, get_commits, search_code
140
140
  ...chronicle_js_1.CHRONICLE_TOOLS, // 4 - chronicle_add, chronicle_list, chronicle_search, chronicle_stats
141
141
  ...packets_js_1.PACKET_TOOLS, // 4 - packet_create, packet_run, packet_list, packet_share
142
- ...stake_js_1.STAKE_TOOLS, // 4 - stake_finch_status, stake_finch, unstake_finch, stake_auto_restake (custodial wallet; requires `finch login`)
143
- // total: 113 tools as measured by ALL_TOOLS.length - do not hand-maintain a
142
+ ...stake_js_1.STAKE_TOOLS, // 5 - stake_finch_status, stake_finch, unstake_finch, claim_vested_rewards, stake_auto_restake (custodial wallet; requires `finch login`)
143
+ // total: 116 tools as measured by ALL_TOOLS.length - do not hand-maintain a
144
144
  // count in this comment (drifted stale multiple times already: before
145
145
  // staking was added, after the P1 audit removed 9 dead framework/agent-
146
146
  // schedule tools, after memory_publish was removed for promising a
147
- // marketplace that doesn't exist, and after stake_auto_restake was added;
148
- // ALL_TOOLS.length is the only number that can't lie). Per-category counts
149
- // above are best-effort documentation, not load-bearing anywhere.
147
+ // marketplace that doesn't exist, after stake_auto_restake was added,
148
+ // after claim_vested_rewards was added to close the gap where the
149
+ // stake-lifecycle notification told users to "run claimVestedRewards" but
150
+ // no MCP tool by that name existed, after code_session_save was added so
151
+ // coding sessions persist the same way deep_research already auto-saves
152
+ // research, and after list_projects was added alongside `workspaceProject`
153
+ // support on vault_save/agent_spawn - MCP tools can now file into the same
154
+ // Projects the webapp Agents page organizes by, resolved/auto-created
155
+ // server-side via POST /projects/resolve; ALL_TOOLS.length is the only
156
+ // number that can't lie). Per-category counts above are best-effort
157
+ // documentation, not load-bearing anywhere.
150
158
  ];
151
159
  exports.HANDLER_MAP = new Map([
152
160
  ...market_js_1.MARKET_TOOLS.map(t => [t.name, market_js_1.handleMarketTool]),
@@ -27,11 +27,11 @@ exports.filterTools = filterTools;
27
27
  // through that same preset). New tools MUST match a preset or the test fails
28
28
  // the build, instead of quietly vanishing like these did.
29
29
  const PRESETS = {
30
- core: /^(memory_|vault_|agent_|ask_finch|finch_status|finch_diagnostics|finch_shell_chat|get_wallet_address|get_wallet_balance|wallet_sign_message|chronicle_|packet_)/,
31
- defi: /^(get_market_data|get_token_data|compare_tokens|market_overview|token_history|get_base_token_data|stock_fundamentals|stock_insider|stock_events|market_thesis|trade_plan|base_mcp_|rh_|base_|get_defi_yields|score_token|check_token|scan_market|get_wallet_balance|wallet_sign_message|create_automation|list_automations|pause_automation|delete_automation|get_automation_runs|run_automation|miroshark_|stake_|unstake_finch)/,
32
- research: /^(memory_|vault_|deep_research|research_compare|research_chain|web_search|web_scrape|schedule_research|list_monitors|cancel_monitor|ask_finch|stock_fundamentals|stock_insider|stock_events)/,
33
- memory: /^(memory_|vault_|agent_|chronicle_)/,
34
- coder: /^(audit_contract|github_)/,
30
+ core: /^(memory_|vault_|code_session_save|list_projects|agent_|ask_finch|finch_status|finch_diagnostics|finch_shell_chat|get_wallet_address|get_wallet_balance|wallet_sign_message|chronicle_|packet_)/,
31
+ defi: /^(get_market_data|get_token_data|compare_tokens|market_overview|token_history|get_base_token_data|stock_fundamentals|stock_insider|stock_events|market_thesis|trade_plan|base_mcp_|rh_|base_|get_defi_yields|score_token|check_token|scan_market|get_wallet_balance|wallet_sign_message|create_automation|list_automations|pause_automation|delete_automation|get_automation_runs|run_automation|miroshark_|stake_|unstake_finch|claim_vested_rewards)/,
32
+ research: /^(memory_|vault_|code_session_save|list_projects|deep_research|research_compare|research_chain|web_search|web_scrape|schedule_research|list_monitors|cancel_monitor|ask_finch|stock_fundamentals|stock_insider|stock_events)/,
33
+ memory: /^(memory_|vault_|code_session_save|list_projects|agent_|chronicle_)/,
34
+ coder: /^(audit_contract|github_|code_session_save)/,
35
35
  };
36
36
  function filterTools(allTools) {
37
37
  // Default is "core" - keeps LLM context cost low while everything
@@ -7,6 +7,8 @@ const zod_1 = require("zod");
7
7
  const convex_js_1 = require("../convex.js");
8
8
  const llm_js_1 = require("../llm.js");
9
9
  const local_vault_js_1 = require("../local-vault.js");
10
+ const memory_js_1 = require("./memory.js");
11
+ const project_js_1 = require("../project.js");
10
12
  // ─── Agent Learning Memory (v3.25) ──────────────────────────────────────────
11
13
  // After every agent_update, an LLM reviews the new progress in context of the
12
14
  // agent's goal + prior learnings to extract a single repeatable insight. The
@@ -41,7 +43,7 @@ async function extractLearning(goal, priorUpdates, priorLearnings, latest) {
41
43
  // the catch below swallows that into a silent skip. This early return just
42
44
  // avoids building the prompt for a call we already know will fail.
43
45
  const hasLLM = !!(process.env.BANKR_API_KEY || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY || process.env.GROK_API_KEY);
44
- if (!hasLLM && !process.env.FINCH_SESSION_TOKEN) {
46
+ if (!hasLLM) {
45
47
  return null;
46
48
  }
47
49
  // Build the user prompt with all the context the model needs.
@@ -133,6 +135,12 @@ exports.AGENT_TOOLS = [
133
135
  name: { type: "string", description: "Unique agent name (e.g. 'market-researcher', 'onboarding-helper')" },
134
136
  goal: { type: "string", description: "What this agent is trying to accomplish" },
135
137
  context: { type: "string", description: "Optional starting context, data, or notes for the agent" },
138
+ workspaceProject: {
139
+ type: "string",
140
+ description: "Optional: file this agent into a named Finch workspace project (visible on the Agents page's " +
141
+ "project switcher). Matched case-insensitively by name; created automatically if it doesn't exist " +
142
+ "yet. Hosted vault only (no effect in local-vault mode).",
143
+ },
136
144
  },
137
145
  required: ["name", "goal"],
138
146
  },
@@ -140,6 +148,8 @@ exports.AGENT_TOOLS = [
140
148
  {
141
149
  name: "agent_recall",
142
150
  description: "Recall a persistent agent by name - loads its goal, current progress, findings, full history, and accumulated learnings (patterns the agent extracted from past runs). " +
151
+ "Also pulls related context from memory/vault (code_session_save entries, deep_research reports, notes) matching the agent's goal, " +
152
+ "so recall reflects everything relevant to the goal - not just what agent_update explicitly logged. " +
143
153
  "Use this to resume a long-running task, check what an agent last did, or hand context to a fresh LLM session. " +
144
154
  "Learnings compound over time - the more an agent runs, the smarter recall becomes.",
145
155
  inputSchema: {
@@ -192,6 +202,7 @@ const SpawnAgentSchema = zod_1.z.object({
192
202
  name: zod_1.z.string().min(1).max(60).regex(/^[a-z0-9-]+$/, "name must be lowercase alphanumeric with hyphens"),
193
203
  goal: zod_1.z.string().min(1),
194
204
  context: zod_1.z.string().optional(),
205
+ workspaceProject: zod_1.z.string().optional(),
195
206
  });
196
207
  const RecallAgentSchema = zod_1.z.object({ name: zod_1.z.string().min(1) });
197
208
  const UpdateAgentSchema = zod_1.z.object({
@@ -217,8 +228,8 @@ async function handleAgentTool(name, args) {
217
228
  if (name === "agent_spawn") {
218
229
  const parsed = SpawnAgentSchema.safeParse(args);
219
230
  if (!parsed.success)
220
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
221
- const { name: agentName, goal, context } = parsed.data;
231
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
232
+ const { name: agentName, goal, context, workspaceProject } = parsed.data;
222
233
  const content = JSON.stringify({
223
234
  goal,
224
235
  status: "active",
@@ -237,19 +248,30 @@ async function handleAgentTool(name, args) {
237
248
  commitMsg: "spawned",
238
249
  };
239
250
  const localVault = (0, local_vault_js_1.getLocalVaultConfig)();
251
+ // Resolve the project name -> id server-side (auto-creates on first use).
252
+ // No-op in local-vault mode - there is no project concept there.
253
+ let resolvedProjectName = null;
254
+ if (workspaceProject && !localVault) {
255
+ const resolved = await (0, project_js_1.resolveProjectId)(workspaceProject);
256
+ if (resolved) {
257
+ savePayload.projectId = resolved.projectId;
258
+ resolvedProjectName = resolved.name;
259
+ }
260
+ }
240
261
  const data = localVault
241
262
  ? (0, local_vault_js_1.localVaultSave)(localVault, savePayload)
242
263
  : await (0, convex_js_1.callConvex)("/vault/save", "POST", savePayload, "vault_save");
243
264
  if (data.error)
244
265
  return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
266
+ const projectLine = resolvedProjectName ? `\n**Project:** ${resolvedProjectName}` : "";
245
267
  return {
246
- content: [{ type: "text", text: `🤖 Agent **${agentName}** spawned${localVault ? " locally" : ""}.\n\n**Goal:** ${goal}\n\nRecall with \`agent_recall\` · Update progress with \`agent_update\`` }],
268
+ content: [{ type: "text", text: `🤖 Agent **${agentName}** spawned${localVault ? " locally" : ""}.\n\n**Goal:** ${goal}${projectLine}\n\nRecall with \`agent_recall\` · Update progress with \`agent_update\`` }],
247
269
  };
248
270
  }
249
271
  if (name === "agent_recall") {
250
272
  const parsed = RecallAgentSchema.safeParse(args);
251
273
  if (!parsed.success)
252
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
274
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
253
275
  const localVault = (0, local_vault_js_1.getLocalVaultConfig)();
254
276
  let data;
255
277
  try {
@@ -291,12 +313,30 @@ async function handleAgentTool(name, args) {
291
313
  lines.push(`\n**Recent updates:**\n${updates.join("\n")}`);
292
314
  if (state.nextStep)
293
315
  lines.push(`\n**Next step:** ${state.nextStep}`);
316
+ // Related context - best-effort pull of relevant memory/vault knowledge
317
+ // (code_session_save entries, deep_research reports, manual notes) so the
318
+ // agent isn't blind to work done on its goal outside its own update log.
319
+ // This is what makes recall "continuously have context" rather than only
320
+ // ever knowing what agent_update explicitly logged.
321
+ if (state.goal) {
322
+ try {
323
+ const related = await (0, memory_js_1.hybridMemorySearch)(state.goal, 4);
324
+ if (related.length) {
325
+ lines.push(`\n**📎 Related context (${related.length}):**`);
326
+ related.forEach((r) => {
327
+ const title = r.metadata?.title ?? r.content.slice(0, 70).replace(/\n/g, " ");
328
+ lines.push(` • ${title}`);
329
+ });
330
+ }
331
+ }
332
+ catch { /* best-effort - recall must never fail because of this */ }
333
+ }
294
334
  return { content: [{ type: "text", text: lines.join("\n") }] };
295
335
  }
296
336
  if (name === "agent_update") {
297
337
  const parsed = UpdateAgentSchema.safeParse(args);
298
338
  if (!parsed.success)
299
- return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
339
+ return { content: [{ type: "text", text: `${parsed.error.issues[0].message}` }], isError: true };
300
340
  const { name: agentName, progress, findings, status = "active", nextStep } = parsed.data;
301
341
  return withAgentLock(agentName, async () => {
302
342
  const localVault = (0, local_vault_js_1.getLocalVaultConfig)();