@finchagentic/mcp 4.6.0 → 4.6.2

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 (58) hide show
  1. package/README.md +7 -7
  2. package/package.json +5 -6
  3. package/dist/_http-cache.js +0 -96
  4. package/dist/agent-loop.js +0 -301
  5. package/dist/annotations.js +0 -122
  6. package/dist/cli.js +0 -1391
  7. package/dist/clink-input.js +0 -15
  8. package/dist/config.js +0 -132
  9. package/dist/convex.js +0 -175
  10. package/dist/dex-pair.js +0 -54
  11. package/dist/enrichment-router.js +0 -315
  12. package/dist/index.js +0 -258
  13. package/dist/llm.js +0 -298
  14. package/dist/local-memory-file.js +0 -147
  15. package/dist/local-memory.js +0 -135
  16. package/dist/local-vault.js +0 -454
  17. package/dist/output-schemas.js +0 -605
  18. package/dist/project.js +0 -36
  19. package/dist/prompts.js +0 -111
  20. package/dist/public-url.js +0 -107
  21. package/dist/resources.js +0 -111
  22. package/dist/server.js +0 -322
  23. package/dist/signal-gate.js +0 -57
  24. package/dist/token-decimals.js +0 -26
  25. package/dist/token-gate.js +0 -88
  26. package/dist/tool-filter.js +0 -53
  27. package/dist/tools/_solidity-scan.js +0 -313
  28. package/dist/tools/agents.js +0 -441
  29. package/dist/tools/automation.js +0 -354
  30. package/dist/tools/base-mcp.js +0 -466
  31. package/dist/tools/base.js +0 -283
  32. package/dist/tools/chronicle.js +0 -268
  33. package/dist/tools/coder.js +0 -94
  34. package/dist/tools/deep-research.js +0 -1421
  35. package/dist/tools/defi.js +0 -292
  36. package/dist/tools/equity.js +0 -372
  37. package/dist/tools/events.js +0 -182
  38. package/dist/tools/github.js +0 -564
  39. package/dist/tools/insider.js +0 -264
  40. package/dist/tools/insight.js +0 -630
  41. package/dist/tools/market.js +0 -555
  42. package/dist/tools/memory.js +0 -1044
  43. package/dist/tools/miroshark.js +0 -350
  44. package/dist/tools/monitor.js +0 -319
  45. package/dist/tools/os.js +0 -236
  46. package/dist/tools/packets.js +0 -296
  47. package/dist/tools/research-chain.js +0 -226
  48. package/dist/tools/research-compare.js +0 -280
  49. package/dist/tools/research.js +0 -188
  50. package/dist/tools/rh-bridge.js +0 -148
  51. package/dist/tools/rh-mcp.js +0 -1448
  52. package/dist/tools/rh-orders.js +0 -556
  53. package/dist/tools/scanner.js +0 -564
  54. package/dist/tools/stake.js +0 -369
  55. package/dist/tools/vault.js +0 -1020
  56. package/dist/tools/wallet.js +0 -200
  57. package/dist/types.js +0 -2
  58. package/dist/wallet.js +0 -372
@@ -1,556 +0,0 @@
1
- "use strict";
2
- // Robinhood Chain automated orders — DCA / TP / SL (Phase 2).
3
- //
4
- // Self-contained order engine layered on the tested `rh_mcp_swap` path.
5
- // Orders persist locally at ~/.finch/rh-orders.json. Execution is opt-in:
6
- // `rh_orders_tick` previews by default and only trades when execute:true, and
7
- // never when the kill-switch is set (env RH_ORDERS_DISABLED=1 or the file
8
- // ~/.finch/rh-orders.OFF exists). DCA is bounded by a hard maxSpendEth;
9
- // TP/SL only ever SELL tokens the wallet already holds.
10
- //
11
- // An always-on scheduler must call `rh_orders_tick {execute:true}` periodically
12
- // (Convex cron via the CLI, Windows Task Scheduler, or the scheduled-tasks MCP).
13
- var __importDefault = (this && this.__importDefault) || function (mod) {
14
- return (mod && mod.__esModule) ? mod : { "default": mod };
15
- };
16
- Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.RH_ORDER_TOOLS = void 0;
18
- exports.buildOrdersList = buildOrdersList;
19
- exports.handleRhOrderTool = handleRhOrderTool;
20
- const fs_1 = __importDefault(require("fs"));
21
- const os_1 = __importDefault(require("os"));
22
- const path_1 = __importDefault(require("path"));
23
- const ethers_1 = require("ethers");
24
- const wallet_js_1 = require("../wallet.js");
25
- const rh_mcp_js_1 = require("./rh-mcp.js");
26
- const ORDERS_DIR = path_1.default.join(os_1.default.homedir(), ".finch");
27
- const ORDERS_FILE = path_1.default.join(ORDERS_DIR, "rh-orders.json");
28
- const KILL_FILE = path_1.default.join(ORDERS_DIR, "rh-orders.OFF");
29
- function textResult(text, isError = false) {
30
- return { content: [{ type: "text", text }], isError };
31
- }
32
- function loadStore() {
33
- try {
34
- const raw = fs_1.default.readFileSync(ORDERS_FILE, "utf8");
35
- const s = JSON.parse(raw);
36
- if (s && Array.isArray(s.orders))
37
- return s;
38
- }
39
- catch {
40
- /* fresh store */
41
- }
42
- return { version: 1, orders: [] };
43
- }
44
- function saveStore(s) {
45
- try {
46
- fs_1.default.mkdirSync(ORDERS_DIR, { recursive: true });
47
- }
48
- catch {
49
- /* ignore */
50
- }
51
- // Atomic write: a crash mid-write must not corrupt the order store.
52
- const tmp = `${ORDERS_FILE}.tmp`;
53
- fs_1.default.writeFileSync(tmp, JSON.stringify(s, null, 2));
54
- fs_1.default.renameSync(tmp, ORDERS_FILE);
55
- }
56
- function genId(prefix) {
57
- return `${prefix}_${Date.now().toString(36)}${Math.floor(Math.random() * 1e6).toString(36)}`;
58
- }
59
- // ── Tick lock ──────────────────────────────────────────────────────────────
60
- // rh_orders_tick reads the store, awaits a real on-chain swap per due order
61
- // (a slow network round trip), and only writes the store back at the end.
62
- // Without a lock, two overlapping executing ticks (a slow RPC making one call
63
- // outlast the next scheduler firing, or a manual call racing the scheduler)
64
- // both read the SAME pre-update state, both see the same order as due, and
65
- // both broadcast a real swap - real funds spent twice for what the store
66
- // ends up recording as one buy. Preview ticks (execute:false) never write and
67
- // are harmless to run concurrently, so only executing ticks take the lock.
68
- const TICK_LOCK_FILE = path_1.default.join(ORDERS_DIR, "rh-orders.tick.lock");
69
- const LOCK_STALE_MS = 5 * 60 * 1000; // a tick this slow is presumed crashed, not just slow
70
- function acquireTickLock() {
71
- try {
72
- fs_1.default.mkdirSync(ORDERS_DIR, { recursive: true });
73
- }
74
- catch {
75
- /* ignore */
76
- }
77
- try {
78
- // Exclusive create - atomically fails if another tick already holds it.
79
- fs_1.default.writeFileSync(TICK_LOCK_FILE, String(process.pid), { flag: "wx" });
80
- return true;
81
- }
82
- catch {
83
- try {
84
- const stat = fs_1.default.statSync(TICK_LOCK_FILE);
85
- if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
86
- // Prior holder almost certainly crashed without releasing - take over
87
- // rather than deadlocking every future tick forever.
88
- fs_1.default.writeFileSync(TICK_LOCK_FILE, String(process.pid));
89
- return true;
90
- }
91
- }
92
- catch {
93
- /* lock vanished between the failed create and this stat - try again next tick */
94
- }
95
- return false;
96
- }
97
- }
98
- function releaseTickLock() {
99
- try {
100
- fs_1.default.unlinkSync(TICK_LOCK_FILE);
101
- }
102
- catch {
103
- /* already gone */
104
- }
105
- }
106
- function killSwitchOn() {
107
- return process.env.RH_ORDERS_DISABLED === "1" || fs_1.default.existsSync(KILL_FILE);
108
- }
109
- function posNum(v) {
110
- const n = Number(v);
111
- return isFinite(n) ? n : NaN;
112
- }
113
- // Same bound quoteRh() enforces in rh-mcp.ts (matching defi.ts's Base-swap
114
- // bound of .positive().max(50)) - reject an out-of-range slippagePct at order
115
- // creation time rather than letting it silently reach an unattended
116
- // rh_orders_tick execution weeks later.
117
- function badSlippagePct(v) {
118
- if (v == null)
119
- return null;
120
- const n = posNum(v);
121
- if (!(n > 0) || n > 50)
122
- return `slippagePct must be greater than 0 and at most 50 (got ${v})`;
123
- return null;
124
- }
125
- function fmtUsd(n) {
126
- if (n == null || !isFinite(n))
127
- return "?";
128
- if (n >= 1)
129
- return "$" + n.toLocaleString(undefined, { maximumFractionDigits: 4 });
130
- return "$" + n.toPrecision(4);
131
- }
132
- function extractTxHash(text) {
133
- // Target the swap tx specifically — sells print "Permit2 approvals: 0x…" BEFORE
134
- // "Tx: 0x…", so a naive first-match would record the approval hash instead.
135
- const tx = text.match(/Tx:\s*`?(0x[a-fA-F0-9]{64})/i);
136
- if (tx)
137
- return tx[1];
138
- const any = text.match(/0x[a-fA-F0-9]{64}/);
139
- return any ? any[0] : null;
140
- }
141
- exports.RH_ORDER_TOOLS = [
142
- {
143
- name: "rh_dca_create",
144
- description: "Robinhood Chain — create a DCA plan: buy a fixed ETH amount of a token every N hours, " +
145
- "up to a total number of buys, capped by maxSpendEth. Token by catalog symbol, crypto " +
146
- "ticker, or 0x contract address (resolved via DexScreener). Does NOT execute now and does " +
147
- "NOT run itself automatically — this only saves the plan. It fires only when something calls " +
148
- "rh_orders_tick {execute:true}, which nothing does on its own. Tell the user to run " +
149
- "`finch orders install-scheduler` (or `finch orders daemon`) after creating this, or the " +
150
- "order will just sit there forever. Always confirm the resolved contract address first.",
151
- inputSchema: {
152
- type: "object",
153
- properties: {
154
- token: { type: "string", description: "Ticker or 0x contract address to accumulate" },
155
- amountEthPerBuy: { type: "string", description: "ETH spent per buy, e.g. '0.01'" },
156
- intervalHours: { type: "number", description: "Hours between buys (e.g. 24 for daily)" },
157
- totalBuys: { type: "number", description: "How many buys total" },
158
- maxSpendEth: {
159
- type: "string",
160
- description: "Hard cap on total ETH spent (default amountEthPerBuy × totalBuys)",
161
- },
162
- slippagePct: { type: "number", description: "Slippage % per buy (default 3.0)" },
163
- note: { type: "string", description: "Optional label" },
164
- },
165
- required: ["token", "amountEthPerBuy", "intervalHours", "totalBuys"],
166
- },
167
- },
168
- {
169
- name: "rh_bracket_create",
170
- description: "Robinhood Chain — set take-profit and/or stop-loss on a token you HOLD. When DexScreener " +
171
- "price crosses tpPriceUsd (≥) or slPriceUsd (≤), sellPct% of the current balance sells for " +
172
- "ETH — but only when something calls rh_orders_tick {execute:true}, which nothing does on " +
173
- "its own by default. Tell the user to run `finch orders install-scheduler` (or `finch orders " +
174
- "daemon`) after creating this, or the trigger will never actually fire. Provide at least one " +
175
- "of tpPriceUsd / slPriceUsd. Only ever sells tokens already in the wallet — never borrows or shorts.",
176
- inputSchema: {
177
- type: "object",
178
- properties: {
179
- token: { type: "string", description: "Ticker or 0x contract address you hold" },
180
- tpPriceUsd: { type: "number", description: "Take-profit price in USD (sell when ≥)" },
181
- slPriceUsd: { type: "number", description: "Stop-loss price in USD (sell when ≤)" },
182
- sellPct: { type: "number", description: "Percent of balance to sell on trigger (default 100)" },
183
- slippagePct: { type: "number", description: "Slippage % on the sell (default 5.0)" },
184
- note: { type: "string", description: "Optional label" },
185
- },
186
- required: ["token"],
187
- },
188
- },
189
- {
190
- name: "rh_orders_list",
191
- description: "Robinhood Chain — list saved DCA / TP / SL orders and their progress. Optional status filter.",
192
- inputSchema: {
193
- type: "object",
194
- properties: {
195
- status: {
196
- type: "string",
197
- description: "Filter: active | completed | cancelled | error (default all)",
198
- },
199
- },
200
- required: [],
201
- },
202
- },
203
- {
204
- name: "rh_order_cancel",
205
- description: "Robinhood Chain — cancel a saved order by id (stops future DCA buys / TP-SL fills).",
206
- inputSchema: {
207
- type: "object",
208
- properties: { id: { type: "string", description: "Order id (rho_… / rhb_…)" } },
209
- required: ["id"],
210
- },
211
- },
212
- {
213
- name: "rh_orders_tick",
214
- description: "Robinhood Chain — evaluate all active orders and act on any that are due (DCA interval " +
215
- "reached) or triggered (TP/SL price crossed). PREVIEW by default; pass execute:true to " +
216
- "broadcast real swaps. Refuses to execute when the kill-switch is set (RH_ORDERS_DISABLED=1 " +
217
- "or ~/.finch/rh-orders.OFF). Nothing calls this automatically — run `finch orders install-scheduler` " +
218
- "(OS-level recurring task) or `finch orders daemon` (foreground loop) from a terminal to make " +
219
- "orders actually fire unattended; calling this tool by hand only ticks once.",
220
- inputSchema: {
221
- type: "object",
222
- properties: {
223
- execute: {
224
- type: "boolean",
225
- description: "true = broadcast real swaps; false/omitted = dry-run preview only",
226
- },
227
- },
228
- required: [],
229
- },
230
- },
231
- ];
232
- // Structured output builder for rh_orders_list (schema in output-schemas.ts).
233
- function buildOrdersList(orders) {
234
- return {
235
- count: orders.length,
236
- orders: orders.map((o) => ({
237
- id: o.id,
238
- type: o.type ?? null,
239
- status: o.status ?? null,
240
- symbol: o.token?.symbol ?? null,
241
- address: o.token?.address ?? null,
242
- dca: o.dca ?? null,
243
- bracket: o.bracket ?? null,
244
- lastError: o.lastError ?? null,
245
- })),
246
- };
247
- }
248
- async function handleRhOrderTool(name, args) {
249
- const a = (args ?? {});
250
- switch (name) {
251
- case "rh_dca_create": {
252
- const amountEth = posNum(a.amountEthPerBuy);
253
- const intervalHours = posNum(a.intervalHours);
254
- const totalBuys = Math.floor(posNum(a.totalBuys));
255
- if (!(amountEth > 0))
256
- return textResult("amountEthPerBuy must be > 0", true);
257
- if (!(intervalHours > 0))
258
- return textResult("intervalHours must be > 0", true);
259
- if (!(totalBuys > 0))
260
- return textResult("totalBuys must be ≥ 1", true);
261
- const slippageErr = badSlippagePct(a.slippagePct);
262
- if (slippageErr)
263
- return textResult(slippageErr, true);
264
- let resolved;
265
- try {
266
- resolved = await (0, rh_mcp_js_1.resolveTokenSmart)(String(a.token));
267
- }
268
- catch (e) {
269
- return textResult(`Token resolve failed: ${e?.message ?? e}`, true);
270
- }
271
- if (resolved.kind !== "token")
272
- return textResult("token must be a stock/crypto, not ETH", true);
273
- const defaultCap = amountEth * totalBuys;
274
- const maxSpendEth = a.maxSpendEth != null ? posNum(a.maxSpendEth) : defaultCap;
275
- if (!(maxSpendEth > 0))
276
- return textResult("maxSpendEth must be > 0", true);
277
- if (maxSpendEth < amountEth) {
278
- return textResult(`maxSpendEth (${maxSpendEth}) is below amountEthPerBuy (${amountEth}) — no buy could ever fire.`, true);
279
- }
280
- const now = Date.now();
281
- const order = {
282
- id: genId("rho"),
283
- type: "dca",
284
- status: "active",
285
- token: { address: resolved.address, symbol: resolved.symbol, decimals: resolved.decimals },
286
- slippagePct: posNum(a.slippagePct) > 0 ? posNum(a.slippagePct) : 3.0,
287
- note: a.note ? String(a.note) : undefined,
288
- createdAt: now,
289
- updatedAt: now,
290
- dca: {
291
- amountEthPerBuy: String(amountEth),
292
- intervalSec: Math.round(intervalHours * 3600),
293
- totalBuys,
294
- completedBuys: 0,
295
- nextRunAt: now, // first buy on the next tick
296
- maxSpendEth: String(maxSpendEth),
297
- spentEth: "0",
298
- fills: [],
299
- },
300
- };
301
- const store = loadStore();
302
- store.orders.push(order);
303
- saveStore(store);
304
- return textResult([
305
- `## ✅ DCA created — ${order.id}`,
306
- `Buy **${amountEth} ETH → ${resolved.symbol}** every **${intervalHours}h**, **${totalBuys}×** (cap **${maxSpendEth} ETH**).`,
307
- `\`${resolved.address}\` · slippage ${order.slippagePct}%`,
308
- ``,
309
- `First buy fires on the next \`rh_orders_tick {execute:true}\`. Preview anytime with \`rh_orders_tick\`.`,
310
- `⚠️ Autonomous execution needs a scheduler calling the tick. Kill-switch: create \`~/.finch/rh-orders.OFF\`.`,
311
- ].join("\n"));
312
- }
313
- case "rh_bracket_create": {
314
- const hasTp = a.tpPriceUsd != null;
315
- const hasSl = a.slPriceUsd != null;
316
- if (!hasTp && !hasSl)
317
- return textResult("Provide at least one of tpPriceUsd / slPriceUsd", true);
318
- const tp = hasTp ? posNum(a.tpPriceUsd) : undefined;
319
- const sl = hasSl ? posNum(a.slPriceUsd) : undefined;
320
- if (hasTp && !(tp > 0))
321
- return textResult("tpPriceUsd must be > 0", true);
322
- if (hasSl && !(sl > 0))
323
- return textResult("slPriceUsd must be > 0", true);
324
- if (hasTp && hasSl && sl >= tp)
325
- return textResult("slPriceUsd must be below tpPriceUsd", true);
326
- const sellPct = a.sellPct != null ? posNum(a.sellPct) : 100;
327
- if (!(sellPct > 0 && sellPct <= 100))
328
- return textResult("sellPct must be 1..100", true);
329
- const bracketSlippageErr = badSlippagePct(a.slippagePct);
330
- if (bracketSlippageErr)
331
- return textResult(bracketSlippageErr, true);
332
- let resolved;
333
- try {
334
- resolved = await (0, rh_mcp_js_1.resolveTokenSmart)(String(a.token));
335
- }
336
- catch (e) {
337
- return textResult(`Token resolve failed: ${e?.message ?? e}`, true);
338
- }
339
- if (resolved.kind !== "token")
340
- return textResult("token must be a stock/crypto, not ETH", true);
341
- const entry = await (0, rh_mcp_js_1.rhPriceUsd)(resolved.address);
342
- const now = Date.now();
343
- const order = {
344
- id: genId("rhb"),
345
- type: "bracket",
346
- status: "active",
347
- token: { address: resolved.address, symbol: resolved.symbol, decimals: resolved.decimals },
348
- slippagePct: posNum(a.slippagePct) > 0 ? posNum(a.slippagePct) : 5.0,
349
- note: a.note ? String(a.note) : undefined,
350
- createdAt: now,
351
- updatedAt: now,
352
- bracket: { entryPriceUsd: entry, tpPriceUsd: tp, slPriceUsd: sl, sellPct, fills: [] },
353
- };
354
- const store = loadStore();
355
- store.orders.push(order);
356
- saveStore(store);
357
- return textResult([
358
- `## ✅ TP/SL created — ${order.id}`,
359
- `Token **${resolved.symbol}** \`${resolved.address}\``,
360
- `Now: ${fmtUsd(entry)}${tp != null ? ` · 🎯 TP ${fmtUsd(tp)}` : ""}${sl != null ? ` · 🛑 SL ${fmtUsd(sl)}` : ""}`,
361
- `Sells **${sellPct}%** of balance on trigger · slippage ${order.slippagePct}%`,
362
- ``,
363
- `Fires via \`rh_orders_tick {execute:true}\` when price crosses. Only sells tokens you already hold.`,
364
- ].join("\n"));
365
- }
366
- case "rh_orders_list": {
367
- const filter = a.status ? String(a.status) : null;
368
- const store = loadStore();
369
- const orders = filter ? store.orders.filter((o) => o.status === filter) : store.orders;
370
- if (!orders.length)
371
- return { content: [{ type: "text", text: `No orders${filter ? ` with status ${filter}` : ""}.` }], structuredContent: buildOrdersList(orders) };
372
- const lines = [`## 📋 RH orders (${orders.length})`, ""];
373
- for (const o of orders) {
374
- if (o.type === "dca" && o.dca) {
375
- lines.push(`**${o.id}** · DCA · ${o.status}`, ` ${o.dca.amountEthPerBuy} ETH → ${o.token.symbol} every ${(o.dca.intervalSec / 3600).toFixed(1)}h · ${o.dca.completedBuys}/${o.dca.totalBuys} done · spent ${o.dca.spentEth}/${o.dca.maxSpendEth} ETH`, ` next: ${o.status === "active" ? new Date(o.dca.nextRunAt).toISOString() : "—"}${o.lastError ? ` · ⚠️ ${o.lastError}` : ""}`);
376
- }
377
- else if (o.type === "bracket" && o.bracket) {
378
- const b = o.bracket;
379
- lines.push(`**${o.id}** · TP/SL · ${o.status}`, ` ${o.token.symbol}${b.tpPriceUsd != null ? ` · 🎯 ${fmtUsd(b.tpPriceUsd)}` : ""}${b.slPriceUsd != null ? ` · 🛑 ${fmtUsd(b.slPriceUsd)}` : ""} · sell ${b.sellPct}% · entry ${fmtUsd(b.entryPriceUsd)}`, ` fills: ${b.fills.length}${o.lastError ? ` · ⚠️ ${o.lastError}` : ""}`);
380
- }
381
- lines.push(` \`${o.token.address}\``);
382
- }
383
- return { content: [{ type: "text", text: lines.join("\n") }], structuredContent: buildOrdersList(orders) };
384
- }
385
- case "rh_order_cancel": {
386
- const id = String(a.id ?? "").trim();
387
- if (!id)
388
- return textResult("Required: id", true);
389
- const store = loadStore();
390
- const o = store.orders.find((x) => x.id === id);
391
- if (!o)
392
- return textResult(`Order ${id} not found.`, true);
393
- if (o.status === "active")
394
- o.status = "cancelled";
395
- o.updatedAt = Date.now();
396
- saveStore(store);
397
- return textResult(`🚫 Order ${id} cancelled.`);
398
- }
399
- case "rh_orders_tick": {
400
- const execute = a.execute === true;
401
- const blocked = execute && killSwitchOn();
402
- const doExecute = execute && !blocked;
403
- // Only an executing tick can double-spend; a preview never writes and
404
- // is harmless to overlap, so only take the lock when it matters.
405
- if (doExecute && !acquireTickLock()) {
406
- return textResult("⏳ Another rh_orders_tick is already executing - refusing to run a second one " +
407
- "concurrently (this is what prevents a slow tick from double-firing the same order). " +
408
- "Try again shortly.", true);
409
- }
410
- try {
411
- const store = loadStore();
412
- const active = store.orders.filter((o) => o.status === "active");
413
- const now = Date.now();
414
- const report = [
415
- `## ⏱️ RH orders tick — ${execute ? (blocked ? "EXECUTE (BLOCKED by kill-switch → preview)" : "EXECUTE") : "PREVIEW"}`,
416
- `${active.length} active order(s)`,
417
- "",
418
- ];
419
- let acted = 0;
420
- let wallet = null;
421
- for (const o of active) {
422
- try {
423
- // ---- DCA ----
424
- if (o.type === "dca" && o.dca) {
425
- const d = o.dca;
426
- if (d.completedBuys >= d.totalBuys) {
427
- o.status = "completed";
428
- continue;
429
- }
430
- if (now < d.nextRunAt) {
431
- report.push(`⏳ ${o.id} DCA ${o.token.symbol}: next buy at ${new Date(d.nextRunAt).toISOString()}`);
432
- continue;
433
- }
434
- const wouldSpend = Number(d.spentEth) + Number(d.amountEthPerBuy);
435
- if (wouldSpend > Number(d.maxSpendEth) + 1e-12) {
436
- o.status = "completed";
437
- report.push(`✅ ${o.id} DCA ${o.token.symbol}: maxSpend reached — completed`);
438
- continue;
439
- }
440
- acted++;
441
- if (!doExecute) {
442
- report.push(`🟡 ${o.id} DCA: would buy ${d.amountEthPerBuy} ETH → ${o.token.symbol} (${d.completedBuys + 1}/${d.totalBuys})`);
443
- continue;
444
- }
445
- const res = await (0, rh_mcp_js_1.handleRhMcpTool)("rh_mcp_swap", {
446
- fromToken: "ETH",
447
- toToken: o.token.address,
448
- amount: d.amountEthPerBuy,
449
- maxSlippagePct: o.slippagePct,
450
- confirm: true,
451
- });
452
- const txt = res?.content?.[0]?.text ?? "";
453
- if (res?.isError) {
454
- o.lastError = txt.slice(0, 140);
455
- o.updatedAt = now;
456
- report.push(`🔴 ${o.id} DCA buy failed: ${o.lastError}`);
457
- continue;
458
- }
459
- const txHash = extractTxHash(txt) ?? "unknown";
460
- d.fills.push({ ts: now, txHash, amountEth: d.amountEthPerBuy });
461
- d.completedBuys += 1;
462
- d.spentEth = String(Number(d.spentEth) + Number(d.amountEthPerBuy));
463
- d.nextRunAt = now + d.intervalSec * 1000; // no burst catch-up
464
- o.lastError = undefined;
465
- o.updatedAt = now;
466
- if (d.completedBuys >= d.totalBuys)
467
- o.status = "completed";
468
- report.push(`🟢 ${o.id} DCA bought ${d.amountEthPerBuy} ETH → ${o.token.symbol} · ${d.completedBuys}/${d.totalBuys} · tx \`${txHash}\``);
469
- continue;
470
- }
471
- // ---- Bracket (TP/SL) ----
472
- if (o.type === "bracket" && o.bracket) {
473
- const b = o.bracket;
474
- const price = await (0, rh_mcp_js_1.rhPriceUsd)(o.token.address);
475
- if (price == null) {
476
- report.push(`⚠️ ${o.id} TP/SL ${o.token.symbol}: no price available`);
477
- continue;
478
- }
479
- let trigger = null;
480
- if (b.tpPriceUsd != null && price >= b.tpPriceUsd)
481
- trigger = "tp";
482
- else if (b.slPriceUsd != null && price <= b.slPriceUsd)
483
- trigger = "sl";
484
- if (!trigger) {
485
- report.push(`⏳ ${o.id} TP/SL ${o.token.symbol}: ${fmtUsd(price)} (🎯${b.tpPriceUsd != null ? fmtUsd(b.tpPriceUsd) : "—"} / 🛑${b.slPriceUsd != null ? fmtUsd(b.slPriceUsd) : "—"})`);
486
- continue;
487
- }
488
- if (!wallet)
489
- wallet = await (0, wallet_js_1.getOrCreateWallet)();
490
- const balRaw = await (0, rh_mcp_js_1.rhErc20Balance)(o.token.address, wallet.address);
491
- if (balRaw <= 0n) {
492
- o.status = "completed";
493
- report.push(`✅ ${o.id} TP/SL ${o.token.symbol}: ${trigger.toUpperCase()} hit but 0 balance — completed`);
494
- continue;
495
- }
496
- const sellRaw = (balRaw * BigInt(Math.round(b.sellPct * 100))) / 10000n;
497
- const sellHuman = ethers_1.ethers.formatUnits(sellRaw, o.token.decimals);
498
- acted++;
499
- if (!doExecute) {
500
- report.push(`🟡 ${o.id} ${trigger.toUpperCase()} at ${fmtUsd(price)}: would sell ${b.sellPct}% (${sellHuman} ${o.token.symbol}) → ETH`);
501
- continue;
502
- }
503
- const res = await (0, rh_mcp_js_1.handleRhMcpTool)("rh_mcp_swap", {
504
- fromToken: o.token.address,
505
- toToken: "ETH",
506
- amount: sellHuman,
507
- maxSlippagePct: o.slippagePct,
508
- confirm: true,
509
- });
510
- const txt = res?.content?.[0]?.text ?? "";
511
- if (res?.isError) {
512
- o.lastError = txt.slice(0, 140);
513
- o.updatedAt = now;
514
- report.push(`🔴 ${o.id} ${trigger.toUpperCase()} sell failed: ${o.lastError}`);
515
- continue;
516
- }
517
- const txHash = extractTxHash(txt) ?? "unknown";
518
- b.fills.push({ ts: now, txHash, kind: trigger, priceUsd: price });
519
- o.status = "completed";
520
- o.lastError = undefined;
521
- o.updatedAt = now;
522
- report.push(`🟢 ${o.id} ${trigger.toUpperCase()} sold ${sellHuman} ${o.token.symbol} → ETH at ${fmtUsd(price)} · tx \`${txHash}\``);
523
- continue;
524
- }
525
- }
526
- catch (e) {
527
- // One bad order (e.g. wallet decrypt failure) must not abort the whole tick.
528
- o.lastError = String(e?.message ?? e).slice(0, 140);
529
- o.updatedAt = now;
530
- report.push(`🔴 ${o.id}: tick error — ${o.lastError}`);
531
- }
532
- }
533
- // Preview must be read-only: only persist state when actually executing.
534
- if (doExecute)
535
- saveStore(store);
536
- if (blocked) {
537
- report.push("", "🛑 Kill-switch active — no swaps broadcast. Remove ~/.finch/rh-orders.OFF or unset RH_ORDERS_DISABLED to enable.");
538
- }
539
- else if (acted === 0) {
540
- report.push("", "Nothing due or triggered this tick.");
541
- }
542
- else if (!doExecute) {
543
- report.push("", `${acted} action(s) pending. Re-run with \`execute:true\` (via scheduler) to broadcast.`);
544
- }
545
- report.push("", `_Explorer: ${rh_mcp_js_1.RH_EXPLORER}_`);
546
- return textResult(report.join("\n"));
547
- }
548
- finally {
549
- if (doExecute)
550
- releaseTickLock();
551
- }
552
- }
553
- default:
554
- return null;
555
- }
556
- }