@crowley/rag-mcp 1.5.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.js +5 -3
- 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 +13 -8
- 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 +196 -39
- 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
|
@@ -1,83 +1,83 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from
|
|
2
|
-
import { summarizeInput, countResults, formatToolError, TRACKING_EXCLUDE, SESSION_TOOLS, TOOL_TIMEOUTS, } from
|
|
3
|
-
describe(
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { summarizeInput, countResults, formatToolError, TRACKING_EXCLUDE, SESSION_TOOLS, TOOL_TIMEOUTS, } from "../tool-middleware.js";
|
|
3
|
+
describe("Tool Middleware", () => {
|
|
4
4
|
beforeEach(() => {
|
|
5
5
|
vi.resetAllMocks();
|
|
6
6
|
});
|
|
7
|
-
describe(
|
|
8
|
-
it(
|
|
9
|
-
expect(summarizeInput(
|
|
7
|
+
describe("summarizeInput", () => {
|
|
8
|
+
it("extracts query field", () => {
|
|
9
|
+
expect(summarizeInput("search", { query: "find auth code" })).toBe("find auth code");
|
|
10
10
|
});
|
|
11
|
-
it(
|
|
12
|
-
expect(summarizeInput(
|
|
11
|
+
it("extracts question field", () => {
|
|
12
|
+
expect(summarizeInput("ask", { question: "what is auth?" })).toBe("what is auth?");
|
|
13
13
|
});
|
|
14
|
-
it(
|
|
15
|
-
expect(summarizeInput(
|
|
14
|
+
it("extracts content field as fallback", () => {
|
|
15
|
+
expect(summarizeInput("remember", { content: "important note" })).toBe("important note");
|
|
16
16
|
});
|
|
17
|
-
it(
|
|
18
|
-
expect(summarizeInput(
|
|
17
|
+
it("extracts file path as fallback", () => {
|
|
18
|
+
expect(summarizeInput("explain", { filePath: "src/auth.ts" })).toBe("src/auth.ts");
|
|
19
19
|
});
|
|
20
|
-
it(
|
|
21
|
-
const long =
|
|
22
|
-
expect(summarizeInput(
|
|
20
|
+
it("truncates long strings to 200 chars", () => {
|
|
21
|
+
const long = "a".repeat(300);
|
|
22
|
+
expect(summarizeInput("search", { query: long }).length).toBeLessThanOrEqual(200);
|
|
23
23
|
});
|
|
24
|
-
it(
|
|
25
|
-
expect(summarizeInput(
|
|
24
|
+
it("returns tool name when no useful field", () => {
|
|
25
|
+
expect(summarizeInput("get_stats", {})).toBe("get_stats");
|
|
26
26
|
});
|
|
27
27
|
});
|
|
28
|
-
describe(
|
|
28
|
+
describe("countResults", () => {
|
|
29
29
|
it('returns 0 for "No results" messages', () => {
|
|
30
|
-
expect(countResults(
|
|
30
|
+
expect(countResults("No results found.")).toBe(0);
|
|
31
31
|
});
|
|
32
32
|
it('returns 0 for "not found" messages', () => {
|
|
33
|
-
expect(countResults(
|
|
33
|
+
expect(countResults("Memory not found")).toBe(0);
|
|
34
34
|
});
|
|
35
|
-
it(
|
|
36
|
-
const text =
|
|
35
|
+
it("counts numbered items", () => {
|
|
36
|
+
const text = "1. First\n2. Second\n3. Third";
|
|
37
37
|
expect(countResults(text)).toBe(3);
|
|
38
38
|
});
|
|
39
|
-
it(
|
|
40
|
-
const text =
|
|
39
|
+
it("counts bullet items", () => {
|
|
40
|
+
const text = "- item1\n- item2";
|
|
41
41
|
expect(countResults(text)).toBe(2);
|
|
42
42
|
});
|
|
43
|
-
it(
|
|
44
|
-
expect(countResults(
|
|
43
|
+
it("returns 1 for generic content", () => {
|
|
44
|
+
expect(countResults("Some response text")).toBe(1);
|
|
45
45
|
});
|
|
46
46
|
});
|
|
47
|
-
describe(
|
|
47
|
+
describe("formatToolError", () => {
|
|
48
48
|
const ctx = {
|
|
49
|
-
api: { defaults: { baseURL:
|
|
49
|
+
api: { defaults: { baseURL: "http://localhost:3100" } },
|
|
50
50
|
};
|
|
51
|
-
it(
|
|
52
|
-
const err = { code:
|
|
51
|
+
it("formats ECONNREFUSED error", () => {
|
|
52
|
+
const err = { code: "ECONNREFUSED" };
|
|
53
53
|
const result = formatToolError(err, ctx);
|
|
54
|
-
expect(result).toContain(
|
|
55
|
-
expect(result).toContain(
|
|
54
|
+
expect(result).toContain("Cannot connect");
|
|
55
|
+
expect(result).toContain("localhost:3100");
|
|
56
56
|
});
|
|
57
|
-
it(
|
|
58
|
-
const err = { response: { status: 404, data: { error:
|
|
57
|
+
it("formats API error with status", () => {
|
|
58
|
+
const err = { response: { status: 404, data: { error: "not found" } } };
|
|
59
59
|
const result = formatToolError(err, ctx);
|
|
60
|
-
expect(result).toContain(
|
|
60
|
+
expect(result).toContain("404");
|
|
61
61
|
});
|
|
62
|
-
it(
|
|
63
|
-
const err = { message:
|
|
62
|
+
it("formats generic error message", () => {
|
|
63
|
+
const err = { message: "Something broke" };
|
|
64
64
|
const result = formatToolError(err, ctx);
|
|
65
|
-
expect(result).toContain(
|
|
65
|
+
expect(result).toContain("Something broke");
|
|
66
66
|
});
|
|
67
67
|
});
|
|
68
|
-
describe(
|
|
69
|
-
it(
|
|
70
|
-
expect(TRACKING_EXCLUDE.has(
|
|
71
|
-
expect(TRACKING_EXCLUDE.has(
|
|
72
|
-
});
|
|
73
|
-
it(
|
|
74
|
-
expect(SESSION_TOOLS.has(
|
|
75
|
-
expect(SESSION_TOOLS.has(
|
|
76
|
-
});
|
|
77
|
-
it(
|
|
78
|
-
expect(TOOL_TIMEOUTS[
|
|
79
|
-
expect(TOOL_TIMEOUTS[
|
|
80
|
-
expect(TOOL_TIMEOUTS[
|
|
68
|
+
describe("constants", () => {
|
|
69
|
+
it("TRACKING_EXCLUDE contains meta tools", () => {
|
|
70
|
+
expect(TRACKING_EXCLUDE.has("get_tool_analytics")).toBe(true);
|
|
71
|
+
expect(TRACKING_EXCLUDE.has("get_quality_metrics")).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
it("SESSION_TOOLS contains session management", () => {
|
|
74
|
+
expect(SESSION_TOOLS.has("start_session")).toBe(true);
|
|
75
|
+
expect(SESSION_TOOLS.has("end_session")).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
it("TOOL_TIMEOUTS has correct tiers", () => {
|
|
78
|
+
expect(TOOL_TIMEOUTS["index_codebase"]).toBe(120_000);
|
|
79
|
+
expect(TOOL_TIMEOUTS["search_codebase"]).toBe(15_000);
|
|
80
|
+
expect(TOOL_TIMEOUTS["recall"]).toBe(10_000);
|
|
81
81
|
});
|
|
82
82
|
});
|
|
83
83
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from
|
|
2
|
-
import { createMemoryTools } from
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { createMemoryTools } from "../../tools/memory.js";
|
|
3
3
|
function createMockCtx() {
|
|
4
4
|
return {
|
|
5
5
|
api: {
|
|
@@ -7,121 +7,136 @@ function createMockCtx() {
|
|
|
7
7
|
get: vi.fn(),
|
|
8
8
|
delete: vi.fn(),
|
|
9
9
|
patch: vi.fn(),
|
|
10
|
-
defaults: { baseURL:
|
|
10
|
+
defaults: { baseURL: "http://localhost:3100" },
|
|
11
11
|
},
|
|
12
|
-
projectName:
|
|
13
|
-
projectPath:
|
|
14
|
-
collectionPrefix:
|
|
12
|
+
projectName: "testproject",
|
|
13
|
+
projectPath: "/tmp/testproject",
|
|
14
|
+
collectionPrefix: "testproject",
|
|
15
15
|
enrichmentEnabled: false,
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
-
describe(
|
|
18
|
+
describe("Memory Tools", () => {
|
|
19
19
|
let tools;
|
|
20
20
|
let ctx;
|
|
21
21
|
beforeEach(() => {
|
|
22
22
|
vi.resetAllMocks();
|
|
23
|
-
tools = createMemoryTools(
|
|
23
|
+
tools = createMemoryTools("testproject");
|
|
24
24
|
ctx = createMockCtx();
|
|
25
25
|
});
|
|
26
26
|
function findTool(name) {
|
|
27
27
|
return tools.find((t) => t.name === name);
|
|
28
28
|
}
|
|
29
|
-
describe(
|
|
30
|
-
it(
|
|
31
|
-
const mem = {
|
|
29
|
+
describe("remember", () => {
|
|
30
|
+
it("stores memory and returns formatted result", async () => {
|
|
31
|
+
const mem = {
|
|
32
|
+
id: "mem-1",
|
|
33
|
+
type: "note",
|
|
34
|
+
content: "test note",
|
|
35
|
+
createdAt: new Date().toISOString(),
|
|
36
|
+
};
|
|
32
37
|
ctx.api.post.mockResolvedValue({ data: { memory: mem } });
|
|
33
|
-
const result = await findTool(
|
|
34
|
-
expect(ctx.api.post).toHaveBeenCalledWith(
|
|
35
|
-
projectName:
|
|
36
|
-
content:
|
|
37
|
-
type:
|
|
38
|
+
const result = await findTool("remember").handler({ content: "test note", type: "note", tags: ["tag1"] }, ctx);
|
|
39
|
+
expect(ctx.api.post).toHaveBeenCalledWith("/api/memory", expect.objectContaining({
|
|
40
|
+
projectName: "testproject",
|
|
41
|
+
content: "test note",
|
|
42
|
+
type: "note",
|
|
38
43
|
}));
|
|
39
|
-
expect(result).toContain(
|
|
40
|
-
expect(result).toContain(
|
|
44
|
+
expect(result).toContain("Memory stored");
|
|
45
|
+
expect(result).toContain("mem-1");
|
|
41
46
|
});
|
|
42
47
|
});
|
|
43
|
-
describe(
|
|
44
|
-
it(
|
|
48
|
+
describe("recall", () => {
|
|
49
|
+
it("returns formatted results", async () => {
|
|
45
50
|
ctx.api.post.mockResolvedValue({
|
|
46
51
|
data: {
|
|
47
52
|
results: [
|
|
48
|
-
{
|
|
53
|
+
{
|
|
54
|
+
memory: {
|
|
55
|
+
type: "insight",
|
|
56
|
+
content: "found it",
|
|
57
|
+
createdAt: new Date().toISOString(),
|
|
58
|
+
tags: [],
|
|
59
|
+
},
|
|
60
|
+
score: 0.85,
|
|
61
|
+
},
|
|
49
62
|
],
|
|
50
63
|
},
|
|
51
64
|
});
|
|
52
|
-
const result = await findTool(
|
|
53
|
-
expect(result).toContain(
|
|
65
|
+
const result = await findTool("recall").handler({ query: "find something", limit: 5 }, ctx);
|
|
66
|
+
expect(result).toContain("Recalled Memories");
|
|
54
67
|
});
|
|
55
|
-
it(
|
|
68
|
+
it("returns empty message when no results", async () => {
|
|
56
69
|
ctx.api.post.mockResolvedValue({ data: { results: [] } });
|
|
57
|
-
const result = await findTool(
|
|
58
|
-
expect(result).toContain(
|
|
70
|
+
const result = await findTool("recall").handler({ query: "nothing" }, ctx);
|
|
71
|
+
expect(result).toContain("No memories found");
|
|
59
72
|
});
|
|
60
73
|
});
|
|
61
|
-
describe(
|
|
62
|
-
it(
|
|
74
|
+
describe("forget", () => {
|
|
75
|
+
it("deletes by memoryId", async () => {
|
|
63
76
|
ctx.api.delete.mockResolvedValue({ data: { success: true } });
|
|
64
|
-
const result = await findTool(
|
|
65
|
-
expect(ctx.api.delete).toHaveBeenCalledWith(expect.stringContaining(
|
|
66
|
-
expect(result).toContain(
|
|
77
|
+
const result = await findTool("forget").handler({ memoryId: "mem-1" }, ctx);
|
|
78
|
+
expect(ctx.api.delete).toHaveBeenCalledWith(expect.stringContaining("/api/memory/mem-1"));
|
|
79
|
+
expect(result).toContain("deleted");
|
|
67
80
|
});
|
|
68
|
-
it(
|
|
81
|
+
it("deletes by type", async () => {
|
|
69
82
|
ctx.api.delete.mockResolvedValue({ data: {} });
|
|
70
|
-
const result = await findTool(
|
|
71
|
-
expect(ctx.api.delete).toHaveBeenCalledWith(expect.stringContaining(
|
|
72
|
-
expect(result).toContain(
|
|
83
|
+
const result = await findTool("forget").handler({ type: "note" }, ctx);
|
|
84
|
+
expect(ctx.api.delete).toHaveBeenCalledWith(expect.stringContaining("/api/memory/type/note"));
|
|
85
|
+
expect(result).toContain("note");
|
|
73
86
|
});
|
|
74
|
-
it(
|
|
87
|
+
it("deletes by olderThanDays", async () => {
|
|
75
88
|
ctx.api.post.mockResolvedValue({ data: { deleted: 10 } });
|
|
76
|
-
const result = await findTool(
|
|
77
|
-
expect(ctx.api.post).toHaveBeenCalledWith(
|
|
89
|
+
const result = await findTool("forget").handler({ olderThanDays: 30 }, ctx);
|
|
90
|
+
expect(ctx.api.post).toHaveBeenCalledWith("/api/memory/forget-older", expect.objectContaining({
|
|
78
91
|
olderThanDays: 30,
|
|
79
92
|
}));
|
|
80
|
-
expect(result).toContain(
|
|
93
|
+
expect(result).toContain("10");
|
|
81
94
|
});
|
|
82
|
-
it(
|
|
83
|
-
const result = await findTool(
|
|
84
|
-
expect(result).toContain(
|
|
95
|
+
it("returns message when nothing specified", async () => {
|
|
96
|
+
const result = await findTool("forget").handler({}, ctx);
|
|
97
|
+
expect(result).toContain("specify");
|
|
85
98
|
});
|
|
86
99
|
});
|
|
87
|
-
describe(
|
|
88
|
-
it(
|
|
89
|
-
const mem = { id:
|
|
100
|
+
describe("promote_memory", () => {
|
|
101
|
+
it("promotes and returns formatted result", async () => {
|
|
102
|
+
const mem = { id: "mem-1", type: "insight", content: "promoted" };
|
|
90
103
|
ctx.api.post.mockResolvedValue({ data: { memory: mem } });
|
|
91
|
-
const result = await findTool(
|
|
92
|
-
expect(result).toContain(
|
|
93
|
-
expect(result).toContain(
|
|
104
|
+
const result = await findTool("promote_memory").handler({ memoryId: "mem-1", reason: "human_validated" }, ctx);
|
|
105
|
+
expect(result).toContain("promoted to durable");
|
|
106
|
+
expect(result).toContain("mem-1");
|
|
94
107
|
});
|
|
95
108
|
});
|
|
96
|
-
describe(
|
|
97
|
-
it(
|
|
109
|
+
describe("memory_maintenance", () => {
|
|
110
|
+
it("formats maintenance results", async () => {
|
|
98
111
|
ctx.api.post.mockResolvedValue({
|
|
99
112
|
data: {
|
|
100
|
-
quarantine_cleanup: { rejected: [
|
|
101
|
-
feedback_maintenance: { promoted: [
|
|
113
|
+
quarantine_cleanup: { rejected: ["q-1", "q-2"], errors: [] },
|
|
114
|
+
feedback_maintenance: { promoted: ["f-1"], pruned: [], errors: [] },
|
|
102
115
|
},
|
|
103
116
|
});
|
|
104
|
-
const result = await findTool(
|
|
105
|
-
expect(result).toContain(
|
|
106
|
-
expect(result).toContain(
|
|
107
|
-
expect(result).toContain(
|
|
117
|
+
const result = await findTool("memory_maintenance").handler({}, ctx);
|
|
118
|
+
expect(result).toContain("Maintenance Results");
|
|
119
|
+
expect(result).toContain("Quarantine Cleanup");
|
|
120
|
+
expect(result).toContain("Feedback Maintenance");
|
|
108
121
|
});
|
|
109
122
|
});
|
|
110
|
-
describe(
|
|
111
|
-
it(
|
|
123
|
+
describe("batch_remember", () => {
|
|
124
|
+
it("stores multiple memories", async () => {
|
|
112
125
|
ctx.api.post.mockResolvedValue({
|
|
113
126
|
data: {
|
|
114
127
|
savedCount: 2,
|
|
115
128
|
memories: [
|
|
116
|
-
{ id:
|
|
117
|
-
{ id:
|
|
129
|
+
{ id: "b-1", type: "note", content: "first" },
|
|
130
|
+
{ id: "b-2", type: "insight", content: "second" },
|
|
118
131
|
],
|
|
119
132
|
errors: [],
|
|
120
133
|
},
|
|
121
134
|
});
|
|
122
|
-
const result = await findTool(
|
|
123
|
-
|
|
124
|
-
|
|
135
|
+
const result = await findTool("batch_remember").handler({
|
|
136
|
+
items: [{ content: "first" }, { content: "second", type: "insight" }],
|
|
137
|
+
}, ctx);
|
|
138
|
+
expect(result).toContain("Saved");
|
|
139
|
+
expect(result).toContain("2");
|
|
125
140
|
});
|
|
126
141
|
});
|
|
127
142
|
});
|
package/dist/api-client.d.ts
CHANGED
|
@@ -1,4 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* API Client -
|
|
2
|
+
* API Client — lightweight fetch-based HTTP client for RAG API calls.
|
|
3
|
+
* Drop-in replacement for axios with identical interface at call sites:
|
|
4
|
+
* ctx.api.post(path, data) → { data: T }
|
|
5
|
+
* ctx.api.get(path) → { data: T }
|
|
6
|
+
* err.response.status / err.code — same shape as AxiosError
|
|
7
|
+
*
|
|
8
|
+
* Phase 5: Unix domain socket support via API_SOCKET_PATH env var.
|
|
3
9
|
*/
|
|
4
|
-
export
|
|
10
|
+
export interface ApiResponse<T = any> {
|
|
11
|
+
data: T;
|
|
12
|
+
status: number;
|
|
13
|
+
headers: Headers;
|
|
14
|
+
}
|
|
15
|
+
export declare class ApiError extends Error {
|
|
16
|
+
code?: string;
|
|
17
|
+
response?: {
|
|
18
|
+
status: number;
|
|
19
|
+
data: any;
|
|
20
|
+
};
|
|
21
|
+
constructor(message: string, opts?: {
|
|
22
|
+
code?: string;
|
|
23
|
+
status?: number;
|
|
24
|
+
data?: any;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
interface RequestConfig {
|
|
28
|
+
headers?: Record<string, string>;
|
|
29
|
+
timeout?: number;
|
|
30
|
+
signal?: AbortSignal;
|
|
31
|
+
}
|
|
32
|
+
export declare class ApiClient {
|
|
33
|
+
readonly defaults: {
|
|
34
|
+
baseURL: string;
|
|
35
|
+
};
|
|
36
|
+
private _headers;
|
|
37
|
+
private _timeout;
|
|
38
|
+
private _pool?;
|
|
39
|
+
constructor(baseURL: string, timeout: number, headers: Record<string, string>, socketPath?: string);
|
|
40
|
+
get<T = any>(path: string, config?: RequestConfig): Promise<ApiResponse<T>>;
|
|
41
|
+
post<T = any>(path: string, data?: unknown, config?: RequestConfig): Promise<ApiResponse<T>>;
|
|
42
|
+
patch<T = any>(path: string, data?: unknown, config?: RequestConfig): Promise<ApiResponse<T>>;
|
|
43
|
+
delete<T = any>(path: string, config?: RequestConfig): Promise<ApiResponse<T>>;
|
|
44
|
+
private _request;
|
|
45
|
+
/** Unix socket request via undici Pool */
|
|
46
|
+
private _requestViaPool;
|
|
47
|
+
/** Map network errors to axios-compatible ApiError */
|
|
48
|
+
private _mapNetworkError;
|
|
49
|
+
}
|
|
50
|
+
export declare function createApiClient(ragApiUrl: string, projectName: string, projectPath: string, apiKey?: string): ApiClient;
|
|
51
|
+
export {};
|
package/dist/api-client.js
CHANGED
|
@@ -1,7 +1,141 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* API Client -
|
|
2
|
+
* API Client — lightweight fetch-based HTTP client for RAG API calls.
|
|
3
|
+
* Drop-in replacement for axios with identical interface at call sites:
|
|
4
|
+
* ctx.api.post(path, data) → { data: T }
|
|
5
|
+
* ctx.api.get(path) → { data: T }
|
|
6
|
+
* err.response.status / err.code — same shape as AxiosError
|
|
7
|
+
*
|
|
8
|
+
* Phase 5: Unix domain socket support via API_SOCKET_PATH env var.
|
|
3
9
|
*/
|
|
4
|
-
import
|
|
10
|
+
import { Pool } from "undici";
|
|
11
|
+
export class ApiError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
14
|
+
response;
|
|
15
|
+
constructor(message,
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
opts) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.name = "ApiError";
|
|
20
|
+
if (opts?.code)
|
|
21
|
+
this.code = opts.code;
|
|
22
|
+
if (opts?.status !== undefined) {
|
|
23
|
+
this.response = { status: opts.status, data: opts.data };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class ApiClient {
|
|
28
|
+
defaults;
|
|
29
|
+
_headers;
|
|
30
|
+
_timeout;
|
|
31
|
+
_pool;
|
|
32
|
+
constructor(baseURL, timeout, headers, socketPath) {
|
|
33
|
+
this.defaults = { baseURL };
|
|
34
|
+
this._headers = headers;
|
|
35
|
+
this._timeout = timeout;
|
|
36
|
+
// Phase 5: Unix domain socket — use undici Pool with socketPath
|
|
37
|
+
if (socketPath) {
|
|
38
|
+
this._pool = new Pool("http://localhost", {
|
|
39
|
+
socketPath,
|
|
40
|
+
connections: 10,
|
|
41
|
+
keepAliveTimeout: 30_000,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
46
|
+
async get(path, config) {
|
|
47
|
+
return this._request("GET", path, undefined, config);
|
|
48
|
+
}
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
50
|
+
async post(path, data, config) {
|
|
51
|
+
return this._request("POST", path, data, config);
|
|
52
|
+
}
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
+
async patch(path, data, config) {
|
|
55
|
+
return this._request("PATCH", path, data, config);
|
|
56
|
+
}
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
58
|
+
async delete(path, config) {
|
|
59
|
+
return this._request("DELETE", path, undefined, config);
|
|
60
|
+
}
|
|
61
|
+
async _request(method, path, body, config) {
|
|
62
|
+
const timeout = config?.timeout ?? this._timeout;
|
|
63
|
+
const signal = config?.signal ?? AbortSignal.timeout(timeout);
|
|
64
|
+
const headers = { ...this._headers, ...config?.headers };
|
|
65
|
+
// Phase 5: Unix socket path — use undici Pool directly
|
|
66
|
+
if (this._pool) {
|
|
67
|
+
return this._requestViaPool(method, path, body, headers, signal);
|
|
68
|
+
}
|
|
69
|
+
// Standard fetch path (TCP)
|
|
70
|
+
const url = `${this.defaults.baseURL}${path}`;
|
|
71
|
+
let res;
|
|
72
|
+
try {
|
|
73
|
+
res = await fetch(url, {
|
|
74
|
+
method,
|
|
75
|
+
headers,
|
|
76
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
77
|
+
signal,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
throw this._mapNetworkError(err, timeout);
|
|
82
|
+
}
|
|
83
|
+
const data = (await res.json().catch(() => null));
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
throw new ApiError(`Request failed with status ${res.status}`, {
|
|
86
|
+
status: res.status,
|
|
87
|
+
data,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return { data, status: res.status, headers: res.headers };
|
|
91
|
+
}
|
|
92
|
+
/** Unix socket request via undici Pool */
|
|
93
|
+
async _requestViaPool(method, path, body, headers, signal) {
|
|
94
|
+
try {
|
|
95
|
+
const { statusCode, headers: resHeaders, body: resBody, } = await this._pool.request({
|
|
96
|
+
method,
|
|
97
|
+
path,
|
|
98
|
+
headers,
|
|
99
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
100
|
+
signal,
|
|
101
|
+
});
|
|
102
|
+
const text = await resBody.text();
|
|
103
|
+
const data = text ? JSON.parse(text) : null;
|
|
104
|
+
if (statusCode >= 400) {
|
|
105
|
+
throw new ApiError(`Request failed with status ${statusCode}`, {
|
|
106
|
+
status: statusCode,
|
|
107
|
+
data,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// Convert undici headers to standard Headers
|
|
111
|
+
const stdHeaders = new Headers();
|
|
112
|
+
for (const [key, val] of Object.entries(resHeaders)) {
|
|
113
|
+
if (val)
|
|
114
|
+
stdHeaders.set(key, Array.isArray(val) ? val.join(", ") : val);
|
|
115
|
+
}
|
|
116
|
+
return { data, status: statusCode, headers: stdHeaders };
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
if (err instanceof ApiError)
|
|
120
|
+
throw err;
|
|
121
|
+
throw this._mapNetworkError(err, this._timeout);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** Map network errors to axios-compatible ApiError */
|
|
125
|
+
_mapNetworkError(err, timeout) {
|
|
126
|
+
const e = err;
|
|
127
|
+
if (e.cause?.code === "ECONNREFUSED" ||
|
|
128
|
+
e.message?.includes("ECONNREFUSED")) {
|
|
129
|
+
return new ApiError("connect ECONNREFUSED", { code: "ECONNREFUSED" });
|
|
130
|
+
}
|
|
131
|
+
if (e.name === "TimeoutError" || e.name === "AbortError") {
|
|
132
|
+
return new ApiError(`timeout of ${timeout}ms exceeded`, {
|
|
133
|
+
code: "ECONNABORTED",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return new ApiError(e.message || String(err), { code: e.cause?.code });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
5
139
|
export function createApiClient(ragApiUrl, projectName, projectPath, apiKey) {
|
|
6
140
|
const headers = {
|
|
7
141
|
"Content-Type": "application/json",
|
|
@@ -11,9 +145,7 @@ export function createApiClient(ragApiUrl, projectName, projectPath, apiKey) {
|
|
|
11
145
|
if (apiKey) {
|
|
12
146
|
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
13
147
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
headers,
|
|
18
|
-
});
|
|
148
|
+
// Phase 5: Use Unix socket if API_SOCKET_PATH is set
|
|
149
|
+
const socketPath = process.env.API_SOCKET_PATH;
|
|
150
|
+
return new ApiClient(ragApiUrl, 120_000, headers, socketPath);
|
|
19
151
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection pool tuning for RAG API communication.
|
|
3
|
+
* Configures undici global dispatcher to optimize localhost HTTP calls.
|
|
4
|
+
*
|
|
5
|
+
* Env overrides:
|
|
6
|
+
* MCP_POOL_CONNECTIONS — max concurrent connections (default 10)
|
|
7
|
+
* MCP_POOL_KEEPALIVE — keep-alive timeout ms (default 30000)
|
|
8
|
+
* MCP_POOL_PIPELINING — HTTP pipelining depth (default 1; 4 for aggressive)
|
|
9
|
+
*/
|
|
10
|
+
export interface PoolConfig {
|
|
11
|
+
connections?: number;
|
|
12
|
+
keepAliveTimeout?: number;
|
|
13
|
+
pipelining?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function configureConnectionPool(config?: PoolConfig): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection pool tuning for RAG API communication.
|
|
3
|
+
* Configures undici global dispatcher to optimize localhost HTTP calls.
|
|
4
|
+
*
|
|
5
|
+
* Env overrides:
|
|
6
|
+
* MCP_POOL_CONNECTIONS — max concurrent connections (default 10)
|
|
7
|
+
* MCP_POOL_KEEPALIVE — keep-alive timeout ms (default 30000)
|
|
8
|
+
* MCP_POOL_PIPELINING — HTTP pipelining depth (default 1; 4 for aggressive)
|
|
9
|
+
*/
|
|
10
|
+
import { Agent, setGlobalDispatcher } from "undici";
|
|
11
|
+
const DEFAULTS = {
|
|
12
|
+
connections: 10,
|
|
13
|
+
keepAliveTimeout: 30_000,
|
|
14
|
+
pipelining: 1,
|
|
15
|
+
};
|
|
16
|
+
export function configureConnectionPool(config) {
|
|
17
|
+
const merged = { ...DEFAULTS, ...config };
|
|
18
|
+
const agent = new Agent({
|
|
19
|
+
connections: merged.connections,
|
|
20
|
+
keepAliveTimeout: merged.keepAliveTimeout,
|
|
21
|
+
pipelining: merged.pipelining,
|
|
22
|
+
});
|
|
23
|
+
setGlobalDispatcher(agent);
|
|
24
|
+
}
|
|
@@ -77,7 +77,7 @@ export class ContextEnricher {
|
|
|
77
77
|
if (!query)
|
|
78
78
|
return null;
|
|
79
79
|
// Check per-session cache
|
|
80
|
-
const cacheKey = `${ctx.activeSessionId ||
|
|
80
|
+
const cacheKey = `${ctx.activeSessionId || "no-session"}:${query.slice(0, 100)}`;
|
|
81
81
|
const cached = this.cache.get(cacheKey);
|
|
82
82
|
if (cached && Date.now() < cached.expiresAt) {
|
|
83
83
|
return cached.result;
|
|
@@ -214,7 +214,8 @@ export class ContextEnricher {
|
|
|
214
214
|
// Process general memories
|
|
215
215
|
if (memoriesRes?.data?.memories) {
|
|
216
216
|
for (const m of memoriesRes.data.memories) {
|
|
217
|
-
if (m.score >= this.config.minRelevance &&
|
|
217
|
+
if (m.score >= this.config.minRelevance &&
|
|
218
|
+
!seenIds.has(m.memory?.id)) {
|
|
218
219
|
seenIds.add(m.memory?.id);
|
|
219
220
|
memories.push({
|
|
220
221
|
type: m.memory?.type || "note",
|
|
@@ -227,7 +228,8 @@ export class ContextEnricher {
|
|
|
227
228
|
// Process decisions/ADRs
|
|
228
229
|
if (decisionsRes?.data?.memories) {
|
|
229
230
|
for (const m of decisionsRes.data.memories) {
|
|
230
|
-
if (m.score >= this.config.minRelevance &&
|
|
231
|
+
if (m.score >= this.config.minRelevance &&
|
|
232
|
+
!seenIds.has(m.memory?.id)) {
|
|
231
233
|
seenIds.add(m.memory?.id);
|
|
232
234
|
memories.push({
|
|
233
235
|
type: m.memory?.type || "decision",
|