@crowley/rag-mcp 1.3.0 → 1.6.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.
@@ -45,6 +45,8 @@ export const DEFAULT_SKIP_TOOLS = new Set([
45
45
  ]);
46
46
  export class ContextEnricher {
47
47
  config;
48
+ cache = new Map();
49
+ static CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
48
50
  constructor(config = {}) {
49
51
  this.config = {
50
52
  enrichableTools: config.enrichableTools ?? DEFAULT_ENRICHABLE_TOOLS,
@@ -54,6 +56,12 @@ export class ContextEnricher {
54
56
  timeoutMs: config.timeoutMs ?? 2000,
55
57
  };
56
58
  }
59
+ /**
60
+ * Clear enrichment cache (call on session end).
61
+ */
62
+ clearCache() {
63
+ this.cache.clear();
64
+ }
57
65
  /**
58
66
  * Before hook: auto-recall relevant memories/patterns/ADRs.
59
67
  * Returns a context prefix string or null if nothing relevant found.
@@ -68,11 +76,29 @@ export class ContextEnricher {
68
76
  const query = this.extractQuery(args);
69
77
  if (!query)
70
78
  return null;
79
+ // Check per-session cache
80
+ const cacheKey = `${ctx.activeSessionId || "no-session"}:${query.slice(0, 100)}`;
81
+ const cached = this.cache.get(cacheKey);
82
+ if (cached && Date.now() < cached.expiresAt) {
83
+ return cached.result;
84
+ }
71
85
  try {
72
86
  const memories = await this.recallWithTimeout(query, ctx);
73
- if (memories.length === 0)
74
- return null;
75
- return this.formatContext(memories);
87
+ const result = memories.length === 0 ? null : this.formatContext(memories);
88
+ // Store in cache
89
+ this.cache.set(cacheKey, {
90
+ result,
91
+ expiresAt: Date.now() + ContextEnricher.CACHE_TTL_MS,
92
+ });
93
+ // Evict expired entries lazily (every 50 calls)
94
+ if (this.cache.size > 100) {
95
+ const now = Date.now();
96
+ for (const [key, entry] of this.cache) {
97
+ if (now > entry.expiresAt)
98
+ this.cache.delete(key);
99
+ }
100
+ }
101
+ return result;
76
102
  }
77
103
  catch {
78
104
  // Enrichment should never break tool calls
@@ -150,8 +176,10 @@ export class ContextEnricher {
150
176
  const controller = new AbortController();
151
177
  const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs);
152
178
  try {
153
- // Parallel recall: general memories + decisions/ADRs
154
- const [memoriesRes, decisionsRes] = await Promise.all([
179
+ // Parallel recall: general memories + decisions/ADRs + LTM (when enabled)
180
+ const graphRecallEnabled = process.env.GRAPH_RECALL_ENABLED === "true";
181
+ const consolidationEnabled = process.env.CONSOLIDATION_ENABLED === "true";
182
+ const recalls = [
155
183
  ctx.api
156
184
  .post("/api/memory/recall-durable", {
157
185
  projectName: ctx.projectName,
@@ -168,13 +196,26 @@ export class ContextEnricher {
168
196
  type: "decision",
169
197
  }, { signal: controller.signal })
170
198
  .catch(() => null),
171
- ]);
199
+ ];
200
+ // Phase 2+4: also recall from LTM (episodic+semantic with Ebbinghaus decay)
201
+ if (consolidationEnabled) {
202
+ recalls.push(ctx.api
203
+ .post("/api/memory/recall-ltm", {
204
+ projectName: ctx.projectName,
205
+ query,
206
+ limit: this.config.maxAutoRecall,
207
+ graphRecall: graphRecallEnabled,
208
+ }, { signal: controller.signal })
209
+ .catch(() => null));
210
+ }
211
+ const [memoriesRes, decisionsRes, ltmRes] = await Promise.all(recalls);
172
212
  const memories = [];
173
213
  const seenIds = new Set();
174
214
  // Process general memories
175
215
  if (memoriesRes?.data?.memories) {
176
216
  for (const m of memoriesRes.data.memories) {
177
- if (m.score >= this.config.minRelevance && !seenIds.has(m.memory?.id)) {
217
+ if (m.score >= this.config.minRelevance &&
218
+ !seenIds.has(m.memory?.id)) {
178
219
  seenIds.add(m.memory?.id);
179
220
  memories.push({
180
221
  type: m.memory?.type || "note",
@@ -187,7 +228,8 @@ export class ContextEnricher {
187
228
  // Process decisions/ADRs
188
229
  if (decisionsRes?.data?.memories) {
189
230
  for (const m of decisionsRes.data.memories) {
190
- if (m.score >= this.config.minRelevance && !seenIds.has(m.memory?.id)) {
231
+ if (m.score >= this.config.minRelevance &&
232
+ !seenIds.has(m.memory?.id)) {
191
233
  seenIds.add(m.memory?.id);
192
234
  memories.push({
193
235
  type: m.memory?.type || "decision",
@@ -197,6 +239,23 @@ export class ContextEnricher {
197
239
  }
198
240
  }
199
241
  }
242
+ // Process LTM results (episodic + semantic)
243
+ if (ltmRes?.data?.results) {
244
+ for (const r of ltmRes.data.results) {
245
+ const mem = r.memory;
246
+ const id = mem?.id;
247
+ if (r.score >= this.config.minRelevance && id && !seenIds.has(id)) {
248
+ seenIds.add(id);
249
+ memories.push({
250
+ type: mem?.subtype || mem?.type || "insight",
251
+ content: mem?.content || "",
252
+ score: r.score,
253
+ });
254
+ }
255
+ }
256
+ }
257
+ // Sort by score and limit
258
+ memories.sort((a, b) => b.score - a.score);
200
259
  return memories.slice(0, this.config.maxAutoRecall + 2);
201
260
  }
202
261
  finally {
@@ -25,7 +25,9 @@ export function formatCodeResults(results, contentLimit = PREVIEW.LONG) {
25
25
  return results
26
26
  .map((r) => `**${r.file}** (${pct(r.score)} match)\n` +
27
27
  (r.startLine ? `Lines ${r.startLine}-${r.endLine || "?"}\n` : "") +
28
- "```" + (r.language || "") + "\n" +
28
+ "```" +
29
+ (r.language || "") +
30
+ "\n" +
29
31
  truncate(r.content, contentLimit) +
30
32
  "\n```")
31
33
  .join("\n\n---\n\n");
@@ -46,7 +48,7 @@ export function formatMemoryResults(results, emptyMessage = "No memories found."
46
48
  results.forEach((r, i) => {
47
49
  const m = r.memory;
48
50
  const type = m.type;
49
- const emoji = type === "todo" && m.status === "done" ? "✅" : (typeEmojis[type] || "📝");
51
+ const emoji = type === "todo" && m.status === "done" ? "✅" : typeEmojis[type] || "📝";
50
52
  result += `### ${i + 1}. ${emoji} ${(type || "note").toUpperCase()}\n`;
51
53
  result += `**Relevance:** ${pct(r.score)}\n`;
52
54
  result += `${m.content}\n`;
@@ -64,21 +66,23 @@ export function formatMemoryResults(results, emptyMessage = "No memories found."
64
66
  export function formatNavigationResults(results) {
65
67
  if (!results?.length)
66
68
  return "No results found.";
67
- return results.map(r => {
68
- const loc = r.lines ? `:${r.lines[0]}-${r.lines[1]}` : '';
69
+ return results
70
+ .map((r) => {
71
+ const loc = r.lines ? `:${r.lines[0]}-${r.lines[1]}` : "";
69
72
  let out = `**${r.file}${loc}** (${pct(r.score)})`;
70
73
  if (r.layer)
71
74
  out += ` [${r.layer}]`;
72
75
  if (r.graphExpanded)
73
- out += ' _(graph)_';
76
+ out += " _(graph)_";
74
77
  if (r.preview)
75
78
  out += `\n\`${truncate(r.preview, 100)}\``;
76
79
  if (r.symbols?.length)
77
- out += `\nSymbols: ${r.symbols.join(', ')}`;
80
+ out += `\nSymbols: ${r.symbols.join(", ")}`;
78
81
  if (r.connections?.length)
79
- out += `\nConnections: ${r.connections.map(c => '`' + c + '`').join(', ')}`;
82
+ out += `\nConnections: ${r.connections.map((c) => "`" + c + "`").join(", ")}`;
80
83
  return out;
81
- }).join('\n\n');
84
+ })
85
+ .join("\n\n");
82
86
  }
83
87
  /** Format pagination footer for list tools */
84
88
  export function paginationFooter(count, limit, offset) {
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Streamable HTTP transport for MCP server.
3
+ * Enables dashboard and remote clients to connect over HTTP.
4
+ *
5
+ * Env:
6
+ * MCP_TRANSPORT — stdio | http | both (default: stdio)
7
+ * MCP_HTTP_PORT — port for HTTP transport (default: 3101)
8
+ * RAG_API_KEY — required for Bearer auth when HTTP is enabled
9
+ */
10
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
+ export interface HttpTransportConfig {
12
+ port: number;
13
+ apiKey?: string;
14
+ }
15
+ export declare function startHttpTransport(server: McpServer, config: HttpTransportConfig): Promise<void>;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Streamable HTTP transport for MCP server.
3
+ * Enables dashboard and remote clients to connect over HTTP.
4
+ *
5
+ * Env:
6
+ * MCP_TRANSPORT — stdio | http | both (default: stdio)
7
+ * MCP_HTTP_PORT — port for HTTP transport (default: 3101)
8
+ * RAG_API_KEY — required for Bearer auth when HTTP is enabled
9
+ */
10
+ import { createServer, } from "node:http";
11
+ import { randomUUID } from "node:crypto";
12
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
13
+ export async function startHttpTransport(server, config) {
14
+ // Per-session transport instances
15
+ const transports = new Map();
16
+ function checkAuth(req, res) {
17
+ if (!config.apiKey)
18
+ return true;
19
+ const auth = req.headers.authorization;
20
+ if (auth !== `Bearer ${config.apiKey}`) {
21
+ res.writeHead(401, { "Content-Type": "application/json" });
22
+ res.end(JSON.stringify({ error: "Unauthorized" }));
23
+ return false;
24
+ }
25
+ return true;
26
+ }
27
+ function parseBody(req) {
28
+ return new Promise((resolve, reject) => {
29
+ const chunks = [];
30
+ req.on("data", (chunk) => chunks.push(chunk));
31
+ req.on("end", () => {
32
+ try {
33
+ const body = Buffer.concat(chunks).toString();
34
+ resolve(body ? JSON.parse(body) : undefined);
35
+ }
36
+ catch (e) {
37
+ reject(e);
38
+ }
39
+ });
40
+ req.on("error", reject);
41
+ });
42
+ }
43
+ const httpServer = createServer(async (req, res) => {
44
+ // Only handle /mcp path
45
+ const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
46
+ if (url.pathname !== "/mcp") {
47
+ res.writeHead(404, { "Content-Type": "application/json" });
48
+ res.end(JSON.stringify({ error: "Not found" }));
49
+ return;
50
+ }
51
+ if (!checkAuth(req, res))
52
+ return;
53
+ const sessionId = req.headers["mcp-session-id"];
54
+ if (req.method === "POST") {
55
+ const body = await parseBody(req).catch(() => undefined);
56
+ let transport = sessionId ? transports.get(sessionId) : undefined;
57
+ if (!transport) {
58
+ // New session — create transport and connect to MCP server
59
+ transport = new StreamableHTTPServerTransport({
60
+ sessionIdGenerator: () => randomUUID(),
61
+ });
62
+ transport.onclose = () => {
63
+ if (transport.sessionId) {
64
+ transports.delete(transport.sessionId);
65
+ }
66
+ };
67
+ await server.connect(transport);
68
+ if (transport.sessionId) {
69
+ transports.set(transport.sessionId, transport);
70
+ }
71
+ }
72
+ await transport.handleRequest(req, res, body);
73
+ }
74
+ else if (req.method === "GET") {
75
+ // SSE stream for server-initiated messages
76
+ if (!sessionId) {
77
+ res.writeHead(400, { "Content-Type": "application/json" });
78
+ res.end(JSON.stringify({ error: "Missing mcp-session-id header" }));
79
+ return;
80
+ }
81
+ const transport = transports.get(sessionId);
82
+ if (!transport) {
83
+ res.writeHead(404, { "Content-Type": "application/json" });
84
+ res.end(JSON.stringify({ error: "Session not found" }));
85
+ return;
86
+ }
87
+ await transport.handleRequest(req, res);
88
+ }
89
+ else if (req.method === "DELETE") {
90
+ // Close session
91
+ if (sessionId) {
92
+ const transport = transports.get(sessionId);
93
+ if (transport) {
94
+ await transport.close();
95
+ transports.delete(sessionId);
96
+ }
97
+ }
98
+ res.writeHead(200);
99
+ res.end();
100
+ }
101
+ else {
102
+ res.writeHead(405, { "Content-Type": "application/json" });
103
+ res.end(JSON.stringify({ error: "Method not allowed" }));
104
+ }
105
+ });
106
+ httpServer.listen(config.port, "127.0.0.1", () => {
107
+ console.error(`MCP HTTP transport listening on http://127.0.0.1:${config.port}/mcp`);
108
+ });
109
+ }
package/dist/index.js CHANGED
@@ -13,8 +13,16 @@
13
13
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
14
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
15
  import { createApiClient } from "./api-client.js";
16
+ import { configureConnectionPool } from "./connection-pool.js";
16
17
  import { ContextEnricher } from "./context-enrichment.js";
18
+ import { startHttpTransport } from "./http-transport.js";
17
19
  import { wrapHandler } from "./tool-middleware.js";
20
+ // Phase 3: Configure undici connection pool for RAG API communication
21
+ configureConnectionPool({
22
+ connections: parseInt(process.env.MCP_POOL_CONNECTIONS || "10"),
23
+ keepAliveTimeout: parseInt(process.env.MCP_POOL_KEEPALIVE || "30000"),
24
+ pipelining: parseInt(process.env.MCP_POOL_PIPELINING || "1"),
25
+ });
18
26
  // Tool modules
19
27
  import { createSearchTools } from "./tools/search.js";
20
28
  import { createAskTools } from "./tools/ask.js";
@@ -51,6 +59,11 @@ const ctx = {
51
59
  collectionPrefix: COLLECTION_PREFIX,
52
60
  enrichmentEnabled: true,
53
61
  };
62
+ // If session ID was injected by SessionStart hook, use it
63
+ const hookSessionId = process.env.RAG_SESSION_ID;
64
+ if (hookSessionId) {
65
+ ctx.activeSessionId = hookSessionId;
66
+ }
54
67
  // Context enrichment middleware
55
68
  const enricher = new ContextEnricher({
56
69
  maxAutoRecall: 3,
@@ -169,11 +182,21 @@ async function cleanup() {
169
182
  }
170
183
  process.on("SIGINT", cleanup);
171
184
  process.on("SIGTERM", cleanup);
172
- // Start server
185
+ // Phase 4: Transport selection — stdio | http | both
186
+ const MCP_TRANSPORT = process.env.MCP_TRANSPORT || "stdio";
187
+ const MCP_HTTP_PORT = parseInt(process.env.MCP_HTTP_PORT || "3101");
173
188
  async function main() {
174
- const transport = new StdioServerTransport();
175
- await server.connect(transport);
176
- console.error(`${PROJECT_NAME} RAG MCP server running (collection prefix: ${COLLECTION_PREFIX})`);
189
+ if (MCP_TRANSPORT === "stdio" || MCP_TRANSPORT === "both") {
190
+ const transport = new StdioServerTransport();
191
+ await server.connect(transport);
192
+ }
193
+ if (MCP_TRANSPORT === "http" || MCP_TRANSPORT === "both") {
194
+ await startHttpTransport(server, {
195
+ port: MCP_HTTP_PORT,
196
+ apiKey: RAG_API_KEY,
197
+ });
198
+ }
199
+ console.error(`${PROJECT_NAME} RAG MCP server running (transport: ${MCP_TRANSPORT}, prefix: ${COLLECTION_PREFIX})`);
177
200
  console.error(`Registered ${coreSpecs.length}/${allSpecs.length} core tools (${allSpecs.length - coreSpecs.length} hidden, accessible via run_agent)`);
178
201
  }
179
202
  main().catch(console.error);
package/dist/schemas.js CHANGED
@@ -22,10 +22,7 @@ export function zodToInputSchema(schema) {
22
22
  };
23
23
  }
24
24
  // ── Primitives ──────────────────────────────────────────────
25
- export const QueryStr = z
26
- .string()
27
- .min(1)
28
- .describe("Search query or question");
25
+ export const QueryStr = z.string().min(1).describe("Search query or question");
29
26
  export const Limit = z
30
27
  .number()
31
28
  .int()
@@ -47,10 +44,7 @@ export const FilePaths = z
47
44
  .array(FilePath)
48
45
  .min(1)
49
46
  .describe("List of file paths");
50
- export const Content = z
51
- .string()
52
- .min(1)
53
- .describe("Text content");
47
+ export const Content = z.string().min(1).describe("Text content");
54
48
  export const CollectionSuffix = z
55
49
  .string()
56
50
  .min(1)
@@ -93,10 +87,7 @@ export const SearchFilters = z
93
87
  .string()
94
88
  .optional()
95
89
  .describe("Filter by file extension (e.g. 'ts', 'py')"),
96
- directory: z
97
- .string()
98
- .optional()
99
- .describe("Filter by directory prefix"),
90
+ directory: z.string().optional().describe("Filter by directory prefix"),
100
91
  })
101
92
  .optional()
102
93
  .describe("Search filters");
@@ -69,7 +69,12 @@ export const SESSION_TOOLS = new Set([
69
69
  // ── Helpers ─────────────────────────────────────────────────
70
70
  /** Summarize tool args into a short string for analytics */
71
71
  export function summarizeInput(name, args) {
72
- const q = args.query || args.question || args.feature || args.description || args.task || "";
72
+ const q = args.query ||
73
+ args.question ||
74
+ args.feature ||
75
+ args.description ||
76
+ args.task ||
77
+ "";
73
78
  if (q && typeof q === "string")
74
79
  return q.slice(0, 200);
75
80
  const content = args.content || args.code || args.diff || "";
@@ -155,6 +160,43 @@ export function trackUsage(name, args, startTime, success, result, errorMessage,
155
160
  })
156
161
  .catch(() => { });
157
162
  }
163
+ // ── Sensory buffer ─────────────────────────────────────────
164
+ /** Extract file paths from tool args */
165
+ function extractFiles(args) {
166
+ const files = [];
167
+ for (const key of ["file", "filePath", "currentFile", "path"]) {
168
+ const v = args[key];
169
+ if (typeof v === "string" && v.length > 0)
170
+ files.push(v);
171
+ }
172
+ const arr = args.affectedFiles || args.files;
173
+ if (Array.isArray(arr)) {
174
+ for (const f of arr) {
175
+ if (typeof f === "string")
176
+ files.push(f);
177
+ }
178
+ }
179
+ return files.slice(0, 20);
180
+ }
181
+ /** Fire-and-forget: capture tool event in sensory buffer */
182
+ function appendToSensoryBuffer(name, args, startTime, success, resultText, ctx) {
183
+ if (!ctx.activeSessionId)
184
+ return;
185
+ if (TRACKING_EXCLUDE.has(name))
186
+ return;
187
+ ctx.api
188
+ .post("/api/sensory/append", {
189
+ projectName: ctx.projectName,
190
+ sessionId: ctx.activeSessionId,
191
+ toolName: name,
192
+ inputSummary: summarizeInput(name, args).slice(0, 500),
193
+ outputSummary: (resultText || "").slice(0, 500),
194
+ filesTouched: extractFiles(args),
195
+ success,
196
+ durationMs: Date.now() - startTime,
197
+ })
198
+ .catch(() => { });
199
+ }
158
200
  // ── Error formatting ────────────────────────────────────────
159
201
  /** Format an error caught during tool execution */
160
202
  export function formatToolError(error, ctx) {
@@ -186,12 +228,12 @@ export function wrapHandler(name, handler, deps) {
186
228
  // Validate: run PreToolUse hooks
187
229
  const validation = await validationPipeline.validate(name, args, ctx);
188
230
  if (!validation.allowed) {
189
- return `Blocked: ${validation.reason || 'validation failed'}`;
231
+ return `Blocked: ${validation.reason || "validation failed"}`;
190
232
  }
191
233
  const validatedArgs = validation.modifiedArgs || args;
192
234
  const warningPrefix = validation.warnings?.length
193
- ? `⚠️ ${validation.warnings.join(' | ')}\n\n`
194
- : '';
235
+ ? `⚠️ ${validation.warnings.join(" | ")}\n\n`
236
+ : "";
195
237
  // Before: auto-enrich context
196
238
  const contextPrefix = ctx.enrichmentEnabled && deps.enricher
197
239
  ? await deps.enricher.before(name, validatedArgs, ctx)
@@ -207,8 +249,10 @@ export function wrapHandler(name, handler, deps) {
207
249
  }
208
250
  // Track usage (fire-and-forget)
209
251
  trackUsage(name, args, startTime, true, text, undefined, ctx);
252
+ // Capture in sensory buffer (fire-and-forget)
253
+ appendToSensoryBuffer(name, args, startTime, true, text, ctx);
210
254
  // Prepend context/warnings if available
211
- const prefix = [warningPrefix, contextPrefix].filter(Boolean).join('');
255
+ const prefix = [warningPrefix, contextPrefix].filter(Boolean).join("");
212
256
  if (prefix) {
213
257
  if (typeof result === "string") {
214
258
  return prefix + result;
@@ -221,6 +265,8 @@ export function wrapHandler(name, handler, deps) {
221
265
  const errorMessage = formatToolError(error, ctx);
222
266
  // Track failed usage (fire-and-forget)
223
267
  trackUsage(name, args, startTime, false, "", errorMessage, ctx);
268
+ // Capture failure in sensory buffer (fire-and-forget)
269
+ appendToSensoryBuffer(name, args, startTime, false, errorMessage, ctx);
224
270
  return errorMessage;
225
271
  }
226
272
  };
@@ -21,7 +21,12 @@ const SESSION_TOOLS = new Set([
21
21
  /** Summarize tool args into a short string for analytics */
22
22
  function summarizeInput(name, args) {
23
23
  // Common patterns: query, question, content, feature, code, file
24
- const q = args.query || args.question || args.feature || args.description || args.task || "";
24
+ const q = args.query ||
25
+ args.question ||
26
+ args.feature ||
27
+ args.description ||
28
+ args.task ||
29
+ "";
25
30
  if (q && typeof q === "string")
26
31
  return q.slice(0, 200);
27
32
  const content = args.content || args.code || args.diff || "";
@@ -40,7 +45,9 @@ function summarizeInput(name, args) {
40
45
  /** Count results from a tool response string */
41
46
  function countResults(result) {
42
47
  // Heuristic: count numbered list items, file matches, or "No results" = 0
43
- if (result.includes("No results") || result.includes("No matches") || result.includes("not found"))
48
+ if (result.includes("No results") ||
49
+ result.includes("No matches") ||
50
+ result.includes("not found"))
44
51
  return 0;
45
52
  const numbered = result.match(/^\d+\./gm);
46
53
  if (numbered)
@@ -166,8 +173,8 @@ export class ToolRegistry {
166
173
  // Track failed usage (fire-and-forget)
167
174
  this.trackUsage(name, args, startTime, false, "", errorMessage, ctx);
168
175
  if (err.code === "ECONNREFUSED") {
169
- return `Error: Cannot connect to RAG API at ${ctx.api.defaults.baseURL}. Is it running?\n` +
170
- `Start with: cd docker && docker-compose up -d`;
176
+ return (`Error: Cannot connect to RAG API at ${ctx.api.defaults.baseURL}. Is it running?\n` +
177
+ `Start with: cd docker && docker-compose up -d`);
171
178
  }
172
179
  if (err.response) {
173
180
  return `API Error (${err.response.status}): ${JSON.stringify(err.response.data)}`;
@@ -14,15 +14,28 @@ export function createAdvancedTools(projectName) {
14
14
  name: "merge_memories",
15
15
  description: `Consolidate duplicate memories for ${projectName}. Finds similar memories and merges them using LLM to reduce clutter.`,
16
16
  schema: z.object({
17
- type: z.string().optional().describe("Filter by memory type (decision, insight, context, todo, conversation, note, or all). Default: all"),
18
- threshold: z.coerce.number().optional().describe("Similarity threshold for merging (0.5-1.0, default: 0.9). Lower = more aggressive merging."),
19
- dryRun: z.boolean().optional().describe("If true, preview merge candidates without making changes (default: true)."),
20
- limit: z.coerce.number().optional().describe("Max clusters to process (default: 50)."),
17
+ type: z
18
+ .string()
19
+ .optional()
20
+ .describe("Filter by memory type (decision, insight, context, todo, conversation, note, or all). Default: all"),
21
+ threshold: z.coerce
22
+ .number()
23
+ .optional()
24
+ .describe("Similarity threshold for merging (0.5-1.0, default: 0.9). Lower = more aggressive merging."),
25
+ dryRun: z
26
+ .boolean()
27
+ .optional()
28
+ .describe("If true, preview merge candidates without making changes (default: true)."),
29
+ limit: z.coerce
30
+ .number()
31
+ .optional()
32
+ .describe("Max clusters to process (default: 50)."),
21
33
  }),
22
34
  annotations: TOOL_ANNOTATIONS["merge_memories"],
23
35
  handler: async (args, ctx) => {
24
- const { type = "all", threshold = 0.9, dryRun = true, limit = 50 } = args;
36
+ const { type = "all", threshold = 0.9, dryRun = true, limit = 50, } = args;
25
37
  const response = await ctx.api.post("/api/memory/merge", {
38
+ projectName: ctx.projectName,
26
39
  type,
27
40
  threshold,
28
41
  dryRun,
@@ -63,13 +76,21 @@ export function createAdvancedTools(projectName) {
63
76
  description: `Get code completion context for ${projectName}. Finds similar patterns, imports, and symbols from the codebase to aid code completion.`,
64
77
  schema: z.object({
65
78
  currentFile: z.string().describe("Path of the file being edited"),
66
- currentCode: z.string().describe("Current code snippet or file content"),
67
- language: z.string().optional().describe("Programming language filter (optional)"),
68
- limit: z.coerce.number().optional().describe("Max results (default: 5)"),
79
+ currentCode: z
80
+ .string()
81
+ .describe("Current code snippet or file content"),
82
+ language: z
83
+ .string()
84
+ .optional()
85
+ .describe("Programming language filter (optional)"),
86
+ limit: z.coerce
87
+ .number()
88
+ .optional()
89
+ .describe("Max results (default: 5)"),
69
90
  }),
70
91
  annotations: TOOL_ANNOTATIONS["get_completion_context"],
71
92
  handler: async (args, ctx) => {
72
- const { currentFile, currentCode, language, limit = 5 } = args;
93
+ const { currentFile, currentCode, language, limit = 5, } = args;
73
94
  const response = await ctx.api.post("/api/code/completion-context", {
74
95
  currentFile,
75
96
  currentCode,
@@ -107,12 +128,18 @@ export function createAdvancedTools(projectName) {
107
128
  schema: z.object({
108
129
  currentFile: z.string().describe("Path of the file being edited"),
109
130
  currentCode: z.string().describe("Current code content"),
110
- language: z.string().optional().describe("Programming language filter (optional)"),
111
- limit: z.coerce.number().optional().describe("Max suggestions (default: 10)"),
131
+ language: z
132
+ .string()
133
+ .optional()
134
+ .describe("Programming language filter (optional)"),
135
+ limit: z.coerce
136
+ .number()
137
+ .optional()
138
+ .describe("Max suggestions (default: 10)"),
112
139
  }),
113
140
  annotations: TOOL_ANNOTATIONS["get_import_suggestions"],
114
141
  handler: async (args, ctx) => {
115
- const { currentFile, currentCode, language, limit = 10 } = args;
142
+ const { currentFile, currentCode, language, limit = 10, } = args;
116
143
  const response = await ctx.api.post("/api/code/import-suggestions", {
117
144
  currentFile,
118
145
  currentCode,
@@ -144,14 +171,26 @@ export function createAdvancedTools(projectName) {
144
171
  name: "get_type_context",
145
172
  description: `Look up type/interface/class definitions and usage in ${projectName}. Finds where a type is defined and how it's used across the codebase.`,
146
173
  schema: z.object({
147
- typeName: z.string().optional().describe("Name of the type/interface/class to look up"),
148
- code: z.string().optional().describe("Code containing types to look up (alternative to typeName)"),
149
- currentFile: z.string().optional().describe("Current file to exclude from results"),
150
- limit: z.coerce.number().optional().describe("Max results per category (default: 5)"),
174
+ typeName: z
175
+ .string()
176
+ .optional()
177
+ .describe("Name of the type/interface/class to look up"),
178
+ code: z
179
+ .string()
180
+ .optional()
181
+ .describe("Code containing types to look up (alternative to typeName)"),
182
+ currentFile: z
183
+ .string()
184
+ .optional()
185
+ .describe("Current file to exclude from results"),
186
+ limit: z.coerce
187
+ .number()
188
+ .optional()
189
+ .describe("Max results per category (default: 5)"),
151
190
  }),
152
191
  annotations: TOOL_ANNOTATIONS["get_type_context"],
153
192
  handler: async (args, ctx) => {
154
- const { typeName, code, currentFile, limit = 5 } = args;
193
+ const { typeName, code, currentFile, limit = 5, } = args;
155
194
  if (!typeName && !code) {
156
195
  return "Error: Either typeName or code is required.";
157
196
  }
@@ -187,8 +226,14 @@ export function createAdvancedTools(projectName) {
187
226
  name: "get_behavior_patterns",
188
227
  description: `Analyze user workflow patterns for ${projectName}. Shows peak hours, tool preferences, common sequences, and session statistics.`,
189
228
  schema: z.object({
190
- days: z.coerce.number().optional().describe("Number of days to analyze (default: 7)"),
191
- sessionId: z.string().optional().describe("Filter to a specific session (optional)"),
229
+ days: z.coerce
230
+ .number()
231
+ .optional()
232
+ .describe("Number of days to analyze (default: 7)"),
233
+ sessionId: z
234
+ .string()
235
+ .optional()
236
+ .describe("Filter to a specific session (optional)"),
192
237
  }),
193
238
  annotations: TOOL_ANNOTATIONS["get_behavior_patterns"],
194
239
  handler: async (args, ctx) => {