@noelclaw/mcp 2.3.0 → 2.4.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/dist/index.js CHANGED
@@ -33,25 +33,26 @@ async function main() {
33
33
  process.stderr.write(BANNER);
34
34
  // ── Tool category counts ──────────────────────────────────────────────────
35
35
  const categories = [
36
- { label: "Market", count: 3, tools: "get_market_data · get_token_data · ask_noel" },
37
- { label: "DeFi", count: 5, tools: "portfolio · swap · send · scan_wallet · estimate" },
36
+ { label: "Market", count: 5, tools: "get_market_data · get_token_data · compare_tokens · market_overview · token_history" },
37
+ { label: "Insight", count: 3, tools: "ask_noel · market_thesis · trade_plan" },
38
+ { label: "DeFi", count: 6, tools: "portfolio · swap · send · scan_wallet · estimate · get_defi_yields" },
38
39
  { label: "Automation", count: 5, tools: "create · list · pause · delete · runs" },
39
- { label: "Scanner", count: 3, tools: "scan_dips · score_token · check_token" },
40
+ { label: "Scanner", count: 4, tools: "scan_dips · scan_momentum · score_token · check_token" },
40
41
  { label: "Agents", count: 2, tools: "list_agents · hire_agent" },
41
- { label: "Swarm", count: 6, tools: "start · stop · status · read_memory · write_memory · scores" },
42
+ { label: "Swarm", count: 11, tools: "start · stop · status · memory · scores · research · trigger · brief · broadcast · pulse" },
42
43
  { label: "Framework", count: 6, tools: "create_task · list_tasks · list_playbooks · run_playbook · ledger · sentinel" },
43
- { label: "Vault", count: 8, tools: "save · read · list · search · history · diff · export · connect" },
44
- { label: "Memory", count: 5, tools: "add · search · context · profile · connect" },
44
+ { label: "Vault", count: 18, tools: "save · read · list · search · history · diff · export · remember · context · credential · publish · explore · connect · pin · delete · link · tag" },
45
+ { label: "Memory", count: 8, tools: "add · search · context · profile · connect · list · delete · update" },
45
46
  { label: "MiroShark", count: 3, tools: "simulate · status · stop" },
46
47
  { label: "Wallet", count: 2, tools: "get_wallet_address · set_telegram" },
47
- { label: "Social", count: 1, tools: "humanize_text" },
48
- { label: "Coder", count: 6, tools: "scaffold_project · generate_component · generate_contract · audit_contract · explain_code · review_code" },
48
+ { label: "Social", count: 3, tools: "humanize_text · write_thread · write_post" },
49
+ { label: "Coder", count: 7, tools: "scaffold_project · generate_component · generate_contract · audit_contract · explain_code · review_code · generate_mcp_skill" },
49
50
  { label: "Base", count: 4, tools: "query_vaults · list_markets · prepare_deposit · chain_stats" },
50
51
  ];
51
52
  const total = server_js_1.ALL_TOOLS.length;
52
53
  divider();
53
54
  process.stderr.write(`\n`);
54
- line("version", `v2.3.0 ${C.dim}MCP protocol 2.1.0${C.reset}`);
55
+ line("version", `v2.4.0 ${C.dim}MCP protocol 2.1.0${C.reset}`);
55
56
  line("network", `Base mainnet ${C.dim}via 0x Protocol · ethers v6${C.reset}`);
56
57
  line("ai", `Bankr LLM ${C.dim}grok-3 · llm.bankr.bot${C.reset}`);
57
58
  line("tools", `${C.white}${C.bold}${total} tools loaded${C.reset} ${C.dim}across ${categories.length} categories${C.reset}`);
package/dist/server.js CHANGED
@@ -21,6 +21,7 @@ const scanner_js_1 = require("./tools/scanner.js");
21
21
  const coder_js_1 = require("./tools/coder.js");
22
22
  const base_js_1 = require("./tools/base.js");
23
23
  const memory_js_1 = require("./tools/memory.js");
24
+ const os_js_1 = require("./tools/os.js");
24
25
  const PRIVATE_KEY_RESPONSE = {
25
26
  content: [{
26
27
  type: "text",
@@ -35,45 +36,54 @@ function containsSensitiveRequest(args) {
35
36
  text.includes("privatekey"));
36
37
  }
37
38
  exports.ALL_TOOLS = [
38
- ...market_js_1.MARKET_TOOLS, // 2 — get_market_data, get_token_data
39
- ...insight_js_1.INSIGHT_TOOLS, // 1 — ask_noel
40
- ...defi_js_1.DEFI_TOOLS, // 5 — get_portfolio, estimate_swap, swap_tokens, send_token, scan_wallet
41
- ...automation_js_1.AUTOMATION_TOOLS, // 5 — create, list, pause, delete, get_runs
42
- ...swarm_js_1.SWARM_TOOLS, // 9 — start, stop, status, read/write memory, scores, research, brief, trigger_agent
39
+ ...market_js_1.MARKET_TOOLS, // 5 — get_market_data, get_token_data, compare_tokens, market_overview, token_history
40
+ ...insight_js_1.INSIGHT_TOOLS, // 3 — ask_noel, market_thesis, trade_plan
41
+ ...defi_js_1.DEFI_TOOLS, // 7 — get_portfolio, estimate_swap, swap_tokens, send_token, scan_wallet, analyze_wallet, get_defi_yields
42
+ ...automation_js_1.AUTOMATION_TOOLS, // 6 — create, list, pause, delete, get_runs, run
43
+ ...swarm_js_1.SWARM_TOOLS, // 13 — start, stop, status, read/write memory, scores, research, brief, trigger_agent, broadcast, pulse, reflect, watch
43
44
  ...framework_js_1.FRAMEWORK_TOOLS, // 6 — task packets, playbooks, sentinel, ledger
44
- ...vault_js_1.VAULT_TOOLS, // 14 — save, read, list, search, history, diff, export, remember, context, store_credential, get_credential, publish, explore, connect
45
+ ...vault_js_1.VAULT_TOOLS, // 15 — save, read, list, search, history, diff, export, store_credential, get_credential, publish, explore, pin, delete, link, tag
45
46
  ...wallet_js_1.WALLET_TOOLS, // 2 — get_wallet_address, set_telegram
46
47
  ...miroshark_js_1.MIROSHARK_TOOLS, // 3 — simulate, status, stop
47
- ...humanizer_js_1.HUMANIZER_TOOLS, // 1 — humanize_text
48
+ ...humanizer_js_1.HUMANIZER_TOOLS, // 3 — humanize_text, write_thread, write_post
48
49
  ...agents_js_1.AGENT_TOOLS, // 2 — list_agents, hire_agent
49
- ...scanner_js_1.SCANNER_TOOLS, // 3 — score_token, check_token, scan_dips
50
- ...coder_js_1.CODER_TOOLS, // 6 — scaffold_project, generate_component, generate_contract, audit_contract, explain_code, review_code
50
+ ...scanner_js_1.SCANNER_TOOLS, // 4 — score_token, check_token, scan_dips, scan_momentum
51
+ ...coder_js_1.CODER_TOOLS, // 7 — scaffold_project, generate_component, generate_contract, audit_contract, explain_code, review_code, generate_mcp_skill
51
52
  ...base_js_1.BASE_TOOLS, // 4 — query_vaults, list_markets, prepare_deposit, chain_stats
52
- ...memory_js_1.MEMORY_TOOLS, // 5 — memory_add, memory_search, memory_context, memory_profile, memory_connect
53
- // total: 67
53
+ ...memory_js_1.MEMORY_TOOLS, // 7 — memory_add, memory_search, memory_context, memory_profile, memory_list, memory_delete, memory_insight
54
+ ...os_js_1.OS_TOOLS, // 3 — noel_status, noel_boot, noel_shutdown
55
+ // total: 90
54
56
  ];
55
- exports.server = new index_js_1.Server({ name: "noelclaw", version: "2.3.0" }, { capabilities: { tools: {} } });
57
+ const HANDLER_MAP = new Map([
58
+ ...market_js_1.MARKET_TOOLS.map(t => [t.name, market_js_1.handleMarketTool]),
59
+ ...defi_js_1.DEFI_TOOLS.map(t => [t.name, defi_js_1.handleDefiTool]),
60
+ ...automation_js_1.AUTOMATION_TOOLS.map(t => [t.name, automation_js_1.handleAutomationTool]),
61
+ ...swarm_js_1.SWARM_TOOLS.map(t => [t.name, swarm_js_1.handleSwarmTool]),
62
+ ...framework_js_1.FRAMEWORK_TOOLS.map(t => [t.name, framework_js_1.handleFrameworkTool]),
63
+ ...vault_js_1.VAULT_TOOLS.map(t => [t.name, vault_js_1.handleVaultTool]),
64
+ ...wallet_js_1.WALLET_TOOLS.map(t => [t.name, wallet_js_1.handleWalletTool]),
65
+ ...insight_js_1.INSIGHT_TOOLS.map(t => [t.name, insight_js_1.handleInsightTool]),
66
+ ...miroshark_js_1.MIROSHARK_TOOLS.map(t => [t.name, miroshark_js_1.handleMirosharkTool]),
67
+ ...humanizer_js_1.HUMANIZER_TOOLS.map(t => [t.name, humanizer_js_1.handleHumanizerTool]),
68
+ ...agents_js_1.AGENT_TOOLS.map(t => [t.name, agents_js_1.handleAgentTool]),
69
+ ...scanner_js_1.SCANNER_TOOLS.map(t => [t.name, scanner_js_1.handleScannerTool]),
70
+ ...coder_js_1.CODER_TOOLS.map(t => [t.name, coder_js_1.handleCoderTool]),
71
+ ...base_js_1.BASE_TOOLS.map(t => [t.name, base_js_1.handleBaseTool]),
72
+ ...memory_js_1.MEMORY_TOOLS.map(t => [t.name, memory_js_1.handleMemoryTool]),
73
+ ...os_js_1.OS_TOOLS.map(t => [t.name, os_js_1.handleOsTool]),
74
+ ]);
75
+ exports.server = new index_js_1.Server({ name: "noelclaw", version: "2.4.0" }, { capabilities: { tools: {} } });
56
76
  exports.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({ tools: exports.ALL_TOOLS }));
57
77
  exports.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
58
78
  const { name, arguments: args } = request.params;
59
79
  if (containsSensitiveRequest(args))
60
80
  return PRIVATE_KEY_RESPONSE;
81
+ const handler = HANDLER_MAP.get(name);
82
+ if (!handler) {
83
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
84
+ }
61
85
  try {
62
- const result = await (0, market_js_1.handleMarketTool)(name, args) ??
63
- await (0, defi_js_1.handleDefiTool)(name, args) ??
64
- await (0, automation_js_1.handleAutomationTool)(name, args) ??
65
- await (0, swarm_js_1.handleSwarmTool)(name, args) ??
66
- await (0, framework_js_1.handleFrameworkTool)(name, args) ??
67
- await (0, vault_js_1.handleVaultTool)(name, args) ??
68
- await (0, wallet_js_1.handleWalletTool)(name, args) ??
69
- await (0, insight_js_1.handleInsightTool)(name, args) ??
70
- await (0, miroshark_js_1.handleMirosharkTool)(name, args) ??
71
- await (0, humanizer_js_1.handleHumanizerTool)(name, args) ??
72
- await (0, agents_js_1.handleAgentTool)(name, args) ??
73
- await (0, scanner_js_1.handleScannerTool)(name, args) ??
74
- await (0, coder_js_1.handleCoderTool)(name, args) ??
75
- await (0, base_js_1.handleBaseTool)(name, args) ??
76
- await (0, memory_js_1.handleMemoryTool)(name, args);
86
+ const result = await handler(name, args);
77
87
  if (result)
78
88
  return result;
79
89
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
@@ -93,7 +103,7 @@ exports.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (reques
93
103
  ` (replace \`<txHash>\` with the actual transaction hash)`,
94
104
  `4. Retry the tool call`, ``,
95
105
  "**Or bypass with a session token:**",
96
- "Set `NOELCLAW_SESSION_TOKEN` with your Noelclaw session token from noelclaw.xyz",
106
+ "Set `NOELCLAW_SESSION_TOKEN` with your Noelclaw session token from noelclaw.com",
97
107
  ] : []),
98
108
  ];
99
109
  return { content: [{ type: "text", text: lines.join("\n") }], isError: true };
@@ -49,6 +49,20 @@ exports.AUTOMATION_TOOLS = [
49
49
  required: ["automationId"],
50
50
  },
51
51
  },
52
+ {
53
+ name: "run_automation",
54
+ description: "Trigger an automation immediately — regardless of its schedule or trigger condition. " +
55
+ "Use to test an automation after creating it, or to run a one-off DCA/swap/alert right now. " +
56
+ "The automation must be active (not paused or deleted). " +
57
+ "Get the ID from list_automations.",
58
+ inputSchema: {
59
+ type: "object",
60
+ properties: {
61
+ automationId: { type: "string", description: "Automation ID to run now (from list_automations)" },
62
+ },
63
+ required: ["automationId"],
64
+ },
65
+ },
52
66
  ];
53
67
  const CreateAutomationSchema = zod_1.z.object({ rawInput: zod_1.z.string().min(1) });
54
68
  const AutomationIdSchema = zod_1.z.object({ automationId: zod_1.z.string().min(1) });
@@ -148,6 +162,30 @@ async function handleAutomationTool(name, args) {
148
162
  }
149
163
  return { content: [{ type: "text", text: lines.join("\n") }] };
150
164
  }
165
+ case "run_automation": {
166
+ const parsed = AutomationIdSchema.safeParse(args);
167
+ if (!parsed.success)
168
+ return { content: [{ type: "text", text: `Invalid input: automationId ${parsed.error.issues[0].message}` }], isError: true };
169
+ const data = await (0, convex_js_1.callConvex)("/automations/run", "POST", { automationId: parsed.data.automationId }, "run_automation");
170
+ if (data.error)
171
+ return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
172
+ const statusIcon = { success: "✅", failed: "❌", skipped: "⏭️" };
173
+ const icon = statusIcon[data.status] ?? "⚡";
174
+ const spent = data.amountUsd != null ? ` · $${Number(data.amountUsd).toFixed(2)} spent` : "";
175
+ const txLine = data.txHash ? `\nTx: https://basescan.org/tx/${data.txHash}` : "";
176
+ return {
177
+ content: [{
178
+ type: "text",
179
+ text: [
180
+ `${icon} **Automation triggered: ${data.status ?? "executed"}**${spent}`,
181
+ data.message ?? "",
182
+ txLine,
183
+ ``,
184
+ `Use \`get_automation_runs automationId: "${parsed.data.automationId}"\` to see full history.`,
185
+ ].filter(Boolean).join("\n"),
186
+ }],
187
+ };
188
+ }
151
189
  default:
152
190
  return null;
153
191
  }
@@ -138,6 +138,33 @@ exports.CODER_TOOLS = [
138
138
  required: ["code"],
139
139
  },
140
140
  },
141
+ {
142
+ name: "generate_mcp_skill",
143
+ description: "Generate a complete Claude Code skill (.md file) from a description. " +
144
+ "Skills are slash-command workflows that run inside Claude Code — they can call tools, " +
145
+ "loop, delegate to subagents, and have persistent behavior. " +
146
+ "Returns a ready-to-use .md file you can drop into your .claude/skills/ directory. " +
147
+ "Use this to automate repetitive Claude Code workflows without writing TypeScript.",
148
+ inputSchema: {
149
+ type: "object",
150
+ properties: {
151
+ description: {
152
+ type: "string",
153
+ description: "What the skill should do — be specific about inputs, outputs, and any tools it should use",
154
+ },
155
+ name: {
156
+ type: "string",
157
+ description: "Skill name in kebab-case, e.g. 'daily-standup', 'code-review', 'deploy-check'",
158
+ },
159
+ tools: {
160
+ type: "array",
161
+ items: { type: "string" },
162
+ description: "Optional: list of Claude Code tools or MCP tools the skill should use, e.g. ['Bash', 'Read', 'memory_search']",
163
+ },
164
+ },
165
+ required: ["description", "name"],
166
+ },
167
+ },
141
168
  {
142
169
  name: "review_code",
143
170
  description: "Review and improve a piece of code. Returns the improved version with a summary of changes. " +
@@ -191,6 +218,11 @@ const ReviewSchema = zod_1.z.object({
191
218
  language: zod_1.z.string().optional(),
192
219
  goals: zod_1.z.string().optional(),
193
220
  });
221
+ const McpSkillSchema = zod_1.z.object({
222
+ description: zod_1.z.string().min(10),
223
+ name: zod_1.z.string().min(1).regex(/^[a-z0-9-]+$/, "must be kebab-case"),
224
+ tools: zod_1.z.array(zod_1.z.string()).optional(),
225
+ });
194
226
  async function handleCoderTool(name, args) {
195
227
  switch (name) {
196
228
  case "scaffold_project": {
@@ -341,6 +373,36 @@ async function handleCoderTool(name, args) {
341
373
  return err(`review_code failed: ${e.message}`);
342
374
  }
343
375
  }
376
+ case "generate_mcp_skill": {
377
+ const p = McpSkillSchema.safeParse(args);
378
+ if (!p.success)
379
+ return err(`Invalid input: ${p.error.message}`);
380
+ const { description, name: skillName, tools = [] } = p.data;
381
+ const prompt = `Generate a complete Claude Code skill (.md file) for the following workflow:\n\n` +
382
+ `Skill name: /${skillName}\n` +
383
+ `Description: ${description}\n` +
384
+ (tools.length ? `Tools to use: ${tools.join(", ")}\n` : "") +
385
+ `\n` +
386
+ `Claude Code skill format rules:\n` +
387
+ `- The file is a markdown document that serves as a system prompt for a Claude Code slash command\n` +
388
+ `- It should start with a brief description of what the skill does\n` +
389
+ `- Include an "## Input" section explaining what arguments the skill accepts (if any)\n` +
390
+ `- Include a "## Steps" section with numbered, concrete steps\n` +
391
+ `- Include a "## Output" section describing what the skill produces\n` +
392
+ `- Steps can reference tool calls like "Use the Bash tool to run X" or "Use memory_search to find Y"\n` +
393
+ `- Steps can reference conditional logic and loops\n` +
394
+ `- Keep it under 200 lines — skills should be focused, not monolithic\n` +
395
+ `- Write in imperative second person ("Run...", "Check...", "If X, then...")\n` +
396
+ `- Do NOT include markdown fences around the output — output the raw .md content directly\n\n` +
397
+ `Output only the .md file content, ready to save as .claude/skills/${skillName}.md`;
398
+ try {
399
+ const response = await (0, llm_js_1.callLLM)(CODER_SYSTEM, prompt, 2000);
400
+ return ok(`# /${skillName} skill — save to .claude/skills/${skillName}.md\n\n---\n\n${response}`);
401
+ }
402
+ catch (e) {
403
+ return err(`generate_mcp_skill failed: ${e.message}`);
404
+ }
405
+ }
344
406
  default:
345
407
  return null;
346
408
  }
@@ -55,9 +55,45 @@ exports.DEFI_TOOLS = [
55
55
  description: "AI-powered portfolio scan — concentration risk, volatility exposure, Base ecosystem opportunities, and a concrete 3-step action plan based on your actual holdings. Requires wallet auth.",
56
56
  inputSchema: { type: "object", properties: {}, required: [] },
57
57
  },
58
+ {
59
+ name: "analyze_wallet",
60
+ description: "AI-powered analysis of any public wallet on Base — not just your own. " +
61
+ "Enter any 0x address: see token holdings, portfolio value, concentration risk, " +
62
+ "DeFi positions, and a behavioral profile (whale, degen, LP provider, etc.). " +
63
+ "Use to track smart money, research whales, or audit any wallet before copying trades.",
64
+ inputSchema: {
65
+ type: "object",
66
+ properties: {
67
+ address: { type: "string", description: "Wallet address to analyze (0x...)" },
68
+ label: { type: "string", description: "Optional label for this wallet (e.g. 'whale from Twitter')" },
69
+ },
70
+ required: ["address"],
71
+ },
72
+ },
73
+ {
74
+ name: "get_defi_yields",
75
+ description: "Fetch top DeFi yield opportunities on Base — Morpho, Moonwell, Aerodrome, Uniswap, and more. " +
76
+ "Returns live APY, TVL, and pool info from DeFiLlama (no API key required). " +
77
+ "Filter by token or minimum APY. Use before depositing to find the best rates.",
78
+ inputSchema: {
79
+ type: "object",
80
+ properties: {
81
+ token: { type: "string", description: "Optional: filter by token symbol, e.g. 'USDC', 'ETH', 'WETH'" },
82
+ minApy: { type: "number", description: "Optional: minimum APY % to show (default 1)" },
83
+ limit: { type: "number", description: "Max results to return (default 20)" },
84
+ },
85
+ required: [],
86
+ },
87
+ },
58
88
  ];
59
89
  const SwapSchema = zod_1.z.object({ fromToken: zod_1.z.string().min(1), toToken: zod_1.z.string().min(1), amount: zod_1.z.string().min(1) });
60
90
  const SendSchema = zod_1.z.object({ token: zod_1.z.string().min(1), toAddress: zod_1.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "must be a valid 0x address"), amount: zod_1.z.string().min(1) });
91
+ const AnalyzeWalletSchema = zod_1.z.object({ address: zod_1.z.string().regex(/^0x[0-9a-fA-F]{40}$/, "must be a valid 0x address"), label: zod_1.z.string().optional() });
92
+ const DefiYieldsSchema = zod_1.z.object({
93
+ token: zod_1.z.string().optional(),
94
+ minApy: zod_1.z.number().optional(),
95
+ limit: zod_1.z.number().int().min(1).max(100).optional(),
96
+ }).default({});
61
97
  const BUY_DECIMALS = { USDC: 6, USDT: 6, DAI: 18, ETH: 18, WETH: 18 };
62
98
  function formatTokenAmount(raw, token) {
63
99
  const dec = BUY_DECIMALS[token.toUpperCase()] ?? 18;
@@ -183,6 +219,97 @@ async function handleDefiTool(name, args) {
183
219
  : "";
184
220
  return { content: [{ type: "text", text: header + body + footer }] };
185
221
  }
222
+ case "analyze_wallet": {
223
+ const parsed = AnalyzeWalletSchema.safeParse(args);
224
+ if (!parsed.success)
225
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
226
+ const { address, label } = parsed.data;
227
+ const data = await (0, convex_js_1.callConvex)("/wallet/analyze", "POST", { address, label }, "analyze_wallet");
228
+ if (data.error)
229
+ return { content: [{ type: "text", text: `Wallet analysis failed: ${data.error}` }], isError: true };
230
+ const total = (data.totalUsd ?? 0).toFixed(2);
231
+ const walletLabel = label ? ` — ${label}` : "";
232
+ const topHoldings = (data.holdings ?? [])
233
+ .slice(0, 8)
234
+ .map(h => `• **${h.token}**: $${(h.valueUsd ?? 0).toFixed(2)}${h.pct != null ? ` (${h.pct}%)` : ""}`)
235
+ .join("\n");
236
+ const profileLine = data.profile ? `**Profile:** ${data.profile}\n` : "";
237
+ const header = [
238
+ `**Wallet Analysis**${walletLabel}`,
239
+ `\`${address}\``,
240
+ `**Portfolio value:** $${total}`,
241
+ ``,
242
+ profileLine,
243
+ `**Holdings:**`,
244
+ topHoldings || "No token holdings found.",
245
+ ``,
246
+ ].join("\n");
247
+ const body = data.analysis ?? (data.analysisError ? `*AI analysis unavailable: ${data.analysisError}*` : "*AI analysis not available*");
248
+ return { content: [{ type: "text", text: header + body }] };
249
+ }
250
+ case "get_defi_yields": {
251
+ const parsed = DefiYieldsSchema.safeParse(args ?? {});
252
+ if (!parsed.success)
253
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
254
+ const { token, minApy = 1, limit = 20 } = parsed.data;
255
+ let pools;
256
+ try {
257
+ const res = await fetch("https://yields.llama.fi/pools", { signal: AbortSignal.timeout(15000) });
258
+ if (!res.ok)
259
+ throw new Error(`HTTP ${res.status}`);
260
+ const data = await res.json();
261
+ pools = data.data ?? [];
262
+ }
263
+ catch (e) {
264
+ return { content: [{ type: "text", text: `DeFiLlama fetch failed: ${e.message}` }], isError: true };
265
+ }
266
+ // Filter to Base chain
267
+ let filtered = pools.filter((p) => p.chain === "Base");
268
+ // Filter by token if specified
269
+ if (token) {
270
+ const upper = token.toUpperCase();
271
+ filtered = filtered.filter((p) => (p.symbol ?? "").toUpperCase().includes(upper) ||
272
+ (p.underlyingTokens ?? []).some((t) => t.toUpperCase().includes(upper)));
273
+ }
274
+ // Filter by minimum APY and remove outliers (>10000% are usually broken)
275
+ filtered = filtered
276
+ .filter((p) => (p.apy ?? 0) >= minApy && (p.apy ?? 0) <= 10000)
277
+ .sort((a, b) => (b.apy ?? 0) - (a.apy ?? 0))
278
+ .slice(0, limit);
279
+ if (!filtered.length) {
280
+ return {
281
+ content: [{
282
+ type: "text",
283
+ text: [
284
+ `## DeFi Yields on Base`,
285
+ `No pools found${token ? ` for ${token.toUpperCase()}` : ""} with APY ≥ ${minApy}%.`,
286
+ ``,
287
+ `Try lowering \`minApy\` or removing the token filter.`,
288
+ ].join("\n"),
289
+ }],
290
+ };
291
+ }
292
+ const fmt = (n) => n >= 1000000000 ? `$${(n / 1000000000).toFixed(1)}B`
293
+ : n >= 1000000 ? `$${(n / 1000000).toFixed(1)}M`
294
+ : n >= 1000 ? `$${(n / 1000).toFixed(0)}K`
295
+ : `$${n.toFixed(0)}`;
296
+ const lines = [
297
+ `## DeFi Yields on Base${token ? ` — ${token.toUpperCase()}` : ""}`,
298
+ `Top ${filtered.length} pools · APY ≥ ${minApy}% · Source: DeFiLlama`,
299
+ ``,
300
+ `| # | Pool | Protocol | APY | TVL |`,
301
+ `|---|------|----------|-----|-----|`,
302
+ ];
303
+ filtered.forEach((p, i) => {
304
+ const apy = (p.apy ?? 0).toFixed(1);
305
+ const tvl = fmt(p.tvlUsd ?? 0);
306
+ const name = (p.symbol ?? p.pool ?? "—").replace(/-/g, " ");
307
+ const proj = p.project ?? "—";
308
+ lines.push(`| ${i + 1} | ${name} | ${proj} | **${apy}%** | ${tvl} |`);
309
+ });
310
+ lines.push(``, `Use \`swap_tokens\` to position, then deposit via the protocol's UI. Always check smart contract risk before depositing.`);
311
+ return { content: [{ type: "text", text: lines.join("\n") }] };
312
+ }
186
313
  default:
187
314
  return null;
188
315
  }
@@ -5,6 +5,66 @@ exports.handleHumanizerTool = handleHumanizerTool;
5
5
  const zod_1 = require("zod");
6
6
  const llm_js_1 = require("../llm.js");
7
7
  exports.HUMANIZER_TOOLS = [
8
+ {
9
+ name: "write_thread",
10
+ description: "Write a viral Twitter/X thread on any crypto or tech topic. " +
11
+ "Returns a numbered thread (1/, 2/, ...) with a hook tweet, " +
12
+ "3-7 content tweets, and a strong closer with CTA. " +
13
+ "Written in a direct, punchy voice — no fluff, no AI tells. " +
14
+ "Optionally provide your own voice sample to match your style.",
15
+ inputSchema: {
16
+ type: "object",
17
+ properties: {
18
+ topic: {
19
+ type: "string",
20
+ description: "The topic or angle for the thread, e.g. 'why Base is winning', 'how I use MCP agents for DeFi research'",
21
+ },
22
+ tone: {
23
+ type: "string",
24
+ enum: ["alpha", "educational", "opinion", "story"],
25
+ description: "Thread tone: 'alpha' (edge/insight), 'educational' (explainer), 'opinion' (hot take), 'story' (personal narrative). Default: opinion.",
26
+ },
27
+ tweets: {
28
+ type: "number",
29
+ description: "Number of tweets in the thread, 4–12 (default: 7)",
30
+ },
31
+ voice_sample: {
32
+ type: "string",
33
+ description: "Optional: paste 1-3 of your existing tweets to match your voice",
34
+ },
35
+ },
36
+ required: ["topic"],
37
+ },
38
+ },
39
+ {
40
+ name: "write_post",
41
+ description: "Write a single viral-style crypto/tech post for Twitter/X. " +
42
+ "Returns one punchy post under 280 characters (or up to 500 with long-form enabled). " +
43
+ "Hooks in the first line, delivers the insight, ends with impact. No AI fluff.",
44
+ inputSchema: {
45
+ type: "object",
46
+ properties: {
47
+ topic: {
48
+ type: "string",
49
+ description: "What to post about — a thought, observation, alpha, or hot take",
50
+ },
51
+ style: {
52
+ type: "string",
53
+ enum: ["hook", "hot-take", "alpha", "question", "observation"],
54
+ description: "Post style. Default: hook.",
55
+ },
56
+ long: {
57
+ type: "boolean",
58
+ description: "Allow up to 500 chars (long-form post). Default: false (280 chars max)",
59
+ },
60
+ voice_sample: {
61
+ type: "string",
62
+ description: "Optional: a sample of your writing to match your voice",
63
+ },
64
+ },
65
+ required: ["topic"],
66
+ },
67
+ },
8
68
  {
9
69
  name: "humanize_text",
10
70
  description: "Remove AI writing patterns from text — makes it sound natural, direct, and human. " +
@@ -28,8 +88,20 @@ exports.HUMANIZER_TOOLS = [
28
88
  },
29
89
  ];
30
90
  const HumanizerSchema = zod_1.z.object({
31
- text: zod_1.z.string().min(1),
32
- voice_sample: zod_1.z.string().optional(),
91
+ text: zod_1.z.string().min(1).max(20000),
92
+ voice_sample: zod_1.z.string().max(5000).optional(),
93
+ });
94
+ const WriteThreadSchema = zod_1.z.object({
95
+ topic: zod_1.z.string().min(3).max(500),
96
+ tone: zod_1.z.enum(["alpha", "educational", "opinion", "story"]).optional(),
97
+ tweets: zod_1.z.number().int().min(4).max(12).optional(),
98
+ voice_sample: zod_1.z.string().max(5000).optional(),
99
+ });
100
+ const WritePostSchema = zod_1.z.object({
101
+ topic: zod_1.z.string().min(3).max(500),
102
+ style: zod_1.z.enum(["hook", "hot-take", "alpha", "question", "observation"]).optional(),
103
+ long: zod_1.z.boolean().optional(),
104
+ voice_sample: zod_1.z.string().max(5000).optional(),
33
105
  });
34
106
  const HUMANIZER_SYSTEM = `You are a text editor that removes signs of AI-generated writing.
35
107
 
@@ -78,7 +150,76 @@ PROCESS:
78
150
  6. Output ONLY the final humanized text — no commentary, no explanation, no "Here is your text:"
79
151
 
80
152
  If a voice sample is provided, match its tone, rhythm, and vocabulary. Otherwise use direct, opinionated, natural prose.`;
153
+ const THREAD_SYSTEM = `You are a crypto Twitter ghostwriter who writes threads that go viral. Your style: direct, no fluff, confident without being cringe. You understand DeFi, on-chain data, narratives, and market structure. You write like a smart practitioner, not a content creator.
154
+
155
+ Rules:
156
+ - First tweet is the hook — bold claim or surprising insight. Must make people stop scrolling.
157
+ - Middle tweets: each one standalone insight. No "in this thread I'll explain" filler.
158
+ - Last tweet: the payoff. Strong closer, optional CTA (follow, RT, reply) — one CTA max.
159
+ - Number format: 1/ 2/ 3/ etc. Each tweet on its own line, separated by blank line.
160
+ - Under 280 chars per tweet unless it genuinely needs more (max 500).
161
+ - No em dashes, no "delve", no "landscape", no "it's worth noting".
162
+ - No hashtags unless they're actually used. No emojis unless they add meaning.
163
+ - Write in the user's voice if a sample is provided.`;
164
+ const POST_SYSTEM = `You are a crypto Twitter ghostwriter. Write one punchy, high-impact post. Direct. No fluff. Hook in the first line. No em dashes, no AI vocabulary. Write like a smart practitioner with an edge.`;
81
165
  async function handleHumanizerTool(name, args) {
166
+ if (name === "write_thread") {
167
+ const parsed = WriteThreadSchema.safeParse(args);
168
+ if (!parsed.success)
169
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
170
+ const { topic, tone = "opinion", tweets = 7, voice_sample } = parsed.data;
171
+ const toneGuides = {
172
+ alpha: "Share non-obvious insights or edge. Act like you have information most people don't.",
173
+ educational: "Explain a concept clearly. Assume smart but non-expert reader.",
174
+ opinion: "Take a clear position. Defend it with reasoning. Don't hedge.",
175
+ story: "Tell a real story with a beginning, conflict, and lesson. Make it personal and specific.",
176
+ };
177
+ const prompt = [
178
+ `Write a ${tweets}-tweet Twitter/X thread on: ${topic}`,
179
+ ``,
180
+ `Tone: ${tone} — ${toneGuides[tone]}`,
181
+ voice_sample ? `Voice sample (match this style):\n${voice_sample}` : "",
182
+ ``,
183
+ `Format: number each tweet as 1/ 2/ 3/ etc., separated by blank lines.`,
184
+ `First tweet = hook. Last tweet = strong closer.`,
185
+ `Output only the tweets — no intro, no explanation.`,
186
+ ].filter(Boolean).join("\n");
187
+ try {
188
+ const output = await (0, llm_js_1.callLLM)(THREAD_SYSTEM, prompt, 2000);
189
+ return { content: [{ type: "text", text: output.trim() }] };
190
+ }
191
+ catch (err) {
192
+ return { content: [{ type: "text", text: `write_thread error: ${err.message}` }], isError: true };
193
+ }
194
+ }
195
+ if (name === "write_post") {
196
+ const parsed = WritePostSchema.safeParse(args);
197
+ if (!parsed.success)
198
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
199
+ const { topic, style = "hook", long = false, voice_sample } = parsed.data;
200
+ const styleGuides = {
201
+ hook: "Strong first line that stops the scroll. Deliver the insight after.",
202
+ "hot-take": "Controversial opinion stated plainly. Don't soften it.",
203
+ alpha: "Non-obvious market insight written like you're sharing it with one smart friend.",
204
+ question: "Ask a sharp, thought-provoking question. Don't answer it.",
205
+ observation: "One specific thing you noticed that most people missed.",
206
+ };
207
+ const charLimit = long ? 500 : 280;
208
+ const prompt = [
209
+ `Write one ${style} post about: ${topic}`,
210
+ `Style: ${styleGuides[style]}`,
211
+ `Max length: ${charLimit} characters.`,
212
+ voice_sample ? `Voice sample:\n${voice_sample}` : "",
213
+ `Output only the post text — nothing else.`,
214
+ ].filter(Boolean).join("\n");
215
+ try {
216
+ const output = await (0, llm_js_1.callLLM)(POST_SYSTEM, prompt, 300);
217
+ return { content: [{ type: "text", text: output.trim() }] };
218
+ }
219
+ catch (err) {
220
+ return { content: [{ type: "text", text: `write_post error: ${err.message}` }], isError: true };
221
+ }
222
+ }
82
223
  if (name !== "humanize_text")
83
224
  return null;
84
225
  const parsed = HumanizerSchema.safeParse(args);