@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.
- package/dist/__tests__/tool-middleware.test.js +51 -51
- package/dist/__tests__/tools/memory.test.js +78 -63
- package/dist/api-client.d.ts +49 -2
- package/dist/api-client.js +139 -7
- package/dist/connection-pool.d.ts +15 -0
- package/dist/connection-pool.js +24 -0
- package/dist/context-enrichment.d.ts +6 -0
- package/dist/context-enrichment.js +67 -8
- package/dist/formatters.js +12 -8
- package/dist/http-transport.d.ts +15 -0
- package/dist/http-transport.js +109 -0
- package/dist/index.js +27 -4
- package/dist/schemas.js +3 -12
- package/dist/tool-middleware.js +51 -5
- package/dist/tool-registry.js +11 -4
- package/dist/tools/advanced.js +64 -19
- package/dist/tools/agents.js +42 -13
- package/dist/tools/analytics.js +17 -5
- package/dist/tools/architecture.js +115 -31
- package/dist/tools/ask.js +23 -8
- package/dist/tools/cache.js +12 -3
- package/dist/tools/clustering.js +53 -17
- package/dist/tools/confluence.js +26 -8
- package/dist/tools/database.js +87 -24
- package/dist/tools/feedback.js +22 -6
- package/dist/tools/guidelines.js +15 -2
- package/dist/tools/indexing.js +34 -8
- package/dist/tools/memory.js +198 -38
- package/dist/tools/pm.js +38 -11
- package/dist/tools/quality.js +7 -2
- package/dist/tools/review.js +25 -7
- package/dist/tools/search.js +92 -31
- package/dist/tools/session.js +58 -26
- package/dist/tools/suggestions.js +75 -22
- package/dist/types.d.ts +2 -2
- package/dist/validation-hooks.js +27 -11
- package/package.json +2 -2
|
@@ -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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
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 &&
|
|
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 &&
|
|
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 {
|
package/dist/formatters.js
CHANGED
|
@@ -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
|
-
"```" +
|
|
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" ? "✅" :
|
|
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
|
|
68
|
-
|
|
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 +=
|
|
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 =>
|
|
82
|
+
out += `\nConnections: ${r.connections.map((c) => "`" + c + "`").join(", ")}`;
|
|
80
83
|
return out;
|
|
81
|
-
})
|
|
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
|
-
//
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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");
|
package/dist/tool-middleware.js
CHANGED
|
@@ -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 ||
|
|
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 ||
|
|
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(
|
|
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
|
};
|
package/dist/tool-registry.js
CHANGED
|
@@ -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 ||
|
|
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") ||
|
|
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)}`;
|
package/dist/tools/advanced.js
CHANGED
|
@@ -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
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
111
|
-
|
|
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
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
191
|
-
|
|
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) => {
|