@noelclaw/mcp 1.5.7 → 2.2.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.
@@ -0,0 +1,375 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SCANNER_TOOLS = void 0;
4
+ exports.handleScannerTool = handleScannerTool;
5
+ const zod_1 = require("zod");
6
+ // ── Constants ─────────────────────────────────────────────────────────────────
7
+ const DEFAULT_MIN_LIQ = 50000;
8
+ const DEFAULT_MIN_SCORE = 50;
9
+ // ── Dip-reversal scorer ───────────────────────────────────────────────────────
10
+ // Ported 1:1 from circuit-agent/lib/scoring.js (battle-tested, pure math).
11
+ function scoreDipReversal(c, minLiquidity = DEFAULT_MIN_LIQ) {
12
+ const pc5m = c.priceChange5m;
13
+ const pc1h = c.priceChange1h;
14
+ const pc6h = c.priceChange6h;
15
+ const pc24h = c.priceChange24h;
16
+ const liq = c.liquidity;
17
+ const vol1h = c.volume1h;
18
+ const totalTxns5m = c.buys5m + c.sells5m;
19
+ const buyRatio5m = totalTxns5m > 0 ? c.buys5m / totalTxns5m : 0;
20
+ const totalTxns1h = c.buys1h + c.sells1h;
21
+ const buyRatio1h = totalTxns1h > 0 ? c.buys1h / totalTxns1h : 0;
22
+ // ── Hard gates — all must pass ────────────────────────────────────────────
23
+ const gateFailures = [];
24
+ if (pc1h >= 0)
25
+ gateFailures.push(`1h not negative (${pc1h.toFixed(1)}%)`);
26
+ if (pc5m < 0.5)
27
+ gateFailures.push(`5m bounce weak (${pc5m.toFixed(1)}% < 0.5%)`);
28
+ if (totalTxns5m > 5 && buyRatio5m <= 0.50)
29
+ gateFailures.push(`buy ratio low (${(buyRatio5m * 100).toFixed(0)}%)`);
30
+ if (liq < minLiquidity)
31
+ gateFailures.push(`liquidity $${(liq / 1000).toFixed(0)}k < $${(minLiquidity / 1000).toFixed(0)}k min`);
32
+ if (pc6h <= -20 && pc24h <= -20)
33
+ gateFailures.push(`dead cat (6h ${pc6h.toFixed(0)}% / 24h ${pc24h.toFixed(0)}%)`);
34
+ if (gateFailures.length > 0) {
35
+ return { score: 0, passed: false, pattern: null, breakdown: {}, gateFailures, buyPressure5m: buyRatio5m * 100 };
36
+ }
37
+ // ── 1. Drop depth (0–25 pts) — deeper dip = more bounce room ─────────────
38
+ const dropPts = pc1h <= -10 ? 25 : pc1h <= -5 ? 20 : pc1h <= -3 ? 15 : 5;
39
+ // ── 2. Bounce confirmation (0–20 pts) ─────────────────────────────────────
40
+ const bouncePts = pc5m >= 5 ? 20 : pc5m >= 3 ? 17 : pc5m >= 2 ? 14 : pc5m >= 1 ? 10 : 5;
41
+ // ── 3. Sentiment shift (0–15 pts) — buyers returning after selloff ────────
42
+ const sentimentShift = buyRatio5m - buyRatio1h;
43
+ const sentPts = sentimentShift >= 0.10 ? 15
44
+ : sentimentShift >= 0.05 ? 10
45
+ : sentimentShift >= 0.02 ? 7
46
+ : sentimentShift > 0 ? 3 : 0;
47
+ // ── 4. Buy pressure (0–10 pts) ────────────────────────────────────────────
48
+ const bp = buyRatio5m * 100;
49
+ const bpPts = bp >= 65 ? 10 : bp >= 58 ? 8 : bp >= 53 ? 5 : 2;
50
+ // ── 5. Volume & activity (0–15 pts) — validates bounce is real ───────────
51
+ const actPts = vol1h >= 100000 && totalTxns1h >= 200 ? 15
52
+ : vol1h >= 50000 && totalTxns1h >= 100 ? 12
53
+ : vol1h >= 20000 && totalTxns1h >= 40 ? 8
54
+ : vol1h >= 5000 && totalTxns1h >= 10 ? 4 : 1;
55
+ // ── 6. Trend alignment (−10 to +15 pts) — dip in uptrend vs dead cat ─────
56
+ let trendPts;
57
+ if (pc6h > 0 && pc24h > 0)
58
+ trendPts = 15;
59
+ else if (pc24h > 0)
60
+ trendPts = 10;
61
+ else if (pc6h > 0)
62
+ trendPts = 5;
63
+ else {
64
+ const avg = (pc6h + pc24h) / 2;
65
+ trendPts = avg <= -15 ? -10 : avg <= -8 ? -7 : avg <= -4 ? -5 : -2;
66
+ }
67
+ const score = Math.max(0, Math.min(100, dropPts + bouncePts + sentPts + bpPts + actPts + trendPts));
68
+ const pattern = pc1h < -10 ? "DEEP-REVERSAL"
69
+ : pc1h < -5 ? "REVERSAL"
70
+ : pc1h < -3 ? "DIP-BUY"
71
+ : "SHALLOW-DIP";
72
+ return {
73
+ score,
74
+ passed: true,
75
+ pattern,
76
+ breakdown: {
77
+ dropDepth: { value: +pc1h.toFixed(1), points: dropPts },
78
+ bounce: { value: +pc5m.toFixed(1), points: bouncePts },
79
+ sentimentShift: { value: +sentimentShift.toFixed(2), points: sentPts },
80
+ buyPressure: { value: +bp.toFixed(0), points: bpPts },
81
+ activity: { vol1h, txns1h: totalTxns1h, points: actPts },
82
+ trendAlignment: { pc6h: +pc6h.toFixed(1), pc24h: +pc24h.toFixed(1), points: trendPts },
83
+ },
84
+ gateFailures: [],
85
+ buyPressure5m: bp,
86
+ };
87
+ }
88
+ // ── Fetch helper ──────────────────────────────────────────────────────────────
89
+ async function fetchJson(url) {
90
+ const res = await fetch(url, { signal: AbortSignal.timeout(15000) });
91
+ if (!res.ok)
92
+ throw new Error(`HTTP ${res.status} from ${new URL(url).hostname}`);
93
+ return res.json();
94
+ }
95
+ // ── Schemas ───────────────────────────────────────────────────────────────────
96
+ const AddressSchema = zod_1.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "must be a valid 0x address");
97
+ const ScoreTokenSchema = zod_1.z.object({ address: AddressSchema, minLiquidity: zod_1.z.number().positive().optional() });
98
+ const CheckTokenSchema = zod_1.z.object({ address: AddressSchema });
99
+ const ScanDipsSchema = zod_1.z.object({
100
+ minScore: zod_1.z.number().min(0).max(100).optional(),
101
+ minLiquidity: zod_1.z.number().positive().optional(),
102
+ limit: zod_1.z.number().int().min(1).max(100).optional(),
103
+ }).default({});
104
+ // ── Tool definitions ──────────────────────────────────────────────────────────
105
+ exports.SCANNER_TOOLS = [
106
+ {
107
+ name: "score_token",
108
+ description: "Run the 6-component dip-reversal score on any Base token. Returns a 0–100 score, pattern label (DEEP-REVERSAL / REVERSAL / DIP-BUY / SHALLOW-DIP), and full component breakdown. Data from DexScreener — no API key required.",
109
+ inputSchema: {
110
+ type: "object",
111
+ properties: {
112
+ address: { type: "string", description: "Token contract address on Base (0x…)" },
113
+ minLiquidity: { type: "number", description: "Minimum liquidity in USD (default 50000)" },
114
+ },
115
+ required: ["address"],
116
+ },
117
+ },
118
+ {
119
+ name: "check_token",
120
+ description: "Security audit a Base token: honeypot, rug risk score, mint authority, freeze authority, LP lock %, buy/sell tax, holder count. Powered by GoPlusLabs (free). Always run this before buying an unknown token.",
121
+ inputSchema: {
122
+ type: "object",
123
+ properties: {
124
+ address: { type: "string", description: "Token contract address on Base (0x…)" },
125
+ },
126
+ required: ["address"],
127
+ },
128
+ },
129
+ {
130
+ name: "scan_dips",
131
+ description: "Scan all trending + new Base pools right now for dip-reversal opportunities. Fetches GeckoTerminal data, runs the 6-component scorer on each pool, returns ranked candidates. Zero API keys required.",
132
+ inputSchema: {
133
+ type: "object",
134
+ properties: {
135
+ minScore: { type: "number", description: "Min score to include in results (default 50)" },
136
+ minLiquidity: { type: "number", description: "Min pool liquidity in USD (default 50000)" },
137
+ limit: { type: "number", description: "Max pools to scan (default 40, max 100)" },
138
+ },
139
+ required: [],
140
+ },
141
+ },
142
+ ];
143
+ // ── Handlers ──────────────────────────────────────────────────────────────────
144
+ async function handleScannerTool(name, args) {
145
+ // ── score_token ────────────────────────────────────────────────────────────
146
+ if (name === "score_token") {
147
+ const parsed = ScoreTokenSchema.safeParse(args);
148
+ if (!parsed.success)
149
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
150
+ const { address, minLiquidity = DEFAULT_MIN_LIQ } = parsed.data;
151
+ const data = await fetchJson(`https://api.dexscreener.com/latest/dex/tokens/${address}`);
152
+ const pair = (data.pairs ?? [])
153
+ .filter((p) => p.chainId === "base")
154
+ .sort((a, b) => (b.liquidity?.usd ?? 0) - (a.liquidity?.usd ?? 0))[0];
155
+ if (!pair) {
156
+ return { content: [{ type: "text", text: `No Base trading pair found for \`${address}\`. Check the address or try a different token.` }], isError: true };
157
+ }
158
+ const c = {
159
+ mint: address,
160
+ symbol: pair.baseToken?.symbol ?? address.slice(0, 8),
161
+ priceUsd: parseFloat(pair.priceUsd ?? "0"),
162
+ priceChange5m: pair.priceChange?.m5 ?? 0,
163
+ priceChange1h: pair.priceChange?.h1 ?? 0,
164
+ priceChange6h: pair.priceChange?.h6 ?? 0,
165
+ priceChange24h: pair.priceChange?.h24 ?? 0,
166
+ volume1h: pair.volume?.h1 ?? 0,
167
+ liquidity: pair.liquidity?.usd ?? 0,
168
+ buys5m: pair.txns?.m5?.buys ?? 0,
169
+ sells5m: pair.txns?.m5?.sells ?? 0,
170
+ buys1h: pair.txns?.h1?.buys ?? 0,
171
+ sells1h: pair.txns?.h1?.sells ?? 0,
172
+ };
173
+ const result = scoreDipReversal(c, minLiquidity);
174
+ const bd = result.breakdown;
175
+ const bar = "█".repeat(Math.round(result.score / 5)).padEnd(20, "░");
176
+ const lines = [
177
+ `## Dip-Reversal Score: ${c.symbol}`,
178
+ `\`${address}\``,
179
+ ``,
180
+ `**Score: ${result.score}/100** \`${bar}\``,
181
+ `**Pattern:** ${result.pattern ?? "—"}`,
182
+ `**Price:** $${c.priceUsd.toFixed(8).replace(/0+$/, "").replace(/\.$/, "")}`,
183
+ `**Liquidity:** $${(c.liquidity / 1000).toFixed(0)}k`,
184
+ ``,
185
+ ];
186
+ if (!result.passed) {
187
+ lines.push(`**Gates failed — not a valid dip-reversal setup:**`);
188
+ result.gateFailures.forEach(f => lines.push(`• ${f}`));
189
+ }
190
+ else {
191
+ lines.push(`**Breakdown:**`);
192
+ lines.push(`| Component | Signal | Points |`);
193
+ lines.push(`|-----------|--------|--------|`);
194
+ lines.push(`| Drop depth | ${bd.dropDepth?.value}% (1h) | ${bd.dropDepth?.points}/25 |`);
195
+ lines.push(`| Bounce | ${bd.bounce?.value}% (5m) | ${bd.bounce?.points}/20 |`);
196
+ lines.push(`| Sentiment shift | ${bd.sentimentShift?.value} ratio shift | ${bd.sentimentShift?.points}/15 |`);
197
+ lines.push(`| Buy pressure | ${bd.buyPressure?.value}% buyers (5m) | ${bd.buyPressure?.points}/10 |`);
198
+ lines.push(`| Activity | $${((bd.activity?.vol1h ?? 0) / 1000).toFixed(0)}k vol / ${bd.activity?.txns1h} txns | ${bd.activity?.points}/15 |`);
199
+ lines.push(`| Trend | 6h ${bd.trendAlignment?.pc6h}% / 24h ${bd.trendAlignment?.pc24h}% | ${(bd.trendAlignment?.points ?? 0) > 0 ? "+" : ""}${bd.trendAlignment?.points}/15 |`);
200
+ lines.push(``);
201
+ if (result.score >= 65)
202
+ lines.push(`**Strong setup.** Score ≥65 — high probability dip-reversal.`);
203
+ else if (result.score >= 50)
204
+ lines.push(`**Marginal.** Score 50–64 — look for additional confirmation before entry.`);
205
+ else
206
+ lines.push(`**Weak.** Score <50 — skip this setup.`);
207
+ lines.push(``, `Run \`check_token address="${address}"\` for a rug/security check before buying.`);
208
+ }
209
+ return { content: [{ type: "text", text: lines.join("\n") }] };
210
+ }
211
+ // ── check_token ────────────────────────────────────────────────────────────
212
+ if (name === "check_token") {
213
+ const parsed = CheckTokenSchema.safeParse(args);
214
+ if (!parsed.success)
215
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
216
+ const { address } = parsed.data;
217
+ // GoPlusLabs — chain 8453 = Base mainnet
218
+ const data = await fetchJson(`https://api.gopluslabs.io/api/v1/token_security/8453?contract_addresses=${address}`);
219
+ const info = data.result?.[address.toLowerCase()] ?? data.result?.[address] ?? {};
220
+ const isHoneypot = info.is_honeypot === "1";
221
+ const isMintable = info.is_mintable === "1";
222
+ const isFreezeAuth = info.transfer_pausable === "1";
223
+ const isOpenSource = info.is_open_source === "1";
224
+ // LP lock: sum of all locked LP holders
225
+ const lpLockedPct = (info.lp_holders ?? [])
226
+ .filter(h => h.is_locked)
227
+ .reduce((s, h) => s + parseFloat(h.percent ?? "0"), 0) * 100;
228
+ const buyTax = parseFloat(info.buy_tax ?? "0");
229
+ const sellTax = parseFloat(info.sell_tax ?? "0");
230
+ // Rug score: accumulate risk factors
231
+ let rugScore = 0;
232
+ if (isHoneypot)
233
+ rugScore += 50; // cannot sell → auto-danger
234
+ if (isMintable)
235
+ rugScore += 30; // unlimited supply risk
236
+ if (isFreezeAuth)
237
+ rugScore += 30; // transfers can be blocked
238
+ if (lpLockedPct < 50)
239
+ rugScore += 20; // LP can be pulled
240
+ if (sellTax > 10)
241
+ rugScore += 15; // high sell tax
242
+ rugScore = Math.min(100, rugScore);
243
+ const verdict = (isHoneypot || rugScore >= 60) ? "DANGER"
244
+ : rugScore >= 30 ? "CAUTION"
245
+ : "SAFE";
246
+ const icon = { DANGER: "🔴", CAUTION: "🟡", SAFE: "🟢" }[verdict];
247
+ const lines = [
248
+ `## Token Security Check`,
249
+ `\`${address}\``,
250
+ ``,
251
+ `**Verdict: ${icon} ${verdict}** (rug score: ${rugScore}/100)`,
252
+ ``,
253
+ `| Check | Status |`,
254
+ `|-------|--------|`,
255
+ `| Honeypot | ${isHoneypot ? "🔴 YES — cannot sell" : "🟢 No"} |`,
256
+ `| Mint authority | ${isMintable ? "🔴 Yes — supply can inflate" : "🟢 No"} |`,
257
+ `| Transfer freeze | ${isFreezeAuth ? "🔴 Yes — transfers can be paused" : "🟢 No"} |`,
258
+ `| Open source | ${isOpenSource ? "🟢 Yes" : "🔴 No — unverified contract"} |`,
259
+ `| LP locked | ${lpLockedPct >= 80 ? "🟢" : lpLockedPct >= 50 ? "🟡" : "🔴"} ${lpLockedPct.toFixed(1)}% |`,
260
+ `| Buy tax | ${buyTax > 5 ? "🟡" : "🟢"} ${buyTax}% |`,
261
+ `| Sell tax | ${sellTax > 10 ? "🔴" : sellTax > 5 ? "🟡" : "🟢"} ${sellTax}% |`,
262
+ `| Holder count | ${info.holder_count ?? "unknown"} |`,
263
+ ``,
264
+ ];
265
+ if (verdict === "DANGER") {
266
+ lines.push(`**Do not buy.** This token has critical red flags. High risk of total loss.`);
267
+ }
268
+ else if (verdict === "CAUTION") {
269
+ lines.push(`**Trade carefully.** Elevated risk — verify team, community, and LP lock before buying. Keep position size small.`);
270
+ }
271
+ else {
272
+ lines.push(`**Passes basic security checks.** No critical red flags found. Always DYOR — security checks are not a guarantee.`);
273
+ }
274
+ return { content: [{ type: "text", text: lines.join("\n") }] };
275
+ }
276
+ // ── scan_dips ──────────────────────────────────────────────────────────────
277
+ if (name === "scan_dips") {
278
+ const parsed = ScanDipsSchema.safeParse(args ?? {});
279
+ if (!parsed.success)
280
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
281
+ const { minScore = DEFAULT_MIN_SCORE, minLiquidity = DEFAULT_MIN_LIQ, limit = 40 } = parsed.data;
282
+ // Fetch trending + new pools on Base from GeckoTerminal in parallel
283
+ const [trendingRes, newPoolsRes] = await Promise.allSettled([
284
+ fetchJson("https://api.geckoterminal.com/api/v2/networks/base/trending_pools?page=1"),
285
+ fetchJson("https://api.geckoterminal.com/api/v2/networks/base/new_pools?page=1"),
286
+ ]);
287
+ const rawPools = [
288
+ ...(trendingRes.status === "fulfilled" ? trendingRes.value.data ?? [] : []),
289
+ ...(newPoolsRes.status === "fulfilled" ? newPoolsRes.value.data ?? [] : []),
290
+ ];
291
+ if (!rawPools.length) {
292
+ return { content: [{ type: "text", text: "GeckoTerminal returned no pools. Try again in a moment." }], isError: true };
293
+ }
294
+ // Deduplicate by pool id
295
+ const seen = new Set();
296
+ const pools = rawPools.filter(p => {
297
+ if (!p?.attributes || !p.id)
298
+ return false;
299
+ if (seen.has(p.id))
300
+ return false;
301
+ seen.add(p.id);
302
+ return true;
303
+ });
304
+ // Map GeckoTerminal pool → Candidate
305
+ const candidates = pools
306
+ .map((p) => {
307
+ const a = p.attributes ?? {};
308
+ const txns = a.transactions ?? {};
309
+ const pc = a.price_change_percentage ?? {};
310
+ const vol = a.volume_usd ?? {};
311
+ const liq = parseFloat(a.reserve_in_usd ?? "0");
312
+ const tokenRel = p.relationships?.base_token?.data?.id ?? "";
313
+ const mint = tokenRel.includes("_") ? tokenRel.split("_")[1] : tokenRel;
314
+ if (!mint?.startsWith("0x"))
315
+ return null;
316
+ return {
317
+ mint,
318
+ symbol: (a.name ?? "").split(" / ")[0] || mint.slice(0, 8),
319
+ priceUsd: parseFloat(a.base_token_price_usd ?? "0"),
320
+ priceChange5m: parseFloat(pc.m5 ?? "0"),
321
+ priceChange1h: parseFloat(pc.h1 ?? "0"),
322
+ priceChange6h: parseFloat(pc.h6 ?? "0"),
323
+ priceChange24h: parseFloat(pc.h24 ?? "0"),
324
+ volume1h: parseFloat(vol.h1 ?? "0"),
325
+ liquidity: liq,
326
+ buys5m: txns.m5?.buys ?? 0,
327
+ sells5m: txns.m5?.sells ?? 0,
328
+ buys1h: txns.h1?.buys ?? 0,
329
+ sells1h: txns.h1?.sells ?? 0,
330
+ };
331
+ })
332
+ .filter((c) => c !== null && c.liquidity >= minLiquidity)
333
+ .slice(0, limit);
334
+ const scored = candidates
335
+ .map(c => ({ ...c, ...scoreDipReversal(c, minLiquidity) }))
336
+ .filter(c => c.passed && c.score >= minScore)
337
+ .sort((a, b) => b.score - a.score);
338
+ if (!scored.length) {
339
+ return {
340
+ content: [{
341
+ type: "text",
342
+ text: [
343
+ `## Dip Scan — No Setups Found`,
344
+ ``,
345
+ `Scanned **${candidates.length} pools** on Base. None passed the dip-reversal gates with score ≥ ${minScore}.`,
346
+ ``,
347
+ `**This is a signal in itself** — when nothing scores, the market is likely not in dip-reversal mode.`,
348
+ ``,
349
+ `Try: \`scan_dips minScore=30\` to see weak setups, or check back in 5–10 minutes.`,
350
+ ].join("\n"),
351
+ }],
352
+ };
353
+ }
354
+ const lines = [
355
+ `## Dip Scan — ${scored.length} Setup${scored.length !== 1 ? "s" : ""} Found`,
356
+ `Scanned **${candidates.length} pools** · Score ≥ ${minScore} · Liq ≥ $${(minLiquidity / 1000).toFixed(0)}k`,
357
+ ``,
358
+ ];
359
+ for (const c of scored.slice(0, 10)) {
360
+ const bar = "█".repeat(Math.round(c.score / 10)).padEnd(10, "░");
361
+ const bd = c.breakdown;
362
+ const sign = (n) => n >= 0 ? `+${n.toFixed(1)}` : n.toFixed(1);
363
+ lines.push(`### ${c.symbol} · ${c.score}/100 \`${bar}\``);
364
+ lines.push(`**Pattern:** ${c.pattern} · **Liq:** $${(c.liquidity / 1000).toFixed(0)}k · **1h:** ${sign(c.priceChange1h)}% · **5m:** ${sign(c.priceChange5m)}%`);
365
+ lines.push(`**Buy pressure:** ${c.buyPressure5m.toFixed(0)}% · **Vol 1h:** $${((bd.activity?.vol1h ?? 0) / 1000).toFixed(0)}k · **Txns:** ${bd.activity?.txns1h ?? 0}`);
366
+ lines.push(`**Trend:** 6h ${sign(c.priceChange6h)}% / 24h ${sign(c.priceChange24h)}%`);
367
+ lines.push(`\`${c.mint}\``);
368
+ lines.push(``);
369
+ }
370
+ lines.push(`---`);
371
+ lines.push(`Next steps: \`score_token\` for full breakdown · \`check_token\` for rug check before buying`);
372
+ return { content: [{ type: "text", text: lines.join("\n") }] };
373
+ }
374
+ return null;
375
+ }
@@ -5,6 +5,9 @@ exports.handleSwarmTool = handleSwarmTool;
5
5
  const zod_1 = require("zod");
6
6
  const convex_js_1 = require("../convex.js");
7
7
  const market_js_1 = require("./market.js");
8
+ function formatDate(ts) {
9
+ return new Date(ts).toUTCString();
10
+ }
8
11
  exports.SWARM_TOOLS = [
9
12
  {
10
13
  name: "start_swarm",
@@ -62,6 +65,57 @@ exports.SWARM_TOOLS = [
62
65
  description: "Get the self-improvement scores for all skills.",
63
66
  inputSchema: { type: "object", properties: {}, required: [] },
64
67
  },
68
+ {
69
+ name: "swarm_research",
70
+ description: "Task the swarm to research a topic right now — triggers market-monitor and sentiment-tracker immediately, " +
71
+ "and saves all findings to your vault as persistent research entries. " +
72
+ "Unlike regular swarm runs (which only write to ephemeral memory), swarm_research always persists to vault. " +
73
+ "Use this when you want the swarm to build up your knowledge base on a specific topic.",
74
+ inputSchema: {
75
+ type: "object",
76
+ properties: {
77
+ topic: { type: "string", description: "What to research — token name, project, narrative, or market question" },
78
+ depth: { type: "string", enum: ["quick", "standard", "deep"], description: "Research depth (default: standard)" },
79
+ },
80
+ required: ["topic"],
81
+ },
82
+ },
83
+ {
84
+ name: "trigger_agent",
85
+ description: "Manually trigger a single swarm agent to run right now — without starting the full swarm. " +
86
+ "Costs 100 credits per call (same as automatic runs). " +
87
+ "market-monitor fetches live price data, sentiment-tracker analyzes sentiment, " +
88
+ "memory-manager compresses ephemeral memory, risk-verifier evaluates an action for risk. " +
89
+ "Results are saved to vault automatically.",
90
+ inputSchema: {
91
+ type: "object",
92
+ properties: {
93
+ agentId: {
94
+ type: "string",
95
+ enum: ["market-monitor", "sentiment-tracker", "memory-manager", "risk-verifier", "workflow-executor"],
96
+ description: "Which agent to run",
97
+ },
98
+ params: {
99
+ type: "object",
100
+ description: "Agent-specific params. market-monitor: { token: 'BTC' }. sentiment-tracker: { token: 'ETH' } or { topic: 'Layer 2s' }. risk-verifier: { type: 'swap', params: { ... } }.",
101
+ },
102
+ },
103
+ required: ["agentId"],
104
+ },
105
+ },
106
+ {
107
+ name: "swarm_brief",
108
+ description: "Get a summary of everything the swarm has researched and saved to your vault. " +
109
+ "Shows the latest research entries written by swarm agents across all sessions. " +
110
+ "Use this to catch up on what the swarm found while you were away.",
111
+ inputSchema: {
112
+ type: "object",
113
+ properties: {
114
+ limit: { type: "number", description: "Max entries to return (default 10)" },
115
+ },
116
+ required: [],
117
+ },
118
+ },
65
119
  ];
66
120
  const StartSwarmSchema = zod_1.z.object({
67
121
  config: zod_1.z.object({
@@ -71,6 +125,12 @@ const StartSwarmSchema = zod_1.z.object({
71
125
  });
72
126
  const WriteMemorySchema = zod_1.z.object({ agentId: zod_1.z.string().min(1), key: zod_1.z.string().min(1), value: zod_1.z.string(), ttlSeconds: zod_1.z.number().optional() });
73
127
  const GetMemorySchema = zod_1.z.object({ key: zod_1.z.string().min(1) });
128
+ const ResearchSchema = zod_1.z.object({ topic: zod_1.z.string().min(1), depth: zod_1.z.enum(["quick", "standard", "deep"]).optional() });
129
+ const BriefSchema = zod_1.z.object({ limit: zod_1.z.number().optional() });
130
+ const TriggerAgentSchema = zod_1.z.object({
131
+ agentId: zod_1.z.enum(["market-monitor", "sentiment-tracker", "memory-manager", "risk-verifier", "workflow-executor"]),
132
+ params: zod_1.z.record(zod_1.z.string(), zod_1.z.any()).optional(),
133
+ });
74
134
  async function handleSwarmTool(name, args) {
75
135
  switch (name) {
76
136
  case "start_swarm": {
@@ -178,6 +238,73 @@ async function handleSwarmTool(name, args) {
178
238
  ];
179
239
  return { content: [{ type: "text", text: lines.join("\n") }] };
180
240
  }
241
+ case "swarm_research": {
242
+ const parsed = ResearchSchema.safeParse(args);
243
+ if (!parsed.success)
244
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
245
+ const { topic, depth = "standard" } = parsed.data;
246
+ const data = await (0, convex_js_1.callConvex)("/swarm/research", "POST", { topic, depth }, "swarm_research");
247
+ if (data.error)
248
+ return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
249
+ return {
250
+ content: [{
251
+ type: "text",
252
+ text: [
253
+ `🔬 **Swarm Research Started**`,
254
+ `Topic: ${topic}`,
255
+ `Depth: ${depth}`,
256
+ ``,
257
+ data.message ?? "Research triggered. Findings will appear in vault.",
258
+ ``,
259
+ `Use \`vault_context topic: "${topic}"\` to retrieve results once complete.`,
260
+ ].join("\n"),
261
+ }],
262
+ };
263
+ }
264
+ case "trigger_agent": {
265
+ const parsed = TriggerAgentSchema.safeParse(args);
266
+ if (!parsed.success)
267
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
268
+ const { agentId, params = {} } = parsed.data;
269
+ const data = await (0, convex_js_1.callConvex)("/swarm/trigger", "POST", { agentId, params }, "trigger_agent");
270
+ if (data.error)
271
+ return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
272
+ const resultText = data.result ? `\n\`\`\`json\n${JSON.stringify(data.result, null, 2).slice(0, 800)}\n\`\`\`` : "";
273
+ return {
274
+ content: [{
275
+ type: "text",
276
+ text: [
277
+ `⚡ **${agentId} triggered**`,
278
+ `100 credits charged.`,
279
+ resultText,
280
+ ``,
281
+ `Use \`vault_context topic: "${agentId.replace("-", " ")}"\` to retrieve findings.`,
282
+ ].filter(Boolean).join("\n"),
283
+ }],
284
+ };
285
+ }
286
+ case "swarm_brief": {
287
+ const parsed = BriefSchema.safeParse(args ?? {});
288
+ if (!parsed.success)
289
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
290
+ const limit = parsed.data.limit ?? 10;
291
+ // Pull recent research entries written by swarm agents from vault
292
+ const params = new URLSearchParams({ type: "research", limit: String(limit) });
293
+ const data = await (0, convex_js_1.callConvex)(`/vault/list?${params}`, "GET", undefined, "swarm_brief");
294
+ if (data.error)
295
+ return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
296
+ const entries = (data.entries ?? []).filter((e) => e.agentId === "market-monitor" || e.agentId === "sentiment-tracker");
297
+ if (!entries.length) {
298
+ return { content: [{ type: "text", text: `No swarm research in vault yet.\nStart the swarm with \`start_swarm\` or run \`swarm_research topic: "BTC"\` to build your knowledge base.` }] };
299
+ }
300
+ const lines = [`📋 **Swarm Brief** — ${entries.length} research entries in vault\n`];
301
+ for (const e of entries) {
302
+ lines.push(`**[${e.agentId}]** ${e.title}`);
303
+ lines.push(` _${e.key}_ · v${e.version} · ${formatDate(e.updatedAt)}`);
304
+ }
305
+ lines.push(`\nUse \`vault_context topic: "<topic>"\` to load full content for any research area.`);
306
+ return { content: [{ type: "text", text: lines.join("\n") }] };
307
+ }
181
308
  default:
182
309
  return null;
183
310
  }