@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.
@@ -1,83 +1,83 @@
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', () => {
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('summarizeInput', () => {
8
- it('extracts query field', () => {
9
- expect(summarizeInput('search', { query: 'find auth code' })).toBe('find auth code');
7
+ describe("summarizeInput", () => {
8
+ it("extracts query field", () => {
9
+ expect(summarizeInput("search", { query: "find auth code" })).toBe("find auth code");
10
10
  });
11
- it('extracts question field', () => {
12
- expect(summarizeInput('ask', { question: 'what is auth?' })).toBe('what is auth?');
11
+ it("extracts question field", () => {
12
+ expect(summarizeInput("ask", { question: "what is auth?" })).toBe("what is auth?");
13
13
  });
14
- it('extracts content field as fallback', () => {
15
- expect(summarizeInput('remember', { content: 'important note' })).toBe('important note');
14
+ it("extracts content field as fallback", () => {
15
+ expect(summarizeInput("remember", { content: "important note" })).toBe("important note");
16
16
  });
17
- it('extracts file path as fallback', () => {
18
- expect(summarizeInput('explain', { filePath: 'src/auth.ts' })).toBe('src/auth.ts');
17
+ it("extracts file path as fallback", () => {
18
+ expect(summarizeInput("explain", { filePath: "src/auth.ts" })).toBe("src/auth.ts");
19
19
  });
20
- it('truncates long strings to 200 chars', () => {
21
- const long = 'a'.repeat(300);
22
- expect(summarizeInput('search', { query: long }).length).toBeLessThanOrEqual(200);
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('returns tool name when no useful field', () => {
25
- expect(summarizeInput('get_stats', {})).toBe('get_stats');
24
+ it("returns tool name when no useful field", () => {
25
+ expect(summarizeInput("get_stats", {})).toBe("get_stats");
26
26
  });
27
27
  });
28
- describe('countResults', () => {
28
+ describe("countResults", () => {
29
29
  it('returns 0 for "No results" messages', () => {
30
- expect(countResults('No results found.')).toBe(0);
30
+ expect(countResults("No results found.")).toBe(0);
31
31
  });
32
32
  it('returns 0 for "not found" messages', () => {
33
- expect(countResults('Memory not found')).toBe(0);
33
+ expect(countResults("Memory not found")).toBe(0);
34
34
  });
35
- it('counts numbered items', () => {
36
- const text = '1. First\n2. Second\n3. Third';
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('counts bullet items', () => {
40
- const text = '- item1\n- item2';
39
+ it("counts bullet items", () => {
40
+ const text = "- item1\n- item2";
41
41
  expect(countResults(text)).toBe(2);
42
42
  });
43
- it('returns 1 for generic content', () => {
44
- expect(countResults('Some response text')).toBe(1);
43
+ it("returns 1 for generic content", () => {
44
+ expect(countResults("Some response text")).toBe(1);
45
45
  });
46
46
  });
47
- describe('formatToolError', () => {
47
+ describe("formatToolError", () => {
48
48
  const ctx = {
49
- api: { defaults: { baseURL: 'http://localhost:3100' } },
49
+ api: { defaults: { baseURL: "http://localhost:3100" } },
50
50
  };
51
- it('formats ECONNREFUSED error', () => {
52
- const err = { code: 'ECONNREFUSED' };
51
+ it("formats ECONNREFUSED error", () => {
52
+ const err = { code: "ECONNREFUSED" };
53
53
  const result = formatToolError(err, ctx);
54
- expect(result).toContain('Cannot connect');
55
- expect(result).toContain('localhost:3100');
54
+ expect(result).toContain("Cannot connect");
55
+ expect(result).toContain("localhost:3100");
56
56
  });
57
- it('formats API error with status', () => {
58
- const err = { response: { status: 404, data: { error: 'not found' } } };
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('404');
60
+ expect(result).toContain("404");
61
61
  });
62
- it('formats generic error message', () => {
63
- const err = { message: 'Something broke' };
62
+ it("formats generic error message", () => {
63
+ const err = { message: "Something broke" };
64
64
  const result = formatToolError(err, ctx);
65
- expect(result).toContain('Something broke');
65
+ expect(result).toContain("Something broke");
66
66
  });
67
67
  });
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);
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 'vitest';
2
- import { createMemoryTools } from '../../tools/memory.js';
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: 'http://localhost:3100' },
10
+ defaults: { baseURL: "http://localhost:3100" },
11
11
  },
12
- projectName: 'testproject',
13
- projectPath: '/tmp/testproject',
14
- collectionPrefix: 'testproject',
12
+ projectName: "testproject",
13
+ projectPath: "/tmp/testproject",
14
+ collectionPrefix: "testproject",
15
15
  enrichmentEnabled: false,
16
16
  };
17
17
  }
18
- describe('Memory Tools', () => {
18
+ describe("Memory Tools", () => {
19
19
  let tools;
20
20
  let ctx;
21
21
  beforeEach(() => {
22
22
  vi.resetAllMocks();
23
- tools = createMemoryTools('testproject');
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('remember', () => {
30
- it('stores memory and returns formatted result', async () => {
31
- const mem = { id: 'mem-1', type: 'note', content: 'test note', createdAt: new Date().toISOString() };
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('remember').handler({ content: 'test note', type: 'note', tags: ['tag1'] }, ctx);
34
- expect(ctx.api.post).toHaveBeenCalledWith('/api/memory', expect.objectContaining({
35
- projectName: 'testproject',
36
- content: 'test note',
37
- type: 'note',
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('Memory stored');
40
- expect(result).toContain('mem-1');
44
+ expect(result).toContain("Memory stored");
45
+ expect(result).toContain("mem-1");
41
46
  });
42
47
  });
43
- describe('recall', () => {
44
- it('returns formatted results', async () => {
48
+ describe("recall", () => {
49
+ it("returns formatted results", async () => {
45
50
  ctx.api.post.mockResolvedValue({
46
51
  data: {
47
52
  results: [
48
- { memory: { type: 'insight', content: 'found it', createdAt: new Date().toISOString(), tags: [] }, score: 0.85 },
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('recall').handler({ query: 'find something', limit: 5 }, ctx);
53
- expect(result).toContain('Recalled Memories');
65
+ const result = await findTool("recall").handler({ query: "find something", limit: 5 }, ctx);
66
+ expect(result).toContain("Recalled Memories");
54
67
  });
55
- it('returns empty message when no results', async () => {
68
+ it("returns empty message when no results", async () => {
56
69
  ctx.api.post.mockResolvedValue({ data: { results: [] } });
57
- const result = await findTool('recall').handler({ query: 'nothing' }, ctx);
58
- expect(result).toContain('No memories found');
70
+ const result = await findTool("recall").handler({ query: "nothing" }, ctx);
71
+ expect(result).toContain("No memories found");
59
72
  });
60
73
  });
61
- describe('forget', () => {
62
- it('deletes by memoryId', async () => {
74
+ describe("forget", () => {
75
+ it("deletes by memoryId", async () => {
63
76
  ctx.api.delete.mockResolvedValue({ data: { success: true } });
64
- const result = await findTool('forget').handler({ memoryId: 'mem-1' }, ctx);
65
- expect(ctx.api.delete).toHaveBeenCalledWith(expect.stringContaining('/api/memory/mem-1'));
66
- expect(result).toContain('deleted');
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('deletes by type', async () => {
81
+ it("deletes by type", async () => {
69
82
  ctx.api.delete.mockResolvedValue({ data: {} });
70
- const result = await findTool('forget').handler({ type: 'note' }, ctx);
71
- expect(ctx.api.delete).toHaveBeenCalledWith(expect.stringContaining('/api/memory/type/note'));
72
- expect(result).toContain('note');
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('deletes by olderThanDays', async () => {
87
+ it("deletes by olderThanDays", async () => {
75
88
  ctx.api.post.mockResolvedValue({ data: { deleted: 10 } });
76
- const result = await findTool('forget').handler({ olderThanDays: 30 }, ctx);
77
- expect(ctx.api.post).toHaveBeenCalledWith('/api/memory/forget-older', expect.objectContaining({
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('10');
93
+ expect(result).toContain("10");
81
94
  });
82
- it('returns message when nothing specified', async () => {
83
- const result = await findTool('forget').handler({}, ctx);
84
- expect(result).toContain('specify');
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('promote_memory', () => {
88
- it('promotes and returns formatted result', async () => {
89
- const mem = { id: 'mem-1', type: 'insight', content: 'promoted' };
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('promote_memory').handler({ memoryId: 'mem-1', reason: 'human_validated' }, ctx);
92
- expect(result).toContain('promoted to durable');
93
- expect(result).toContain('mem-1');
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('memory_maintenance', () => {
97
- it('formats maintenance results', async () => {
109
+ describe("memory_maintenance", () => {
110
+ it("formats maintenance results", async () => {
98
111
  ctx.api.post.mockResolvedValue({
99
112
  data: {
100
- quarantine_cleanup: { rejected: ['q-1', 'q-2'], errors: [] },
101
- feedback_maintenance: { promoted: ['f-1'], pruned: [], errors: [] },
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('memory_maintenance').handler({}, ctx);
105
- expect(result).toContain('Maintenance Results');
106
- expect(result).toContain('Quarantine Cleanup');
107
- expect(result).toContain('Feedback Maintenance');
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('batch_remember', () => {
111
- it('stores multiple memories', async () => {
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: 'b-1', type: 'note', content: 'first' },
117
- { id: 'b-2', type: 'insight', content: 'second' },
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('batch_remember').handler({ items: [{ content: 'first' }, { content: 'second', type: 'insight' }] }, ctx);
123
- expect(result).toContain('Saved');
124
- expect(result).toContain('2');
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
  });
@@ -1,4 +1,51 @@
1
1
  /**
2
- * API Client - Shared axios instance for RAG API calls.
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 declare function createApiClient(ragApiUrl: string, projectName: string, projectPath: string, apiKey?: string): import("axios").AxiosInstance;
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 {};
@@ -1,7 +1,141 @@
1
1
  /**
2
- * API Client - Shared axios instance for RAG API calls.
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 axios from "axios";
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
- return axios.create({
15
- baseURL: ragApiUrl,
16
- timeout: 120000,
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 || 'no-session'}:${query.slice(0, 100)}`;
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 && !seenIds.has(m.memory?.id)) {
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 && !seenIds.has(m.memory?.id)) {
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",