@noelclaw/mcp 2.1.0 → 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.
package/dist/convex.js CHANGED
@@ -54,7 +54,9 @@ async function callConvex(path, method, body, toolName = "unknown") {
54
54
  const paymentHeader = process.env.NOELCLAW_PAYMENT_HEADER;
55
55
  if (paymentHeader)
56
56
  headers["X-Payment"] = paymentHeader;
57
- // BYOK headers
57
+ // BYOK headers — user pays for their own AI/service costs
58
+ if (process.env.ANTHROPIC_API_KEY)
59
+ headers["X-User-Anthropic-Key"] = process.env.ANTHROPIC_API_KEY;
58
60
  if (process.env.GROK_API_KEY)
59
61
  headers["X-User-Grok-Key"] = process.env.GROK_API_KEY;
60
62
  if (process.env.BANKR_API_KEY)
package/dist/llm.js ADDED
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.callLLM = callLLM;
4
+ const ANTHROPIC_URL = "https://api.anthropic.com/v1/messages";
5
+ const BANKR_URL = "https://llm.bankr.bot/v1/chat/completions";
6
+ /**
7
+ * Call the best available LLM.
8
+ * Priority: ANTHROPIC_API_KEY → BANKR_API_KEY → throws
9
+ * Claude Desktop automatically injects ANTHROPIC_API_KEY, so users
10
+ * pay for their own AI usage without the server owner absorbing the cost.
11
+ */
12
+ async function callLLM(systemPrompt, userPrompt, maxTokens = 1024, history = [], timeoutMs = 60000) {
13
+ const anthropicKey = process.env.ANTHROPIC_API_KEY;
14
+ const bankrKey = process.env.BANKR_API_KEY;
15
+ if (anthropicKey)
16
+ return callAnthropic(anthropicKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs);
17
+ if (bankrKey)
18
+ return callBankr(bankrKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs);
19
+ throw new Error("No LLM API key available. Claude Desktop users: make sure ANTHROPIC_API_KEY is in your MCP config. " +
20
+ "Standalone users: set BANKR_API_KEY.");
21
+ }
22
+ async function callAnthropic(apiKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs) {
23
+ const messages = [...history, { role: "user", content: userPrompt }];
24
+ const model = process.env.ANTHROPIC_MODEL ?? "claude-haiku-4-5-20251001";
25
+ const res = await fetch(ANTHROPIC_URL, {
26
+ method: "POST",
27
+ headers: {
28
+ "Content-Type": "application/json",
29
+ "x-api-key": apiKey,
30
+ "anthropic-version": "2023-06-01",
31
+ },
32
+ body: JSON.stringify({ model, max_tokens: maxTokens, system: systemPrompt, messages }),
33
+ signal: AbortSignal.timeout(timeoutMs),
34
+ });
35
+ if (!res.ok) {
36
+ const body = await res.text().catch(() => "");
37
+ throw new Error(`Anthropic error ${res.status}: ${body.slice(0, 200)}`);
38
+ }
39
+ const data = await res.json();
40
+ return data.content?.find(b => b.type === "text")?.text ?? "";
41
+ }
42
+ async function callBankr(apiKey, systemPrompt, userPrompt, maxTokens, history, timeoutMs) {
43
+ const model = process.env.BANKR_MODEL ?? "grok-3";
44
+ const res = await fetch(BANKR_URL, {
45
+ method: "POST",
46
+ headers: { "Content-Type": "application/json", "X-API-Key": apiKey },
47
+ body: JSON.stringify({
48
+ model,
49
+ messages: [
50
+ { role: "system", content: systemPrompt },
51
+ ...history,
52
+ { role: "user", content: userPrompt },
53
+ ],
54
+ max_tokens: maxTokens,
55
+ }),
56
+ signal: AbortSignal.timeout(timeoutMs),
57
+ });
58
+ if (!res.ok) {
59
+ const body = await res.text().catch(() => "");
60
+ throw new Error(`Bankr error ${res.status}: ${body.slice(0, 200)}`);
61
+ }
62
+ const data = await res.json();
63
+ return data.choices?.[0]?.message?.content ?? "";
64
+ }
package/dist/server.js CHANGED
@@ -38,9 +38,9 @@ exports.ALL_TOOLS = [
38
38
  ...insight_js_1.INSIGHT_TOOLS, // 1 — ask_noel
39
39
  ...defi_js_1.DEFI_TOOLS, // 5 — get_portfolio, estimate_swap, swap_tokens, send_token, scan_wallet
40
40
  ...automation_js_1.AUTOMATION_TOOLS, // 5 — create, list, pause, delete, get_runs
41
- ...swarm_js_1.SWARM_TOOLS, // 6 — start, stop, status, read/write memory, scores
41
+ ...swarm_js_1.SWARM_TOOLS, // 9 — start, stop, status, read/write memory, scores, research, brief, trigger_agent
42
42
  ...framework_js_1.FRAMEWORK_TOOLS, // 6 — task packets, playbooks, sentinel, ledger
43
- ...vault_js_1.VAULT_TOOLS, // 7 — save, read, list, search, history, diff, export
43
+ ...vault_js_1.VAULT_TOOLS, // 13 — save, read, list, search, history, diff, export, remember, context, store_credential, get_credential, publish, explore
44
44
  ...wallet_js_1.WALLET_TOOLS, // 2 — get_wallet_address, set_telegram
45
45
  ...miroshark_js_1.MIROSHARK_TOOLS, // 3 — simulate, status, stop
46
46
  ...humanizer_js_1.HUMANIZER_TOOLS, // 1 — humanize_text
@@ -48,9 +48,9 @@ exports.ALL_TOOLS = [
48
48
  ...scanner_js_1.SCANNER_TOOLS, // 3 — score_token, check_token, scan_dips
49
49
  ...coder_js_1.CODER_TOOLS, // 6 — scaffold_project, generate_component, generate_contract, audit_contract, explain_code, review_code
50
50
  ...base_js_1.BASE_TOOLS, // 4 — query_vaults, list_markets, prepare_deposit, chain_stats
51
- // total: 53
51
+ // total: 62
52
52
  ];
53
- exports.server = new index_js_1.Server({ name: "noelclaw", version: "2.1.0" }, { capabilities: { tools: {} } });
53
+ exports.server = new index_js_1.Server({ name: "noelclaw", version: "2.2.0" }, { capabilities: { tools: {} } });
54
54
  exports.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({ tools: exports.ALL_TOOLS }));
55
55
  exports.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
56
56
  const { name, arguments: args } = request.params;
@@ -87,8 +87,8 @@ async function fetchMorphoVaults(asset, limit = 10) {
87
87
  const gql = `{
88
88
  vaults(
89
89
  where: { chainId_in: [8453] }
90
- orderBy: "state_netApy"
91
- orderDirection: "desc"
90
+ orderBy: NetApy
91
+ orderDirection: Desc
92
92
  first: 50
93
93
  ) {
94
94
  items {
@@ -109,6 +109,12 @@ async function fetchMorphoVaults(asset, limit = 10) {
109
109
  throw new Error(`Morpho API error: ${res.status}`);
110
110
  const data = await res.json();
111
111
  let vaults = data?.data?.vaults?.items ?? [];
112
+ // Filter out test/spam vaults: min $10k TVL, max 500% APY
113
+ vaults = vaults.filter((v) => {
114
+ const tvl = v.state?.totalAssetsUsd ?? 0;
115
+ const apy = v.state?.netApy ?? v.state?.apy ?? 0;
116
+ return tvl >= 10000 && apy <= 5;
117
+ });
112
118
  if (asset) {
113
119
  vaults = vaults.filter((v) => v.asset?.symbol?.toLowerCase().includes(asset.toLowerCase()));
114
120
  }
@@ -131,17 +137,19 @@ async function fetchMoonwellMarkets(asset) {
131
137
  if (!res.ok)
132
138
  throw new Error(`Moonwell API error: ${res.status}`);
133
139
  const data = await res.json();
134
- let markets = data?.data ?? data?.markets ?? [];
140
+ let markets = Array.isArray(data) ? data : (data?.data ?? data?.markets ?? []);
141
+ // Filter deprecated markets
142
+ markets = markets.filter((m) => !m.deprecated);
135
143
  if (asset) {
136
- markets = markets.filter((m) => (m.underlyingSymbol ?? m.symbol ?? "").toLowerCase().includes(asset.toLowerCase()));
144
+ markets = markets.filter((m) => (m.asset ?? m.underlyingSymbol ?? m.symbol ?? "").toLowerCase().includes(asset.toLowerCase()));
137
145
  }
138
146
  if (!markets.length)
139
147
  return "No markets found.";
140
148
  const lines = markets.slice(0, 15).map((m, i) => {
141
- const symbol = m.underlyingSymbol ?? m.symbol ?? "?";
142
- const supplyApy = pct((m.supplyApy ?? m.supplyRate ?? 0));
143
- const borrowApy = pct((m.borrowApy ?? m.borrowRate ?? 0));
144
- const liquidity = fmt(m.totalSupplyUsd ?? m.totalSupply ?? 0);
149
+ const symbol = m.asset ?? m.underlyingSymbol ?? m.symbol ?? "?";
150
+ const supplyApy = pct((m.baseSupplyApy ?? m.supplyApy ?? m.supplyRate ?? 0) / 100);
151
+ const borrowApy = pct((m.baseBorrowApy ?? m.borrowApy ?? m.borrowRate ?? 0) / 100);
152
+ const liquidity = fmt(m.totalSupplyUsd ?? m.liquidityUsd ?? m.totalSupply ?? 0);
145
153
  const util = m.utilization != null ? `${(m.utilization * 100).toFixed(1)}%` : "—";
146
154
  return `${i + 1}. ${symbol}\n Supply APY: ${supplyApy} Borrow APY: ${borrowApy} Liquidity: ${liquidity} Util: ${util}`;
147
155
  });
@@ -179,8 +187,8 @@ async function prepareDeposit(vaultName, asset, amount) {
179
187
  const gql = `{
180
188
  vaults(
181
189
  where: { chainId_in: [8453] }
182
- orderBy: "state_netApy"
183
- orderDirection: "desc"
190
+ orderBy: NetApy
191
+ orderDirection: Desc
184
192
  first: 100
185
193
  ) {
186
194
  items {
@@ -3,8 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CODER_TOOLS = void 0;
4
4
  exports.handleCoderTool = handleCoderTool;
5
5
  const zod_1 = require("zod");
6
- const BANKR_LLM_URL = "https://llm.bankr.bot/v1/chat/completions";
7
- const BANKR_MODEL = process.env.BANKR_MODEL ?? "grok-3";
6
+ const llm_js_1 = require("../llm.js");
8
7
  const CODER_SYSTEM = `You are Noel Coder — an expert software engineer embedded in the Noelclaw AI OS on Base chain.
9
8
  You specialize in:
10
9
  - Solidity smart contracts (ERC-20, ERC-721, DeFi hooks, Uniswap v3/v4 integrations)
@@ -20,30 +19,6 @@ Rules:
20
19
  - For React: use TypeScript, functional components, Tailwind CSS.
21
20
  - For MCP tools: follow the Noelclaw pattern (Zod schema, handler returns ToolResult | null).
22
21
  - Never include secrets or private keys in output.`;
23
- async function callLLM(systemPrompt, userPrompt, maxTokens = 4096) {
24
- const key = process.env.BANKR_API_KEY;
25
- if (!key)
26
- throw new Error("BANKR_API_KEY not set — add it to your .env");
27
- const res = await fetch(BANKR_LLM_URL, {
28
- method: "POST",
29
- headers: { "Content-Type": "application/json", "X-API-Key": key },
30
- body: JSON.stringify({
31
- model: BANKR_MODEL,
32
- messages: [
33
- { role: "system", content: systemPrompt },
34
- { role: "user", content: userPrompt },
35
- ],
36
- max_tokens: maxTokens,
37
- }),
38
- signal: AbortSignal.timeout(90000),
39
- });
40
- if (!res.ok) {
41
- const body = await res.text().catch(() => "");
42
- throw new Error(`LLM error ${res.status}: ${body.slice(0, 200)}`);
43
- }
44
- const data = await res.json();
45
- return data.choices?.[0]?.message?.content ?? "";
46
- }
47
22
  function ok(text) {
48
23
  return { content: [{ type: "text", text }] };
49
24
  }
@@ -234,7 +209,7 @@ async function handleCoderTool(name, args) {
234
209
  `4. Setup instructions (3-5 steps)\n\n` +
235
210
  `Use real, runnable code. No TODO placeholders.`;
236
211
  try {
237
- const response = await callLLM(CODER_SYSTEM, prompt, 4096);
212
+ const response = await (0, llm_js_1.callLLM)(CODER_SYSTEM, prompt, 4096);
238
213
  return ok(response);
239
214
  }
240
215
  catch (e) {
@@ -259,7 +234,7 @@ async function handleCoderTool(name, args) {
259
234
  `- Named export (not default)\n` +
260
235
  `Output only the .tsx file content, no prose before or after.`;
261
236
  try {
262
- const response = await callLLM(CODER_SYSTEM, prompt, 3000);
237
+ const response = await (0, llm_js_1.callLLM)(CODER_SYSTEM, prompt, 3000);
263
238
  return ok(response);
264
239
  }
265
240
  catch (e) {
@@ -285,7 +260,7 @@ async function handleCoderTool(name, args) {
285
260
  `- Include a basic test outline as a comment at the bottom\n` +
286
261
  `Output only the .sol file content.`;
287
262
  try {
288
- const response = await callLLM(CODER_SYSTEM, prompt, 3500);
263
+ const response = await (0, llm_js_1.callLLM)(CODER_SYSTEM, prompt, 3500);
289
264
  return ok(response);
290
265
  }
291
266
  catch (e) {
@@ -314,7 +289,7 @@ async function handleCoderTool(name, args) {
314
289
  `## Positive Patterns\n` +
315
290
  `(what the contract does well)`;
316
291
  try {
317
- const response = await callLLM(CODER_SYSTEM, prompt, 3000);
292
+ const response = await (0, llm_js_1.callLLM)(CODER_SYSTEM, prompt, 3000);
318
293
  return ok(response);
319
294
  }
320
295
  catch (e) {
@@ -335,7 +310,7 @@ async function handleCoderTool(name, args) {
335
310
  `Code:\n\`\`\`\n${code}\n\`\`\`\n\n` +
336
311
  `Write in plain language. Assume the reader understands programming but may not know this specific codebase or language.`;
337
312
  try {
338
- const response = await callLLM(CODER_SYSTEM, prompt, 2000);
313
+ const response = await (0, llm_js_1.callLLM)(CODER_SYSTEM, prompt, 2000);
339
314
  return ok(response);
340
315
  }
341
316
  catch (e) {
@@ -359,7 +334,7 @@ async function handleCoderTool(name, args) {
359
334
  `## Further recommendations\n` +
360
335
  `(optional: things to consider beyond this snippet)`;
361
336
  try {
362
- const response = await callLLM(CODER_SYSTEM, prompt, 3500);
337
+ const response = await (0, llm_js_1.callLLM)(CODER_SYSTEM, prompt, 3500);
363
338
  return ok(response);
364
339
  }
365
340
  catch (e) {
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HUMANIZER_TOOLS = void 0;
4
4
  exports.handleHumanizerTool = handleHumanizerTool;
5
5
  const zod_1 = require("zod");
6
+ const llm_js_1 = require("../llm.js");
6
7
  exports.HUMANIZER_TOOLS = [
7
8
  {
8
9
  name: "humanize_text",
@@ -30,8 +31,6 @@ const HumanizerSchema = zod_1.z.object({
30
31
  text: zod_1.z.string().min(1),
31
32
  voice_sample: zod_1.z.string().optional(),
32
33
  });
33
- const BANKR_LLM_URL = "https://llm.bankr.bot/v1/chat/completions";
34
- const BANKR_MODEL = process.env.BANKR_MODEL ?? "grok-3";
35
34
  const HUMANIZER_SYSTEM = `You are a text editor that removes signs of AI-generated writing.
36
35
 
37
36
  Your job: rewrite the input so it sounds natural, direct, and human — without changing the meaning.
@@ -87,40 +86,14 @@ async function handleHumanizerTool(name, args) {
87
86
  return { content: [{ type: "text", text: `Invalid input: text ${parsed.error.issues[0].message}` }], isError: true };
88
87
  }
89
88
  const { text, voice_sample } = parsed.data;
90
- const apiKey = process.env.BANKR_API_KEY;
91
- if (!apiKey) {
92
- return { content: [{ type: "text", text: "BANKR_API_KEY not set — add it to your MCP env config." }], isError: true };
93
- }
94
89
  const userMsg = voice_sample
95
90
  ? `VOICE SAMPLE (match this style):\n${voice_sample}\n\n---\n\nTEXT TO HUMANIZE:\n${text}`
96
91
  : text;
97
92
  try {
98
- const res = await fetch(BANKR_LLM_URL, {
99
- method: "POST",
100
- headers: {
101
- "X-API-Key": apiKey,
102
- "Content-Type": "application/json",
103
- },
104
- body: JSON.stringify({
105
- model: BANKR_MODEL,
106
- messages: [
107
- { role: "system", content: HUMANIZER_SYSTEM },
108
- { role: "user", content: userMsg },
109
- ],
110
- temperature: 0.7,
111
- max_tokens: 4096,
112
- }),
113
- signal: AbortSignal.timeout(30000),
114
- });
115
- if (!res.ok) {
116
- const err = await res.text().catch(() => res.statusText);
117
- return { content: [{ type: "text", text: `Bankr LLM error ${res.status}: ${err.slice(0, 200)}` }], isError: true };
118
- }
119
- const data = await res.json();
120
- const output = data.choices?.[0]?.message?.content?.trim() ?? "";
93
+ const output = await (0, llm_js_1.callLLM)(HUMANIZER_SYSTEM, userMsg, 4096);
121
94
  if (!output)
122
95
  return { content: [{ type: "text", text: "Empty response from model" }], isError: true };
123
- return { content: [{ type: "text", text: output }] };
96
+ return { content: [{ type: "text", text: output.trim() }] };
124
97
  }
125
98
  catch (err) {
126
99
  return { content: [{ type: "text", text: `Humanizer error: ${err.message}` }], isError: true };
@@ -4,6 +4,7 @@ exports.INSIGHT_TOOLS = void 0;
4
4
  exports.handleInsightTool = handleInsightTool;
5
5
  const zod_1 = require("zod");
6
6
  const convex_js_1 = require("../convex.js");
7
+ const llm_js_1 = require("../llm.js");
7
8
  exports.INSIGHT_TOOLS = [
8
9
  {
9
10
  name: "ask_noel",
@@ -30,34 +31,7 @@ const AskNoelSchema = zod_1.z.object({
30
31
  question: zod_1.z.string().min(1),
31
32
  messages: zod_1.z.array(zod_1.z.object({ role: zod_1.z.enum(["user", "assistant"]), content: zod_1.z.string() })).optional(),
32
33
  });
33
- const BANKR_LLM_URL = "https://llm.bankr.bot/v1/chat/completions";
34
- const BANKR_MODEL = process.env.BANKR_MODEL ?? "grok-3";
35
34
  const NOEL_SYSTEM_PROMPT = `You are Noel, a crypto AI analyst with deep expertise in DeFi, on-chain data, market structure, and trading psychology. You provide sharp, direct analysis — no fluff, no disclaimers. You understand narratives, liquidity flows, whale behavior, and how sentiment drives price. When asked about a token or market, give your honest read with supporting reasoning.`;
36
- async function askViaBankr(question, messages) {
37
- const res = await fetch(BANKR_LLM_URL, {
38
- method: "POST",
39
- headers: {
40
- "X-API-Key": process.env.BANKR_API_KEY,
41
- "Content-Type": "application/json",
42
- },
43
- body: JSON.stringify({
44
- model: BANKR_MODEL,
45
- messages: [
46
- { role: "system", content: NOEL_SYSTEM_PROMPT },
47
- ...messages,
48
- { role: "user", content: question },
49
- ],
50
- max_tokens: 1024,
51
- }),
52
- signal: AbortSignal.timeout(30000),
53
- });
54
- if (!res.ok) {
55
- const err = await res.text().catch(() => res.statusText);
56
- throw new Error(`Bankr LLM error ${res.status}: ${err.slice(0, 200)}`);
57
- }
58
- const data = await res.json();
59
- return data.choices?.[0]?.message?.content ?? "No response from model";
60
- }
61
35
  async function handleInsightTool(name, args) {
62
36
  if (name !== "ask_noel")
63
37
  return null;
@@ -65,18 +39,18 @@ async function handleInsightTool(name, args) {
65
39
  if (!parsed.success)
66
40
  return { content: [{ type: "text", text: `Invalid input: question ${parsed.error.issues[0].message}` }], isError: true };
67
41
  const { question, messages = [] } = parsed.data;
68
- // If BANKR_API_KEY is set, call Bankr LLM directly faster, no Convex hop
69
- if (process.env.BANKR_API_KEY) {
42
+ // Try local LLM first (Anthropic from Claude Desktop, or Bankr if configured)
43
+ if (process.env.ANTHROPIC_API_KEY || process.env.BANKR_API_KEY) {
70
44
  try {
71
- const answer = await askViaBankr(question, messages);
45
+ const history = messages.map(m => ({ role: m.role, content: m.content }));
46
+ const answer = await (0, llm_js_1.callLLM)(NOEL_SYSTEM_PROMPT, question, 1024, history);
72
47
  return { content: [{ type: "text", text: answer }] };
73
48
  }
74
49
  catch (err) {
75
- // Fall through to Convex if Bankr call fails
76
- console.error(`Bankr LLM failed, falling back to Convex: ${err.message}`);
50
+ console.error(`Local LLM failed, falling back to Convex: ${err.message}`);
77
51
  }
78
52
  }
79
- // Fallback: route through Convex backend
53
+ // Fallback: route through Convex backend (uses server's configured key)
80
54
  const data = await (0, convex_js_1.callConvex)("/mcp/chat", "POST", {
81
55
  question,
82
56
  agentId: "noel-default",
@@ -2,6 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MIROSHARK_TOOLS = void 0;
4
4
  exports.handleMirosharkTool = handleMirosharkTool;
5
+ const llm_js_1 = require("../llm.js");
6
+ const convex_js_1 = require("../convex.js");
5
7
  const CONVEX_SITE = process.env.NOELCLAW_CONVEX_URL ?? "https://api.noelclaw.com";
6
8
  exports.MIROSHARK_TOOLS = [
7
9
  {
@@ -239,25 +241,67 @@ async function handleMirosharkTool(name, args) {
239
241
  };
240
242
  }
241
243
  if (runnerStatus === "completed" || runnerStatus === "stopped") {
242
- // Fetch a sample of agent actions for the summary
243
- const actionsData = await miroJson(`/miroshark/api/simulation/${simId}/actions?limit=10`, "GET").catch(() => ({ actions: [] }));
244
+ const actionsData = await miroJson(`/miroshark/api/simulation/${simId}/actions?limit=50`, "GET").catch(() => ({ actions: [] }));
244
245
  const actions = actionsData?.actions ?? [];
245
- const lines = [
246
- `**MiroShark \`${simId}\`** ${runnerStatus}`,
247
- ``,
248
- `Rounds completed: ${runStatus.current_round ?? "?"}`,
249
- `Total actions: ${runStatus.total_actions_count ?? actions.length}`,
250
- ];
251
- if (actions.length > 0) {
252
- lines.push("", "**Sample agent activity:**");
253
- for (const act of actions.slice(0, 8)) {
246
+ const totalActions = runStatus.total_actions_count ?? actions.length;
247
+ const rounds = runStatus.current_round ?? "?";
248
+ const ACTION_EMOJI = {
249
+ tweet: "🐦", post: "📝", sell: "📉", buy: "📈",
250
+ article: "📰", comment: "💬", alert: "🚨", analyze: "🔍",
251
+ };
252
+ // Format agent feed
253
+ const feed = actions.slice(0, 20).map((act) => {
254
+ const who = act.agent_name ?? act.agent_id ?? "agent";
255
+ const what = (act.action_type ?? act.type ?? "action").toLowerCase();
256
+ const emoji = ACTION_EMOJI[what] ?? "•";
257
+ const content = act.content ?? act.text ?? "";
258
+ return `${emoji} **${who}** [${what}]${content ? `: ${String(content).slice(0, 120)}` : ""}`;
259
+ });
260
+ // Generate AI brief from agent activity
261
+ let brief = "";
262
+ if (actions.length > 0 && (process.env.ANTHROPIC_API_KEY || process.env.BANKR_API_KEY)) {
263
+ const activitySummary = actions.slice(0, 30).map((act) => {
254
264
  const who = act.agent_name ?? act.agent_id ?? "agent";
255
265
  const what = act.action_type ?? act.type ?? "action";
256
266
  const content = act.content ?? act.text ?? "";
257
- lines.push(`• **${who}** [${what}]${content ? `: ${String(content).slice(0, 80)}` : ""}`);
267
+ return `${who} [${what}]: ${String(content).slice(0, 150)}`;
268
+ }).join("\n");
269
+ try {
270
+ brief = await (0, llm_js_1.callLLM)("You are a market intelligence analyst. Given a MiroShark multi-agent simulation log, extract: 1) Key market sentiment, 2) Dominant narrative, 3) Top 3 agent behaviors, 4) Outlook. Be concise and direct — max 150 words.", `Simulation activity log:\n${activitySummary}`, 400);
271
+ }
272
+ catch {
273
+ // brief stays empty — non-critical
274
+ }
275
+ }
276
+ // Auto-save to vault if key is available
277
+ const savedToVault = false;
278
+ if (brief) {
279
+ try {
280
+ await (0, convex_js_1.callConvex)("/vault/save", "POST", {
281
+ key: `miroshark-${simId.slice(0, 8)}`,
282
+ value: brief,
283
+ tags: ["miroshark", "simulation", "research"],
284
+ }, "vault_save");
258
285
  }
286
+ catch {
287
+ // non-critical
288
+ }
289
+ }
290
+ const lines = [
291
+ `**MiroShark \`${simId}\`** — ${runnerStatus}`,
292
+ `Rounds: ${rounds} · Actions: ${totalActions} agents`,
293
+ "",
294
+ ];
295
+ if (brief) {
296
+ lines.push("**🧠 AI Brief:**", brief, "");
297
+ }
298
+ if (feed.length > 0) {
299
+ lines.push(`**Agent Feed** (${Math.min(actions.length, 20)} of ${totalActions}):`);
300
+ lines.push(...feed);
301
+ }
302
+ if (brief) {
303
+ lines.push("", `_Findings auto-saved to vault as \`miroshark-${simId.slice(0, 8)}\`_`);
259
304
  }
260
- lines.push("", `Full transcript: \`miroshark_status\` returns results above.`);
261
305
  return { content: [{ type: "text", text: lines.join("\n") }] };
262
306
  }
263
307
  // Fallback: unknown state
@@ -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
  }
@@ -4,7 +4,7 @@ exports.VAULT_TOOLS = void 0;
4
4
  exports.handleVaultTool = handleVaultTool;
5
5
  const zod_1 = require("zod");
6
6
  const convex_js_1 = require("../convex.js");
7
- const VAULT_TYPES = ["research", "execution", "workflow", "prompt", "file", "memory"];
7
+ const VAULT_TYPES = ["research", "execution", "workflow", "prompt", "file", "memory", "credential"];
8
8
  const LINK_RELATIONS = ["references", "derived_from", "supersedes", "related", "continues"];
9
9
  exports.VAULT_TOOLS = [
10
10
  {
@@ -106,6 +106,93 @@ exports.VAULT_TOOLS = [
106
106
  required: [],
107
107
  },
108
108
  },
109
+ {
110
+ name: "vault_remember",
111
+ description: "One-liner: save anything to Noel-Vault without specifying type or title. " +
112
+ "Just pass content — Noel infers the title and saves as a memory entry. " +
113
+ "Use this for quick notes, preferences, decisions, and things to remember across sessions. " +
114
+ "Example: vault_remember content: 'User prefers low-risk DeFi, confirmed multiple times'",
115
+ inputSchema: {
116
+ type: "object",
117
+ properties: {
118
+ content: { type: "string", description: "What to remember — plain text, markdown, or a short note" },
119
+ title: { type: "string", description: "Optional title. Auto-inferred from content if omitted." },
120
+ tags: { type: "array", items: { type: "string" }, description: "Optional tags for later retrieval" },
121
+ },
122
+ required: ["content"],
123
+ },
124
+ },
125
+ {
126
+ name: "vault_context",
127
+ description: "Load relevant vault entries for a topic and return them as formatted context ready to inject into a prompt. " +
128
+ "Call this at the start of any research or analysis task to prime your AI with all past knowledge on the topic. " +
129
+ "Returns full content of matching entries, filtered by relevance. Credentials are never included.",
130
+ inputSchema: {
131
+ type: "object",
132
+ properties: {
133
+ topic: { type: "string", description: "Topic or query to load context for, e.g. 'ETH DeFi strategy' or 'BTC market thesis'" },
134
+ limit: { type: "number", description: "Max entries to return (default 8, max 20)" },
135
+ },
136
+ required: ["topic"],
137
+ },
138
+ },
139
+ {
140
+ name: "vault_store_credential",
141
+ description: "Securely store an API key, token, or secret in your vault. " +
142
+ "Credentials are stored under type=credential and are excluded from normal search and export. " +
143
+ "Use this to keep API keys organized and accessible across agent sessions.",
144
+ inputSchema: {
145
+ type: "object",
146
+ properties: {
147
+ name: { type: "string", description: "Credential name, e.g. 'ALCHEMY_API_KEY', 'TELEGRAM_BOT_TOKEN'" },
148
+ value: { type: "string", description: "The secret value to store" },
149
+ description: { type: "string", description: "Optional note about this credential — what it's for, expiry, etc." },
150
+ },
151
+ required: ["name", "value"],
152
+ },
153
+ },
154
+ {
155
+ name: "vault_get_credential",
156
+ description: "Retrieve a stored credential from the vault by name. " +
157
+ "Only returns credentials owned by the authenticated user.",
158
+ inputSchema: {
159
+ type: "object",
160
+ properties: {
161
+ name: { type: "string", description: "Credential name as used in vault_store_credential" },
162
+ },
163
+ required: ["name"],
164
+ },
165
+ },
166
+ {
167
+ name: "vault_publish",
168
+ description: "Publish a vault entry to the community vault so other Noelclaw users can discover and fork it. " +
169
+ "Credentials are never published — only research, prompts, workflows, memory, and execution entries. " +
170
+ "You can also unpublish by setting isPublic to false.",
171
+ inputSchema: {
172
+ type: "object",
173
+ properties: {
174
+ key: { type: "string", description: "Entry key to publish or unpublish" },
175
+ isPublic: { type: "boolean", description: "true to publish, false to unpublish (default true)" },
176
+ authorName: { type: "string", description: "Display name shown to the community (default: Anonymous)" },
177
+ },
178
+ required: ["key"],
179
+ },
180
+ },
181
+ {
182
+ name: "vault_explore",
183
+ description: "Browse the community vault — public entries shared by all Noelclaw users. " +
184
+ "Discover research, prompts, and workflows published by the community. " +
185
+ "Use vault_save to fork any entry into your own vault.",
186
+ inputSchema: {
187
+ type: "object",
188
+ properties: {
189
+ type: { type: "string", enum: ["research", "execution", "workflow", "prompt", "file", "memory"], description: "Filter by entry type" },
190
+ search: { type: "string", description: "Search query to filter community entries" },
191
+ limit: { type: "number", description: "Max entries to return (default 20)" },
192
+ },
193
+ required: [],
194
+ },
195
+ },
109
196
  ];
110
197
  // ─── Zod schemas ─────────────────────────────────────────────────────────────
111
198
  const SaveSchema = zod_1.z.object({
@@ -134,6 +221,12 @@ const SearchSchema = zod_1.z.object({
134
221
  const HistorySchema = zod_1.z.object({ key: zod_1.z.string().min(1) });
135
222
  const DiffSchema = zod_1.z.object({ key: zod_1.z.string().min(1), fromVersion: zod_1.z.number(), toVersion: zod_1.z.number() });
136
223
  const ExportSchema = zod_1.z.object({ type: zod_1.z.enum(VAULT_TYPES).optional() });
224
+ const RememberSchema = zod_1.z.object({ content: zod_1.z.string().min(1), title: zod_1.z.string().optional(), tags: zod_1.z.array(zod_1.z.string()).optional() });
225
+ const ContextSchema = zod_1.z.object({ topic: zod_1.z.string().min(1), limit: zod_1.z.number().optional() });
226
+ const StoreCredentialSchema = zod_1.z.object({ name: zod_1.z.string().min(1), value: zod_1.z.string().min(1), description: zod_1.z.string().optional() });
227
+ const GetCredentialSchema = zod_1.z.object({ name: zod_1.z.string().min(1) });
228
+ const PublishSchema = zod_1.z.object({ key: zod_1.z.string().min(1), isPublic: zod_1.z.boolean().optional(), authorName: zod_1.z.string().optional() });
229
+ const ExploreSchema = zod_1.z.object({ type: zod_1.z.enum(["research", "execution", "workflow", "prompt", "file", "memory"]).optional(), search: zod_1.z.string().optional(), limit: zod_1.z.number().optional() });
137
230
  // ─── Helpers ─────────────────────────────────────────────────────────────────
138
231
  function formatBytes(n) {
139
232
  if (!n)
@@ -287,6 +380,110 @@ async function handleVaultTool(name, args) {
287
380
  const rows = entries.map((e) => `**[\`${e.key}\`]** ${e.title} (${e.type} · v${e.version})\n${e.content.slice(0, 500)}${e.content.length > 500 ? "\n…" : ""}`);
288
381
  return { content: [{ type: "text", text: [...header, ...rows.join("\n\n---\n\n").split("\n")].join("\n") }] };
289
382
  }
383
+ case "vault_remember": {
384
+ const parsed = RememberSchema.safeParse(args);
385
+ if (!parsed.success)
386
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
387
+ const { content, title, tags } = parsed.data;
388
+ const inferredTitle = title ?? content.slice(0, 60).replace(/\n/g, " ").trim() + (content.length > 60 ? "…" : "");
389
+ const data = await (0, convex_js_1.callConvex)("/vault/save", "POST", {
390
+ type: "memory",
391
+ title: inferredTitle,
392
+ content,
393
+ tags,
394
+ commitMsg: "vault_remember",
395
+ }, "vault_remember");
396
+ if (data.error)
397
+ return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
398
+ return { content: [{ type: "text", text: `✅ Remembered — key: \`${data.key}\` (v${data.version})\nRetrieve later with: \`vault_context topic: "${inferredTitle.slice(0, 40)}"\`` }] };
399
+ }
400
+ case "vault_context": {
401
+ const parsed = ContextSchema.safeParse(args);
402
+ if (!parsed.success)
403
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
404
+ const params = new URLSearchParams({ q: parsed.data.topic });
405
+ if (parsed.data.limit)
406
+ params.set("limit", String(parsed.data.limit));
407
+ const data = await (0, convex_js_1.callConvex)(`/vault/context?${params}`, "GET", undefined, "vault_context");
408
+ if (data.error)
409
+ return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
410
+ if (!data.context)
411
+ return { content: [{ type: "text", text: `No vault context found for: "${parsed.data.topic}"\nStart building your knowledge base with vault_remember or vault_save.` }] };
412
+ const summary = data.entries.map((e) => `[${e.type}] ${e.title}`).join(", ");
413
+ return { content: [{ type: "text", text: `📚 **Vault Context** loaded for: "${parsed.data.topic}"\nEntries: ${summary}\n\n---\n\n${data.context}` }] };
414
+ }
415
+ case "vault_store_credential": {
416
+ const parsed = StoreCredentialSchema.safeParse(args);
417
+ if (!parsed.success)
418
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
419
+ const data = await (0, convex_js_1.callConvex)("/vault/credential/store", "POST", parsed.data, "vault_store_credential");
420
+ if (data.error)
421
+ return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
422
+ return { content: [{ type: "text", text: `🔐 Credential stored: \`${data.name}\`\nKey: \`${data.key}\`\nRetrieve with: \`vault_get_credential name: "${data.name}"\`` }] };
423
+ }
424
+ case "vault_get_credential": {
425
+ const parsed = GetCredentialSchema.safeParse(args);
426
+ if (!parsed.success)
427
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
428
+ const params = new URLSearchParams({ name: parsed.data.name });
429
+ const data = await (0, convex_js_1.callConvex)(`/vault/credential?${params}`, "GET", undefined, "vault_get_credential");
430
+ if (data.error)
431
+ return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
432
+ const lines = [`🔐 **${data.name}**`, `Value: \`${data.value}\``];
433
+ if (data.description)
434
+ lines.push(`Note: ${data.description}`);
435
+ if (data.storedAt)
436
+ lines.push(`Stored: ${data.storedAt}`);
437
+ return { content: [{ type: "text", text: lines.join("\n") }] };
438
+ }
439
+ case "vault_publish": {
440
+ const parsed = PublishSchema.safeParse(args);
441
+ if (!parsed.success)
442
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
443
+ const { key, isPublic = true, authorName } = parsed.data;
444
+ const endpoint = isPublic ? "/vault/publish" : "/vault/unpublish";
445
+ const data = await (0, convex_js_1.callConvex)(endpoint, "POST", { key, authorName }, "vault_publish");
446
+ if (data.error)
447
+ return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
448
+ return {
449
+ content: [{
450
+ type: "text",
451
+ text: isPublic
452
+ ? `🌐 **Published to community vault**\nKey: \`${key}\`\nOther Noelclaw users can now discover this entry.\nUse \`vault_publish key: "${key}" isPublic: false\` to unpublish.`
453
+ : `🔒 **Unpublished**\nKey: \`${key}\` is now private.`,
454
+ }],
455
+ };
456
+ }
457
+ case "vault_explore": {
458
+ const parsed = ExploreSchema.safeParse(args ?? {});
459
+ if (!parsed.success)
460
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
461
+ const params = new URLSearchParams();
462
+ if (parsed.data.type)
463
+ params.set("type", parsed.data.type);
464
+ if (parsed.data.search)
465
+ params.set("search", parsed.data.search);
466
+ if (parsed.data.limit)
467
+ params.set("limit", String(parsed.data.limit));
468
+ const data = await (0, convex_js_1.callConvex)(`/vault/community?${params}`, "GET", undefined, "vault_explore");
469
+ if (data.error)
470
+ return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
471
+ const entries = data.entries ?? [];
472
+ if (!entries.length)
473
+ return { content: [{ type: "text", text: `No community vault entries found${parsed.data.type ? ` of type '${parsed.data.type}'` : ""}.${parsed.data.search ? ` Try a different search term.` : "\nBe the first to publish with vault_publish!"}` }] };
474
+ const header = `🌐 **Community Vault** — ${entries.length} public entries`;
475
+ const rows = entries.map((e) => [
476
+ `**[${e.type}]** ${e.title} · by ${e.authorName ?? "Anonymous"}`,
477
+ ` \`${e.key}\` · v${e.version} · ${formatDate(e.updatedAt)}`,
478
+ e.content ? ` ${e.content.slice(0, 100)}${e.content.length > 100 ? "…" : ""}` : "",
479
+ ].filter(Boolean).join("\n"));
480
+ return {
481
+ content: [{
482
+ type: "text",
483
+ text: [header, "", ...rows, "", "To fork an entry: `vault_save type: <type> title: <title> content: <content>`"].join("\n"),
484
+ }],
485
+ };
486
+ }
290
487
  default:
291
488
  return null;
292
489
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noelclaw/mcp",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Noelclaw as an MCP skill — persistent memory, multi-agent coordination, scenario simulation, DeFi execution, and Sentinel-gated playbooks.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {