@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.
@@ -1,82 +1,54 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MEMORY_TOOLS = exports.SM_SPACE = void 0;
4
- exports.smFetch = smFetch;
3
+ exports.MEMORY_TOOLS = void 0;
5
4
  exports.syncToSupermemory = syncToSupermemory;
6
5
  exports.searchSupermemory = searchSupermemory;
7
6
  exports.handleMemoryTool = handleMemoryTool;
8
7
  const zod_1 = require("zod");
9
- const SM_BASE = "https://api.supermemory.ai/v3";
10
- exports.SM_SPACE = "noelclaw-vault";
11
- // ─── Supermemory HTTP helpers ────────────────────────────────────────────────
12
- async function smFetch(path, method, body) {
13
- const apiKey = process.env.SUPERMEMORY_API_KEY;
14
- if (!apiKey)
15
- return { ok: false, data: null, error: "SUPERMEMORY_API_KEY not set" };
16
- try {
17
- const res = await fetch(`${SM_BASE}${path}`, {
18
- method,
19
- headers: {
20
- Authorization: `Bearer ${apiKey}`,
21
- "Content-Type": "application/json",
22
- },
23
- body: body ? JSON.stringify(body) : undefined,
24
- signal: AbortSignal.timeout(15000),
25
- });
26
- if (!res.ok) {
27
- const err = await res.text().catch(() => "");
28
- return { ok: false, data: null, error: `${res.status}: ${err.slice(0, 120)}` };
29
- }
30
- return { ok: true, data: await res.json() };
31
- }
32
- catch (err) {
33
- return { ok: false, data: null, error: err.message };
34
- }
35
- }
8
+ const convex_js_1 = require("../convex.js");
9
+ // ─── Helpers (proxied through Convex — server-side Supermemory key) ──────────
36
10
  async function syncToSupermemory(content, metadata, sourceUrl) {
37
- await smFetch("/memories", "POST", {
11
+ await (0, convex_js_1.callConvex)("/memory/add", "POST", {
38
12
  content,
39
13
  metadata,
40
- spaces: [exports.SM_SPACE],
41
14
  ...(sourceUrl ? { sourceUrl } : {}),
42
- });
15
+ }).catch(() => { });
43
16
  }
44
17
  async function searchSupermemory(query, limit = 10) {
45
- const { ok, data } = await smFetch("/search", "POST", {
46
- q: query,
47
- n: limit,
48
- spaces: [exports.SM_SPACE],
49
- returnContext: true,
50
- });
51
- if (!ok || !data?.results)
18
+ try {
19
+ const data = await (0, convex_js_1.callConvex)("/memory/search", "POST", { q: query, n: limit });
20
+ return data?.results ?? [];
21
+ }
22
+ catch {
52
23
  return [];
53
- return data.results;
24
+ }
54
25
  }
55
26
  // ─── Tool definitions ────────────────────────────────────────────────────────
56
27
  exports.MEMORY_TOOLS = [
57
28
  {
58
29
  name: "memory_add",
59
- description: "Add content to Noelclaw's semantic memory layer (powered by Supermemory). " +
60
- "Unlike vault_save, memory_add is quick no versioning, no type required. " +
61
- "Use this for notes, decisions, preferences, URLs, or anything you want to " +
62
- "find later with natural language search. Memory is indexed semantically — " +
63
- "'what did I say about ETH yield?' will find it even without exact keywords.",
30
+ description: "Add content to your Noelclaw semantic memory no setup needed, no extra API keys. " +
31
+ "Unlike vault_save, memory_add is instant: no versioning, no type required. " +
32
+ "Use for notes, decisions, preferences, or anything you want to find later with natural language. " +
33
+ "Pass sourceUrl to fetch and index any web page, GitHub repo, or Notion page automatically — " +
34
+ "searchable in ~30s. Memory is indexed semantically — 'what did I say about ETH yield?' " +
35
+ "will find it even without exact keywords.",
64
36
  inputSchema: {
65
37
  type: "object",
66
38
  properties: {
67
- content: { type: "string", description: "Content to remember — text, markdown, or a note" },
39
+ content: { type: "string", description: "Content to remember — text, markdown, or a note. Use a short title if providing sourceUrl." },
68
40
  title: { type: "string", description: "Optional title for this memory" },
69
41
  tags: { type: "array", items: { type: "string" }, description: "Tags for grouping" },
70
- sourceUrl: { type: "string", description: "Optional URL if provided, Supermemory also fetches and indexes the page content" },
42
+ sourceUrl: { type: "string", description: "URL to fetch and index automatically (GitHub, Notion, web page, etc.). Content becomes searchable in ~30s." },
71
43
  },
72
44
  required: ["content"],
73
45
  },
74
46
  },
75
47
  {
76
48
  name: "memory_search",
77
- description: "Semantic search across all Noelclaw memories. Unlike keyword search, this " +
78
- "understands meaning — 'low risk crypto yield' will match 'conservative DeFi strategies'. " +
79
- "Also searches memories synced from vault_save and vault_remember automatically.",
49
+ description: "Semantic search across all your Noelclaw memories. Understands meaning, not just keywords — " +
50
+ "'low risk crypto yield' matches 'conservative DeFi strategies'. " +
51
+ "Also searches memories auto-synced from vault_save.",
80
52
  inputSchema: {
81
53
  type: "object",
82
54
  properties: {
@@ -88,10 +60,9 @@ exports.MEMORY_TOOLS = [
88
60
  },
89
61
  {
90
62
  name: "memory_context",
91
- description: "Retrieve the most semantically relevant memories for a topic and format them " +
92
- "as ready-to-inject context. Use at the start of research tasks to prime with " +
93
- "everything stored about a topic. Smarter than vault_context uses vector search, " +
94
- "not just keyword matching.",
63
+ description: "Retrieve the most semantically relevant memories for a topic, formatted as AI-ready context. " +
64
+ "Use at the start of research tasks to prime with everything stored about a topic. " +
65
+ "Uses vector search finds semantically related content, not just exact keyword matches.",
95
66
  inputSchema: {
96
67
  type: "object",
97
68
  properties: {
@@ -103,8 +74,8 @@ exports.MEMORY_TOOLS = [
103
74
  },
104
75
  {
105
76
  name: "memory_profile",
106
- description: "Show your semantic memory stats — how many memories, recent additions, and " +
107
- "connected data sources. Useful for auditing what Noelclaw knows about you.",
77
+ description: "Show your semantic memory stats — total memories stored, your memory space, and connected sources. " +
78
+ "Useful for auditing what Noelclaw knows about you.",
108
79
  inputSchema: {
109
80
  type: "object",
110
81
  properties: {},
@@ -112,33 +83,44 @@ exports.MEMORY_TOOLS = [
112
83
  },
113
84
  },
114
85
  {
115
- name: "memory_connect",
116
- description: "Connect an external data source to Noelclaw's semantic memory, or add content " +
117
- "from a specific URL. Supported sources:\n" +
118
- "- url: fetch and index any web page or GitHub file\n" +
119
- "- github: index a GitHub repository README/docs\n" +
120
- "- notion: add content from a public Notion page URL\n" +
121
- "- manual: paste content directly (same as memory_add with sourceUrl)\n" +
122
- "For Google Drive, Gmail, and full Notion sync, connect via your Noelclaw dashboard.",
86
+ name: "memory_list",
87
+ description: "List your most recent Noelclaw memories without a search query. " +
88
+ "Useful to browse what's stored or audit before clearing. " +
89
+ "Sorted by most recently added.",
90
+ inputSchema: {
91
+ type: "object",
92
+ properties: {
93
+ limit: { type: "number", description: "Max memories to return (default 20)" },
94
+ tag: { type: "string", description: "Optional: filter by tag" },
95
+ },
96
+ required: [],
97
+ },
98
+ },
99
+ {
100
+ name: "memory_delete",
101
+ description: "Delete a specific memory by its ID. Get IDs from memory_search or memory_list results. " +
102
+ "This permanently removes the memory from your semantic store.",
123
103
  inputSchema: {
124
104
  type: "object",
125
105
  properties: {
126
- source: {
127
- type: "string",
128
- enum: ["url", "github", "notion", "manual"],
129
- description: "Source type",
130
- },
131
- url: {
132
- type: "string",
133
- description: "URL to fetch and index (required for url/github/notion sources)",
134
- },
135
- content: {
136
- type: "string",
137
- description: "Direct content (for source: manual)",
138
- },
139
- title: { type: "string", description: "Title for this memory" },
106
+ id: { type: "string", description: "Memory ID to delete (from memory_search or memory_list results)" },
140
107
  },
141
- required: ["source"],
108
+ required: ["id"],
109
+ },
110
+ },
111
+ {
112
+ name: "memory_insight",
113
+ description: "Get a full intelligence report on any topic — combines semantic memory AND vault entries, " +
114
+ "then identifies knowledge gaps and suggests next actions. " +
115
+ "Use this before starting any research or trade decision to see everything Noelclaw already knows. " +
116
+ "Returns: confidence level, what you know, coverage timeline, gaps, and recommended next steps.",
117
+ inputSchema: {
118
+ type: "object",
119
+ properties: {
120
+ topic: { type: "string", description: "Topic to analyze — token, protocol, strategy, or any concept" },
121
+ depth: { type: "string", enum: ["quick", "standard", "deep"], description: "How many sources to pull (default: standard)" },
122
+ },
123
+ required: ["topic"],
142
124
  },
143
125
  },
144
126
  ];
@@ -157,42 +139,40 @@ const ContextSchema = zod_1.z.object({
157
139
  topic: zod_1.z.string().min(1),
158
140
  limit: zod_1.z.number().optional(),
159
141
  });
160
- const ConnectSchema = zod_1.z.object({
161
- source: zod_1.z.enum(["url", "github", "notion", "manual"]),
162
- url: zod_1.z.string().url().optional(),
163
- content: zod_1.z.string().optional(),
164
- title: zod_1.z.string().optional(),
142
+ const ListSchema = zod_1.z.object({
143
+ limit: zod_1.z.number().optional(),
144
+ tag: zod_1.z.string().optional(),
145
+ });
146
+ const DeleteMemSchema = zod_1.z.object({ id: zod_1.z.string().min(1) });
147
+ const InsightSchema = zod_1.z.object({
148
+ topic: zod_1.z.string().min(1),
149
+ depth: zod_1.z.enum(["quick", "standard", "deep"]).optional(),
165
150
  });
166
151
  // ─── Handler ─────────────────────────────────────────────────────────────────
167
152
  async function handleMemoryTool(name, args) {
168
- const apiKey = process.env.SUPERMEMORY_API_KEY;
169
153
  switch (name) {
170
154
  case "memory_add": {
171
155
  const parsed = AddSchema.safeParse(args);
172
156
  if (!parsed.success)
173
157
  return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
174
- if (!apiKey)
175
- return { content: [{ type: "text", text: "⚠️ SUPERMEMORY_API_KEY not configured.\nSet it in your MCP env to enable semantic memory.\nGet a free key at supermemory.ai" }], isError: true };
176
158
  const { content, title, tags, sourceUrl } = parsed.data;
177
- const { ok, data, error } = await smFetch("/memories", "POST", {
159
+ const data = await (0, convex_js_1.callConvex)("/memory/add", "POST", {
178
160
  content,
179
- metadata: { title, tags, source: "noelclaw-mcp", addedAt: Date.now() },
180
- spaces: [exports.SM_SPACE],
161
+ metadata: { title, tags, source: "memory_add", addedAt: Date.now() },
181
162
  ...(sourceUrl ? { sourceUrl } : {}),
182
- });
183
- if (!ok)
184
- return { content: [{ type: "text", text: `Error adding to memory: ${error}` }], isError: true };
185
- const memId = data?.id ?? data?.memoryId ?? "saved";
163
+ }).catch((err) => ({ error: err.message }));
164
+ if (data?.error)
165
+ return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
186
166
  return {
187
167
  content: [{
188
168
  type: "text",
189
169
  text: [
190
- `🧠 **Memory added** — ID: \`${memId}\``,
170
+ `🧠 **Memory added** — ID: \`${data?.id ?? "saved"}\``,
191
171
  title ? `Title: ${title}` : "",
192
- sourceUrl ? `Source: ${sourceUrl}` : "",
172
+ sourceUrl ? `Source: ${sourceUrl} (indexing in background…)` : "",
193
173
  tags?.length ? `Tags: ${tags.join(", ")}` : "",
194
174
  ``,
195
- `Find it later with: \`memory_search query: "${(title ?? content).slice(0, 40)}"\``,
175
+ `Find it with: \`memory_search query: "${(title ?? content).slice(0, 40)}"\``,
196
176
  ].filter(Boolean).join("\n"),
197
177
  }],
198
178
  };
@@ -201,8 +181,6 @@ async function handleMemoryTool(name, args) {
201
181
  const parsed = SearchSchema.safeParse(args);
202
182
  if (!parsed.success)
203
183
  return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
204
- if (!apiKey)
205
- return { content: [{ type: "text", text: "⚠️ SUPERMEMORY_API_KEY not configured. Set it in your MCP env to enable semantic memory." }], isError: true };
206
184
  const { query, limit = 10 } = parsed.data;
207
185
  const results = await searchSupermemory(query, limit);
208
186
  if (!results.length)
@@ -223,12 +201,10 @@ async function handleMemoryTool(name, args) {
223
201
  const parsed = ContextSchema.safeParse(args);
224
202
  if (!parsed.success)
225
203
  return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
226
- if (!apiKey)
227
- return { content: [{ type: "text", text: "⚠️ SUPERMEMORY_API_KEY not configured. Set it in your MCP env to enable semantic memory." }], isError: true };
228
204
  const { topic, limit = 8 } = parsed.data;
229
205
  const results = await searchSupermemory(topic, limit);
230
206
  if (!results.length)
231
- return { content: [{ type: "text", text: `No semantic context found for: "${topic}"\nBuild your memory base with vault_save, vault_remember, or memory_add.` }] };
207
+ return { content: [{ type: "text", text: `No semantic context found for: "${topic}"\nBuild your memory base with vault_save or memory_add.` }] };
232
208
  const contextParts = results.map((r, i) => {
233
209
  const title = r.metadata?.title ? `### ${r.metadata.title}` : `### Memory ${i + 1}`;
234
210
  return `${title}\n${r.content}`;
@@ -249,89 +225,141 @@ async function handleMemoryTool(name, args) {
249
225
  };
250
226
  }
251
227
  case "memory_profile": {
252
- if (!apiKey)
253
- return { content: [{ type: "text", text: "⚠️ SUPERMEMORY_API_KEY not configured.\n\nSet `SUPERMEMORY_API_KEY` in your MCP env vars.\nGet a free key at supermemory.ai" }], isError: true };
254
- const { ok, data, error } = await smFetch(`/memories?spaces=${exports.SM_SPACE}&limit=1`, "GET");
255
- if (!ok)
256
- return { content: [{ type: "text", text: `Error fetching profile: ${error}` }], isError: true };
257
- const count = data?.total ?? data?.count ?? "unknown";
228
+ const data = await (0, convex_js_1.callConvex)("/memory/profile", "GET").catch(() => null);
229
+ const total = data?.total ?? 0;
230
+ const status = data?.status ?? "unknown";
231
+ const space = data?.space ?? "—";
258
232
  return {
259
233
  content: [{
260
234
  type: "text",
261
235
  text: [
262
- `🧠 **Noelclaw Semantic Memory Profile**`,
236
+ `🧠 **Noelclaw Semantic Memory**`,
263
237
  ``,
264
- `Space: \`${exports.SM_SPACE}\``,
265
- `Total memories: **${count}**`,
266
- `Status: ✅ Connected (Supermemory)`,
238
+ `Space: \`${space}\``,
239
+ `Total memories: **${total}**`,
240
+ `Status: ${status === "ok" ? "Active" : status === "not_configured" ? "⏳ Setting up" : "⚠️ " + status}`,
267
241
  ``,
268
- `**Data sources:**`,
269
- `• vault_save / vault_remember auto-synced ✅`,
270
- `• memory_add direct input ✅`,
271
- `• memory_connectURL ingestion ✅`,
272
- `• Google Drive / Gmail / Notion — connect via noelclaw.com dashboard`,
242
+ `**Auto-synced sources:**`,
243
+ `• vault_save — ✅`,
244
+ `• memory_add (URL indexing) ✅`,
245
+ `• Google Drive / Gmail / Notion connect at noelclaw.com`,
273
246
  ``,
274
- `**Capabilities:**`,
275
- `• Semantic search (vector similarity)`,
276
- `• 81.6% LongMemEval accuracy`,
277
- `• Context injection for AI tasks`,
247
+ `**Capabilities:** Semantic search · Vector context · 81.6% LongMemEval`,
278
248
  ].join("\n"),
279
249
  }],
280
250
  };
281
251
  }
282
- case "memory_connect": {
283
- const parsed = ConnectSchema.safeParse(args);
252
+ case "memory_list": {
253
+ const parsed = ListSchema.safeParse(args ?? {});
284
254
  if (!parsed.success)
285
255
  return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
286
- if (!apiKey)
287
- return { content: [{ type: "text", text: "⚠️ SUPERMEMORY_API_KEY not configured. Set it in your MCP env to enable semantic memory." }], isError: true };
288
- const { source, url, content, title } = parsed.data;
289
- if (source === "manual") {
290
- if (!content)
291
- return { content: [{ type: "text", text: "content is required for source: manual" }], isError: true };
292
- const { ok, data, error } = await smFetch("/memories", "POST", {
293
- content,
294
- metadata: { title, source: "manual", addedAt: Date.now() },
295
- spaces: [exports.SM_SPACE],
296
- });
297
- if (!ok)
298
- return { content: [{ type: "text", text: `Error: ${error}` }], isError: true };
299
- return { content: [{ type: "text", text: `✅ Memory connected — ID: \`${data?.id ?? "saved"}\`` }] };
256
+ const { limit = 20, tag } = parsed.data;
257
+ const data = await (0, convex_js_1.callConvex)("/memory/list", "POST", { n: limit, tag }).catch((err) => ({ error: err.message }));
258
+ if (data?.error)
259
+ return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
260
+ const results = data?.results ?? [];
261
+ if (!results.length)
262
+ return { content: [{ type: "text", text: `No memories stored yet. Use \`memory_add\` to start building your knowledge base.` }] };
263
+ const header = `🧠 **Memories** (${results.length} shown${tag ? `, tag: ${tag}` : ""})`;
264
+ const rows = results.map((r, i) => {
265
+ const title = r.metadata?.title ?? "";
266
+ const preview = r.content.slice(0, 100).replace(/\n/g, " ");
267
+ return `${i + 1}. \`${r.id}\`${title ? ` **${title}**` : ""}\n ${preview}${r.content.length > 100 ? "…" : ""}`;
268
+ });
269
+ return { content: [{ type: "text", text: [header, "", ...rows].join("\n") }] };
270
+ }
271
+ case "memory_delete": {
272
+ const parsed = DeleteMemSchema.safeParse(args);
273
+ if (!parsed.success)
274
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
275
+ const data = await (0, convex_js_1.callConvex)("/memory/delete", "POST", { id: parsed.data.id }).catch((err) => ({ error: err.message }));
276
+ if (data?.error)
277
+ return { content: [{ type: "text", text: `Error: ${data.error}` }], isError: true };
278
+ return { content: [{ type: "text", text: `🗑️ Memory deleted: \`${parsed.data.id}\`` }] };
279
+ }
280
+ case "memory_insight": {
281
+ const parsed = InsightSchema.safeParse(args);
282
+ if (!parsed.success)
283
+ return { content: [{ type: "text", text: `Invalid input: ${parsed.error.issues[0].message}` }], isError: true };
284
+ const { topic, depth = "standard" } = parsed.data;
285
+ const memLimit = depth === "deep" ? 15 : depth === "quick" ? 5 : 8;
286
+ // Pull from semantic memory and vault in parallel
287
+ const [memResults, vaultData] = await Promise.all([
288
+ searchSupermemory(topic, memLimit),
289
+ (0, convex_js_1.callConvex)(`/vault/search?q=${encodeURIComponent(topic)}&limit=6`, "GET", undefined, "memory_insight").catch(() => ({ results: [] })),
290
+ ]);
291
+ const vaultResults = vaultData.results ?? [];
292
+ const total = memResults.length + vaultResults.length;
293
+ if (!total) {
294
+ return {
295
+ content: [{
296
+ type: "text",
297
+ text: [
298
+ `🔮 **Intelligence Report: "${topic}"**`,
299
+ ``,
300
+ `No knowledge found yet.`,
301
+ ``,
302
+ `**Start building:**`,
303
+ `• \`swarm_research topic: "${topic}"\` — run deep research now`,
304
+ `• \`memory_add content: "..." \` — add a manual note`,
305
+ `• \`swarm_watch topic: "${topic}"\` — start monitoring`,
306
+ ].join("\n"),
307
+ }],
308
+ };
300
309
  }
301
- if (!url)
302
- return { content: [{ type: "text", text: `url is required for source: ${source}` }], isError: true };
303
- const { ok, data, error } = await smFetch("/memories", "POST", {
304
- content: title ?? "",
305
- sourceUrl: url,
306
- metadata: { title, source, connectedAt: Date.now() },
307
- spaces: [exports.SM_SPACE],
310
+ // Confidence tier
311
+ const confidence = total >= 10 ? "🟢 High" : total >= 4 ? "🟡 Medium" : "🔴 Low";
312
+ // Timeline from metadata timestamps
313
+ const timestamps = memResults.map(r => r.metadata?.addedAt).filter(Boolean);
314
+ const oldest = timestamps.length ? Math.min(...timestamps) : null;
315
+ const newest = timestamps.length ? Math.max(...timestamps) : null;
316
+ const daysSinceUpdate = newest ? Math.round((Date.now() - newest) / 86400000) : null;
317
+ // Knowledge summary lines
318
+ const memLines = memResults.slice(0, 6).map(r => {
319
+ const title = r.metadata?.title ?? r.content.slice(0, 70).replace(/\n/g, " ");
320
+ const score = r.score != null ? ` [${(r.score * 100).toFixed(0)}%]` : "";
321
+ return ` •${score} ${title}`;
308
322
  });
309
- if (!ok)
310
- return { content: [{ type: "text", text: `Error connecting source: ${error}` }], isError: true };
311
- const sourceLabels = {
312
- url: "Web page",
313
- github: "GitHub repo",
314
- notion: "Notion page",
315
- };
316
- return {
317
- content: [{
318
- type: "text",
319
- text: [
320
- `✅ **${sourceLabels[source] ?? source} connected**`,
321
- `URL: ${url}`,
322
- `Memory ID: \`${data?.id ?? "queued"}\``,
323
- ``,
324
- `Supermemory is fetching and indexing the content in the background.`,
325
- `Search it in ~30s with: \`memory_search query: "${title ?? url.split("/").pop()}"\``,
326
- ``,
327
- source === "github"
328
- ? `💡 Tip: For full repo sync (all files), connect via the Noelclaw dashboard at noelclaw.com`
329
- : source === "notion"
330
- ? `💡 Tip: For full Notion workspace sync, connect via the Noelclaw dashboard at noelclaw.com`
331
- : "",
332
- ].filter(Boolean).join("\n"),
333
- }],
334
- };
323
+ const vaultLines = vaultResults.slice(0, 4).map((r) => ` • [vault/${r.type}] ${r.title} — v${r.version}`);
324
+ // Gap analysis
325
+ const gaps = [];
326
+ if (daysSinceUpdate !== null && daysSinceUpdate > 7) {
327
+ gaps.push(`Stale data — last update ${daysSinceUpdate} day${daysSinceUpdate !== 1 ? "s" : ""} ago`);
328
+ }
329
+ if (!vaultResults.some((r) => r.type === "research")) {
330
+ gaps.push("No formal research saved — only informal notes exist");
331
+ }
332
+ if (memResults.length < 3) {
333
+ gaps.push("Thin coverage — fewer than 3 semantic memories on this topic");
334
+ }
335
+ if (!vaultResults.some((r) => r.type === "execution")) {
336
+ gaps.push("No execution history no trades or actions logged");
337
+ }
338
+ const lines = [
339
+ `🔮 **Intelligence Report: "${topic}"**`,
340
+ `Confidence: ${confidence} · ${memResults.length} semantic memories · ${vaultResults.length} vault entries`,
341
+ oldest ? `Coverage: ${new Date(oldest).toLocaleDateString("en-US")} – ${daysSinceUpdate === 0 ? "today" : daysSinceUpdate !== null ? `${daysSinceUpdate}d ago` : "unknown"}` : "",
342
+ ``,
343
+ `**What you know:**`,
344
+ ...memLines,
345
+ ...(vaultLines.length ? ["", "**Vault entries:**", ...vaultLines] : []),
346
+ ``,
347
+ ];
348
+ if (gaps.length) {
349
+ lines.push(`**⚠️ Knowledge gaps:**`);
350
+ gaps.forEach(g => lines.push(` • ${g}`));
351
+ lines.push("");
352
+ }
353
+ lines.push(`**Suggested actions:**`);
354
+ if (gaps.some(g => g.includes("research") || g.includes("Stale"))) {
355
+ lines.push(`• \`swarm_research topic: "${topic}"\` — refresh with deep research`);
356
+ }
357
+ if (gaps.some(g => g.includes("Stale"))) {
358
+ lines.push(`• \`trigger_agent agentId: "market-monitor" params: { token: "${topic.split(" ")[0].toUpperCase()}" }\``);
359
+ }
360
+ lines.push(`• \`memory_context topic: "${topic}"\` — inject full context into your next prompt`);
361
+ lines.push(`• \`swarm_watch topic: "${topic}"\` — monitor this topic continuously`);
362
+ return { content: [{ type: "text", text: lines.filter(l => l !== undefined).join("\n") }] };
335
363
  }
336
364
  default:
337
365
  return null;
@@ -179,6 +179,9 @@ async function handleMirosharkTool(name, args) {
179
179
  return { content: [{ type: "text", text: "simulation_id is required" }], isError: true };
180
180
  }
181
181
  const simId = a.simulation_id.trim();
182
+ if (!/^[a-zA-Z0-9_-]{5,100}$/.test(simId)) {
183
+ return { content: [{ type: "text", text: "Invalid simulation_id format." }], isError: true };
184
+ }
182
185
  try {
183
186
  // Check run status first
184
187
  const runStatus = await miroJson(`/miroshark/api/simulation/${simId}/run-status`, "GET").catch(() => ({ runner_status: "idle" }));
@@ -273,15 +276,20 @@ async function handleMirosharkTool(name, args) {
273
276
  // brief stays empty — non-critical
274
277
  }
275
278
  }
276
- // Auto-save to vault if key is available
277
- const savedToVault = false;
279
+ // Auto-save to vault if brief was generated
280
+ let savedToVault = false;
278
281
  if (brief) {
279
282
  try {
280
283
  await (0, convex_js_1.callConvex)("/vault/save", "POST", {
284
+ type: "research",
285
+ title: `MiroShark: ${a.scenario?.slice(0, 80) ?? simId}`,
286
+ content: brief,
281
287
  key: `miroshark-${simId.slice(0, 8)}`,
282
- value: brief,
288
+ agentId: "miroshark",
283
289
  tags: ["miroshark", "simulation", "research"],
290
+ commitMsg: "miroshark auto-save",
284
291
  }, "vault_save");
292
+ savedToVault = true;
285
293
  }
286
294
  catch {
287
295
  // non-critical
@@ -299,7 +307,7 @@ async function handleMirosharkTool(name, args) {
299
307
  lines.push(`**Agent Feed** (${Math.min(actions.length, 20)} of ${totalActions}):`);
300
308
  lines.push(...feed);
301
309
  }
302
- if (brief) {
310
+ if (savedToVault) {
303
311
  lines.push("", `_Findings auto-saved to vault as \`miroshark-${simId.slice(0, 8)}\`_`);
304
312
  }
305
313
  return { content: [{ type: "text", text: lines.join("\n") }] };
@@ -326,6 +334,9 @@ async function handleMirosharkTool(name, args) {
326
334
  return { content: [{ type: "text", text: "simulation_id is required" }], isError: true };
327
335
  }
328
336
  const simId = a.simulation_id.trim();
337
+ if (!/^[a-zA-Z0-9_-]{5,100}$/.test(simId)) {
338
+ return { content: [{ type: "text", text: "Invalid simulation_id format." }], isError: true };
339
+ }
329
340
  try {
330
341
  await miroJson(`/miroshark/api/simulation/${simId}/stop`, "POST", {});
331
342
  return { content: [{ type: "text", text: `⏹️ Simulation \`${simId}\` stopped.` }] };