@usewhisper/mcp-server 0.3.0 → 0.5.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.
Files changed (57) hide show
  1. package/README.md +182 -154
  2. package/dist/autosubscribe-6EDKPBE2.js +4068 -4068
  3. package/dist/autosubscribe-GHO6YR5A.js +4068 -4068
  4. package/dist/autosubscribe-ISDETQIB.js +435 -435
  5. package/dist/chunk-3WGYBAYR.js +8387 -8387
  6. package/dist/chunk-52VJYCZ7.js +455 -455
  7. package/dist/chunk-5KBZQHDL.js +189 -189
  8. package/dist/chunk-5KIJNY6Z.js +370 -370
  9. package/dist/chunk-7SN3CKDK.js +1076 -1076
  10. package/dist/chunk-B3VWOHUA.js +271 -271
  11. package/dist/chunk-C57DHKTL.js +459 -459
  12. package/dist/chunk-EI5CE3EY.js +616 -616
  13. package/dist/chunk-FTWUJBAH.js +386 -386
  14. package/dist/chunk-H3HSKH2P.js +4841 -4841
  15. package/dist/chunk-JO3ORBZD.js +616 -616
  16. package/dist/chunk-L6DXSM2U.js +456 -456
  17. package/dist/chunk-LMEYV4JD.js +368 -368
  18. package/dist/chunk-MEFLJ4PV.js +8385 -8385
  19. package/dist/chunk-OBLI4FE4.js +275 -275
  20. package/dist/chunk-PPGYJJED.js +271 -271
  21. package/dist/chunk-QGM4M3NI.js +37 -37
  22. package/dist/chunk-T7KMSTWP.js +399 -399
  23. package/dist/chunk-TWEIYHI6.js +399 -399
  24. package/dist/chunk-UYWE7HSU.js +368 -368
  25. package/dist/chunk-X2DL2GWT.js +32 -32
  26. package/dist/chunk-X7HNNNJJ.js +1079 -1079
  27. package/dist/consolidation-2GCKI4RE.js +220 -220
  28. package/dist/consolidation-4JOPW6BG.js +220 -220
  29. package/dist/consolidation-FOVQTWNQ.js +222 -222
  30. package/dist/consolidation-IFQ52E44.js +209 -209
  31. package/dist/context-sharing-4ITCNKG4.js +307 -307
  32. package/dist/context-sharing-6CCFIAKL.js +275 -275
  33. package/dist/context-sharing-GYKLXHZA.js +307 -307
  34. package/dist/context-sharing-PH64JTXS.js +308 -308
  35. package/dist/context-sharing-Y6LTZZOF.js +307 -307
  36. package/dist/cost-optimization-6OIKRSBV.js +195 -195
  37. package/dist/cost-optimization-7DVSTL6R.js +307 -307
  38. package/dist/cost-optimization-BH5NAX33.js +286 -286
  39. package/dist/cost-optimization-F3L5BS5F.js +303 -303
  40. package/dist/ingest-2LPTWUUM.js +16 -16
  41. package/dist/ingest-7T5FAZNC.js +15 -15
  42. package/dist/ingest-EBNIE7XB.js +15 -15
  43. package/dist/ingest-FSHT5BCS.js +15 -15
  44. package/dist/ingest-QE2BTV72.js +14 -14
  45. package/dist/oracle-3RLQF3DP.js +259 -259
  46. package/dist/oracle-FKRTQUUG.js +282 -282
  47. package/dist/oracle-J47QCSEW.js +263 -263
  48. package/dist/oracle-MDP5MZRC.js +256 -256
  49. package/dist/search-BLVHWLWC.js +14 -14
  50. package/dist/search-CZ5NYL5B.js +12 -12
  51. package/dist/search-EG6TYWWW.js +13 -13
  52. package/dist/search-I22QQA7T.js +13 -13
  53. package/dist/search-T7H5G6DW.js +13 -13
  54. package/dist/server.d.ts +2 -2
  55. package/dist/server.js +1973 -169
  56. package/dist/server.js.map +1 -1
  57. package/package.json +51 -51
package/dist/server.js CHANGED
@@ -4,6 +4,375 @@
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import { z } from "zod";
7
+ import { execSync, spawnSync } from "child_process";
8
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, appendFileSync } from "fs";
9
+ import { join, relative, extname } from "path";
10
+ import { homedir } from "os";
11
+ import { createHash, randomUUID } from "crypto";
12
+
13
+ // ../src/sdk/core/telemetry.ts
14
+ var DiagnosticsStore = class {
15
+ maxEntries;
16
+ records = [];
17
+ subscribers = /* @__PURE__ */ new Set();
18
+ constructor(maxEntries = 1e3) {
19
+ this.maxEntries = Math.max(1, maxEntries);
20
+ }
21
+ add(record) {
22
+ this.records.push(record);
23
+ if (this.records.length > this.maxEntries) {
24
+ this.records.splice(0, this.records.length - this.maxEntries);
25
+ }
26
+ for (const fn of this.subscribers) {
27
+ try {
28
+ fn(record);
29
+ } catch {
30
+ }
31
+ }
32
+ }
33
+ getLast(limit = 25) {
34
+ const count = Math.max(1, limit);
35
+ return this.records.slice(-count);
36
+ }
37
+ snapshot() {
38
+ const total = this.records.length;
39
+ const success = this.records.filter((r) => r.success).length;
40
+ const failure = total - success;
41
+ const duration = this.records.reduce((acc, item) => acc + item.durationMs, 0);
42
+ return {
43
+ total,
44
+ success,
45
+ failure,
46
+ avgDurationMs: total > 0 ? duration / total : 0,
47
+ lastTraceId: this.records[this.records.length - 1]?.traceId
48
+ };
49
+ }
50
+ subscribe(fn) {
51
+ this.subscribers.add(fn);
52
+ return () => {
53
+ this.subscribers.delete(fn);
54
+ };
55
+ }
56
+ };
57
+
58
+ // ../src/sdk/core/utils.ts
59
+ function normalizeBaseUrl(url) {
60
+ let normalized = url.trim().replace(/\/+$/, "");
61
+ normalized = normalized.replace(/\/api\/v1$/i, "");
62
+ normalized = normalized.replace(/\/v1$/i, "");
63
+ normalized = normalized.replace(/\/api$/i, "");
64
+ return normalized;
65
+ }
66
+ function normalizeEndpoint(endpoint) {
67
+ const withLeadingSlash = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
68
+ if (/^\/api\/v1(\/|$)/i.test(withLeadingSlash)) {
69
+ return withLeadingSlash.replace(/^\/api/i, "");
70
+ }
71
+ return withLeadingSlash;
72
+ }
73
+ function nowIso() {
74
+ return (/* @__PURE__ */ new Date()).toISOString();
75
+ }
76
+ function stableHash(input) {
77
+ let hash = 2166136261;
78
+ for (let i = 0; i < input.length; i += 1) {
79
+ hash ^= input.charCodeAt(i);
80
+ hash = Math.imul(hash, 16777619);
81
+ }
82
+ return (hash >>> 0).toString(16).padStart(8, "0");
83
+ }
84
+ function randomId(prefix = "id") {
85
+ return `${prefix}_${stableHash(`${Date.now()}_${Math.random()}`)}`;
86
+ }
87
+
88
+ // ../src/sdk/core/client.ts
89
+ var DEFAULT_TIMEOUTS = {
90
+ searchMs: 3e3,
91
+ writeAckMs: 2e3,
92
+ bulkMs: 1e4,
93
+ profileMs: 2500,
94
+ sessionMs: 2500
95
+ };
96
+ var DEFAULT_RETRYABLE_STATUS = [408, 429, 500, 502, 503, 504];
97
+ var DEFAULT_RETRY_ATTEMPTS = {
98
+ search: 3,
99
+ writeAck: 2,
100
+ bulk: 2,
101
+ profile: 2,
102
+ session: 2,
103
+ query: 3,
104
+ get: 2
105
+ };
106
+ function isObject(value) {
107
+ return typeof value === "object" && value !== null;
108
+ }
109
+ function toMessage(payload, status, statusText) {
110
+ if (typeof payload === "string" && payload.trim()) return payload;
111
+ if (isObject(payload)) {
112
+ const maybeError = payload.error;
113
+ const maybeMessage = payload.message;
114
+ if (typeof maybeError === "string" && maybeError.trim()) return maybeError;
115
+ if (typeof maybeMessage === "string" && maybeMessage.trim()) return maybeMessage;
116
+ if (isObject(maybeError) && typeof maybeError.message === "string") return maybeError.message;
117
+ }
118
+ return `HTTP ${status}: ${statusText}`;
119
+ }
120
+ var RuntimeClientError = class extends Error {
121
+ status;
122
+ retryable;
123
+ code;
124
+ details;
125
+ traceId;
126
+ constructor(args) {
127
+ super(args.message);
128
+ this.name = "RuntimeClientError";
129
+ this.status = args.status;
130
+ this.retryable = args.retryable;
131
+ this.code = args.code;
132
+ this.details = args.details;
133
+ this.traceId = args.traceId;
134
+ }
135
+ };
136
+ var RuntimeClient = class {
137
+ apiKey;
138
+ baseUrl;
139
+ sdkVersion;
140
+ compatMode;
141
+ retryPolicy;
142
+ timeouts;
143
+ diagnostics;
144
+ inFlight = /* @__PURE__ */ new Map();
145
+ sendApiKeyHeader;
146
+ constructor(options, diagnostics) {
147
+ if (!options.apiKey) {
148
+ throw new RuntimeClientError({
149
+ code: "INVALID_API_KEY",
150
+ message: "API key is required",
151
+ retryable: false
152
+ });
153
+ }
154
+ this.apiKey = options.apiKey;
155
+ this.baseUrl = normalizeBaseUrl(options.baseUrl || "https://context.usewhisper.dev");
156
+ this.sdkVersion = options.sdkVersion || "2.x-runtime";
157
+ this.compatMode = options.compatMode || "fallback";
158
+ this.retryPolicy = {
159
+ retryableStatusCodes: options.retryPolicy?.retryableStatusCodes || DEFAULT_RETRYABLE_STATUS,
160
+ retryOnNetworkError: options.retryPolicy?.retryOnNetworkError ?? true,
161
+ maxBackoffMs: options.retryPolicy?.maxBackoffMs ?? 1200,
162
+ baseBackoffMs: options.retryPolicy?.baseBackoffMs ?? 250,
163
+ maxAttemptsByOperation: options.retryPolicy?.maxAttemptsByOperation || {}
164
+ };
165
+ this.timeouts = {
166
+ ...DEFAULT_TIMEOUTS,
167
+ ...options.timeouts || {}
168
+ };
169
+ this.sendApiKeyHeader = process.env.WHISPER_SEND_X_API_KEY === "1";
170
+ this.diagnostics = diagnostics || new DiagnosticsStore(1e3);
171
+ }
172
+ getDiagnosticsStore() {
173
+ return this.diagnostics;
174
+ }
175
+ getCompatMode() {
176
+ return this.compatMode;
177
+ }
178
+ timeoutFor(operation) {
179
+ switch (operation) {
180
+ case "search":
181
+ return this.timeouts.searchMs;
182
+ case "writeAck":
183
+ return this.timeouts.writeAckMs;
184
+ case "bulk":
185
+ return this.timeouts.bulkMs;
186
+ case "profile":
187
+ return this.timeouts.profileMs;
188
+ case "session":
189
+ return this.timeouts.sessionMs;
190
+ case "query":
191
+ case "get":
192
+ default:
193
+ return this.timeouts.searchMs;
194
+ }
195
+ }
196
+ maxAttemptsFor(operation) {
197
+ const override = this.retryPolicy.maxAttemptsByOperation?.[operation];
198
+ return Math.max(1, override ?? DEFAULT_RETRY_ATTEMPTS[operation]);
199
+ }
200
+ shouldRetryStatus(status) {
201
+ return status !== void 0 && this.retryPolicy.retryableStatusCodes?.includes(status) === true;
202
+ }
203
+ backoff(attempt) {
204
+ const base = this.retryPolicy.baseBackoffMs ?? 250;
205
+ const max = this.retryPolicy.maxBackoffMs ?? 1200;
206
+ const jitter = 0.8 + Math.random() * 0.4;
207
+ return Math.min(max, Math.floor(base * Math.pow(2, attempt) * jitter));
208
+ }
209
+ runtimeName() {
210
+ const maybeWindow = globalThis.window;
211
+ return maybeWindow && typeof maybeWindow === "object" ? "browser" : "node";
212
+ }
213
+ createRequestFingerprint(options) {
214
+ const normalizedEndpoint = normalizeEndpoint(options.endpoint);
215
+ const authFingerprint = stableHash(this.apiKey.replace(/^Bearer\s+/i, ""));
216
+ const payload = JSON.stringify({
217
+ method: options.method || "GET",
218
+ endpoint: normalizedEndpoint,
219
+ body: options.body || null,
220
+ extra: options.dedupeKeyExtra || "",
221
+ authFingerprint
222
+ });
223
+ return stableHash(payload);
224
+ }
225
+ async request(options) {
226
+ const dedupeKey = options.idempotent ? this.createRequestFingerprint(options) : null;
227
+ if (dedupeKey) {
228
+ const inFlight = this.inFlight.get(dedupeKey);
229
+ if (inFlight) {
230
+ const data = await inFlight;
231
+ this.diagnostics.add({
232
+ id: randomId("diag"),
233
+ startedAt: nowIso(),
234
+ endedAt: nowIso(),
235
+ traceId: data.traceId,
236
+ spanId: randomId("span"),
237
+ operation: options.operation,
238
+ method: options.method || "GET",
239
+ endpoint: normalizeEndpoint(options.endpoint),
240
+ status: data.status,
241
+ durationMs: 0,
242
+ success: true,
243
+ deduped: true
244
+ });
245
+ const cloned = {
246
+ data: data.data,
247
+ status: data.status,
248
+ traceId: data.traceId
249
+ };
250
+ return cloned;
251
+ }
252
+ }
253
+ const runner = this.performRequest(options).then((data) => {
254
+ if (dedupeKey) this.inFlight.delete(dedupeKey);
255
+ return data;
256
+ }).catch((error) => {
257
+ if (dedupeKey) this.inFlight.delete(dedupeKey);
258
+ throw error;
259
+ });
260
+ if (dedupeKey) {
261
+ this.inFlight.set(dedupeKey, runner);
262
+ }
263
+ return runner;
264
+ }
265
+ async performRequest(options) {
266
+ const method = options.method || "GET";
267
+ const normalizedEndpoint = normalizeEndpoint(options.endpoint);
268
+ const operation = options.operation;
269
+ const maxAttempts = this.maxAttemptsFor(operation);
270
+ const timeoutMs = this.timeoutFor(operation);
271
+ const traceId = options.traceId || randomId("trace");
272
+ let lastError = null;
273
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
274
+ const spanId = randomId("span");
275
+ const startedAt = Date.now();
276
+ const controller = new AbortController();
277
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
278
+ try {
279
+ const response = await fetch(`${this.baseUrl}${normalizedEndpoint}`, {
280
+ method,
281
+ signal: controller.signal,
282
+ keepalive: method !== "GET",
283
+ headers: {
284
+ "Content-Type": "application/json",
285
+ Authorization: this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`,
286
+ ...this.sendApiKeyHeader ? { "X-API-Key": this.apiKey.replace(/^Bearer\s+/i, "") } : {},
287
+ "x-trace-id": traceId,
288
+ "x-span-id": spanId,
289
+ "x-sdk-version": this.sdkVersion,
290
+ "x-sdk-runtime": this.runtimeName(),
291
+ ...options.headers || {}
292
+ },
293
+ body: method === "GET" || method === "DELETE" ? void 0 : JSON.stringify(options.body || {})
294
+ });
295
+ clearTimeout(timeout);
296
+ let payload = null;
297
+ try {
298
+ payload = await response.json();
299
+ } catch {
300
+ payload = await response.text().catch(() => "");
301
+ }
302
+ const durationMs = Date.now() - startedAt;
303
+ const record = {
304
+ id: randomId("diag"),
305
+ startedAt: new Date(startedAt).toISOString(),
306
+ endedAt: nowIso(),
307
+ traceId,
308
+ spanId,
309
+ operation,
310
+ method,
311
+ endpoint: normalizedEndpoint,
312
+ status: response.status,
313
+ durationMs,
314
+ success: response.ok
315
+ };
316
+ this.diagnostics.add(record);
317
+ if (response.ok) {
318
+ return {
319
+ data: payload,
320
+ status: response.status,
321
+ traceId
322
+ };
323
+ }
324
+ const message = toMessage(payload, response.status, response.statusText);
325
+ const retryable = this.shouldRetryStatus(response.status);
326
+ const error = new RuntimeClientError({
327
+ message,
328
+ status: response.status,
329
+ retryable,
330
+ code: response.status === 404 ? "NOT_FOUND" : "REQUEST_FAILED",
331
+ details: payload,
332
+ traceId
333
+ });
334
+ lastError = error;
335
+ if (!retryable || attempt === maxAttempts - 1) {
336
+ throw error;
337
+ }
338
+ } catch (error) {
339
+ clearTimeout(timeout);
340
+ const durationMs = Date.now() - startedAt;
341
+ const isAbort = isObject(error) && error.name === "AbortError";
342
+ const mapped = error instanceof RuntimeClientError ? error : new RuntimeClientError({
343
+ message: isAbort ? "Request timed out" : error instanceof Error ? error.message : "Network error",
344
+ retryable: this.retryPolicy.retryOnNetworkError ?? true,
345
+ code: isAbort ? "TIMEOUT" : "NETWORK_ERROR",
346
+ traceId
347
+ });
348
+ lastError = mapped;
349
+ this.diagnostics.add({
350
+ id: randomId("diag"),
351
+ startedAt: new Date(startedAt).toISOString(),
352
+ endedAt: nowIso(),
353
+ traceId,
354
+ spanId,
355
+ operation,
356
+ method,
357
+ endpoint: normalizedEndpoint,
358
+ durationMs,
359
+ success: false,
360
+ errorCode: mapped.code,
361
+ errorMessage: mapped.message
362
+ });
363
+ if (!mapped.retryable || attempt === maxAttempts - 1) {
364
+ throw mapped;
365
+ }
366
+ }
367
+ await new Promise((resolve) => setTimeout(resolve, this.backoff(attempt)));
368
+ }
369
+ throw lastError || new RuntimeClientError({
370
+ message: "Request failed",
371
+ retryable: false,
372
+ code: "REQUEST_FAILED"
373
+ });
374
+ }
375
+ };
7
376
 
8
377
  // ../src/sdk/index.ts
9
378
  var WhisperError = class extends Error {
@@ -25,23 +394,42 @@ var DEFAULT_BASE_DELAY_MS = 250;
25
394
  var DEFAULT_MAX_DELAY_MS = 2e3;
26
395
  var DEFAULT_TIMEOUT_MS = 15e3;
27
396
  var PROJECT_CACHE_TTL_MS = 3e4;
28
- function sleep(ms) {
29
- return new Promise((resolve) => setTimeout(resolve, ms));
30
- }
31
- function getBackoffDelay(attempt, base, max) {
32
- const jitter = 0.8 + Math.random() * 0.4;
33
- return Math.min(max, Math.floor(base * Math.pow(2, attempt) * jitter));
397
+ var DEPRECATION_WARNINGS = /* @__PURE__ */ new Set();
398
+ function warnDeprecatedOnce(key, message) {
399
+ if (DEPRECATION_WARNINGS.has(key)) return;
400
+ DEPRECATION_WARNINGS.add(key);
401
+ if (typeof console !== "undefined" && typeof console.warn === "function") {
402
+ console.warn(message);
403
+ }
34
404
  }
35
405
  function isLikelyProjectId(projectRef) {
36
406
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(projectRef);
37
407
  }
408
+ function normalizeBaseUrl2(url) {
409
+ let normalized = url.trim().replace(/\/+$/, "");
410
+ normalized = normalized.replace(/\/api\/v1$/i, "");
411
+ normalized = normalized.replace(/\/v1$/i, "");
412
+ normalized = normalized.replace(/\/api$/i, "");
413
+ return normalized;
414
+ }
415
+ function normalizeEndpoint2(endpoint) {
416
+ const withLeadingSlash = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
417
+ if (/^\/api\/v1(\/|$)/i.test(withLeadingSlash)) {
418
+ return withLeadingSlash.replace(/^\/api/i, "");
419
+ }
420
+ return withLeadingSlash;
421
+ }
422
+ function isProjectNotFoundMessage(message) {
423
+ const normalized = message.toLowerCase();
424
+ return normalized.includes("project not found") || normalized.includes("no project found") || normalized.includes("project does not exist");
425
+ }
38
426
  var WhisperContext = class _WhisperContext {
39
427
  apiKey;
40
428
  baseUrl;
41
429
  defaultProject;
42
- orgId;
43
430
  timeoutMs;
44
431
  retryConfig;
432
+ runtimeClient;
45
433
  projectRefToId = /* @__PURE__ */ new Map();
46
434
  projectCache = [];
47
435
  projectCacheExpiresAt = 0;
@@ -53,22 +441,49 @@ var WhisperContext = class _WhisperContext {
53
441
  });
54
442
  }
55
443
  this.apiKey = config.apiKey;
56
- this.baseUrl = config.baseUrl || "https://context.usewhisper.dev";
444
+ this.baseUrl = normalizeBaseUrl2(config.baseUrl || "https://context.usewhisper.dev");
57
445
  this.defaultProject = config.project;
58
- this.orgId = config.orgId;
59
446
  this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
60
447
  this.retryConfig = {
61
448
  maxAttempts: config.retry?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
62
449
  baseDelayMs: config.retry?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS,
63
450
  maxDelayMs: config.retry?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS
64
451
  };
452
+ this.runtimeClient = new RuntimeClient({
453
+ apiKey: this.apiKey,
454
+ baseUrl: this.baseUrl,
455
+ compatMode: "fallback",
456
+ timeouts: {
457
+ searchMs: this.timeoutMs,
458
+ writeAckMs: this.timeoutMs,
459
+ bulkMs: Math.max(this.timeoutMs, 1e4),
460
+ profileMs: this.timeoutMs,
461
+ sessionMs: this.timeoutMs
462
+ },
463
+ retryPolicy: {
464
+ baseBackoffMs: this.retryConfig.baseDelayMs,
465
+ maxBackoffMs: this.retryConfig.maxDelayMs,
466
+ maxAttemptsByOperation: {
467
+ search: this.retryConfig.maxAttempts,
468
+ writeAck: this.retryConfig.maxAttempts,
469
+ bulk: this.retryConfig.maxAttempts,
470
+ profile: this.retryConfig.maxAttempts,
471
+ session: this.retryConfig.maxAttempts,
472
+ query: this.retryConfig.maxAttempts,
473
+ get: this.retryConfig.maxAttempts
474
+ }
475
+ }
476
+ });
477
+ warnDeprecatedOnce(
478
+ "whisper_context_class",
479
+ "[Whisper SDK] WhisperContext remains supported in v2 but is legacy. Prefer WhisperClient for runtime features (queue/cache/session/diagnostics)."
480
+ );
65
481
  }
66
482
  withProject(project) {
67
483
  return new _WhisperContext({
68
484
  apiKey: this.apiKey,
69
485
  baseUrl: this.baseUrl,
70
486
  project,
71
- orgId: this.orgId,
72
487
  timeoutMs: this.timeoutMs,
73
488
  retry: this.retryConfig
74
489
  });
@@ -144,9 +559,17 @@ var WhisperContext = class _WhisperContext {
144
559
  return Array.from(candidates).filter(Boolean);
145
560
  }
146
561
  async withProjectRefFallback(projectRef, execute) {
562
+ try {
563
+ return await execute(projectRef);
564
+ } catch (error) {
565
+ if (!(error instanceof WhisperError) || error.code !== "PROJECT_NOT_FOUND") {
566
+ throw error;
567
+ }
568
+ }
147
569
  const refs = await this.getProjectRefCandidates(projectRef);
148
570
  let lastError;
149
571
  for (const ref of refs) {
572
+ if (ref === projectRef) continue;
150
573
  try {
151
574
  return await execute(ref);
152
575
  } catch (error) {
@@ -169,7 +592,7 @@ var WhisperContext = class _WhisperContext {
169
592
  if (status === 401 || /api key|unauthorized|forbidden/i.test(message)) {
170
593
  return { code: "INVALID_API_KEY", retryable: false };
171
594
  }
172
- if (status === 404 || /project not found/i.test(message)) {
595
+ if (status === 404 && isProjectNotFoundMessage(message)) {
173
596
  return { code: "PROJECT_NOT_FOUND", retryable: false };
174
597
  }
175
598
  if (status === 408) {
@@ -183,64 +606,68 @@ var WhisperContext = class _WhisperContext {
183
606
  }
184
607
  return { code: "REQUEST_FAILED", retryable: false };
185
608
  }
609
+ isEndpointNotFoundError(error) {
610
+ if (!(error instanceof WhisperError)) {
611
+ return false;
612
+ }
613
+ if (error.status !== 404) {
614
+ return false;
615
+ }
616
+ const message = (error.message || "").toLowerCase();
617
+ return !isProjectNotFoundMessage(message);
618
+ }
619
+ inferOperation(endpoint, method) {
620
+ const normalized = normalizeEndpoint2(endpoint).toLowerCase();
621
+ if (normalized.includes("/memory/search")) return "search";
622
+ if (normalized.includes("/memory/bulk")) return "bulk";
623
+ if (normalized.includes("/memory/profile") || normalized.includes("/memory/session")) return "profile";
624
+ if (normalized.includes("/memory/ingest/session")) return "session";
625
+ if (normalized.includes("/context/query")) return "query";
626
+ if (method === "GET") return "get";
627
+ return "writeAck";
628
+ }
186
629
  async request(endpoint, options = {}) {
187
- const maxAttempts = Math.max(1, this.retryConfig.maxAttempts);
188
- let lastError;
189
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
190
- const controller = new AbortController();
191
- const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
630
+ const method = String(options.method || "GET").toUpperCase();
631
+ const normalizedEndpoint = normalizeEndpoint2(endpoint);
632
+ const operation = this.inferOperation(normalizedEndpoint, method);
633
+ let body;
634
+ if (typeof options.body === "string") {
192
635
  try {
193
- const response = await fetch(`${this.baseUrl}${endpoint}`, {
194
- ...options,
195
- signal: controller.signal,
196
- headers: {
197
- Authorization: `Bearer ${this.apiKey}`,
198
- "Content-Type": "application/json",
199
- ...this.orgId ? { "X-Whisper-Org-Id": this.orgId } : {},
200
- ...options.headers
201
- }
202
- });
203
- clearTimeout(timeout);
204
- if (!response.ok) {
205
- let payload = null;
206
- try {
207
- payload = await response.json();
208
- } catch {
209
- payload = await response.text().catch(() => "");
210
- }
211
- const message = typeof payload === "string" ? payload : payload?.error || payload?.message || `HTTP ${response.status}: ${response.statusText}`;
212
- const { code, retryable } = this.classifyError(response.status, message);
213
- const err = new WhisperError({
214
- code,
215
- message,
216
- status: response.status,
217
- retryable,
218
- details: payload
219
- });
220
- if (!retryable || attempt === maxAttempts - 1) {
221
- throw err;
222
- }
223
- await sleep(getBackoffDelay(attempt, this.retryConfig.baseDelayMs, this.retryConfig.maxDelayMs));
224
- continue;
225
- }
226
- return response.json();
227
- } catch (error) {
228
- clearTimeout(timeout);
229
- const isAbort = error?.name === "AbortError";
230
- const mapped = error instanceof WhisperError ? error : new WhisperError({
231
- code: isAbort ? "TIMEOUT" : "NETWORK_ERROR",
232
- message: isAbort ? "Request timed out" : error?.message || "Network request failed",
233
- retryable: true,
234
- details: error
235
- });
236
- lastError = mapped;
237
- if (!mapped.retryable || attempt === maxAttempts - 1) {
238
- throw mapped;
239
- }
240
- await sleep(getBackoffDelay(attempt, this.retryConfig.baseDelayMs, this.retryConfig.maxDelayMs));
636
+ body = JSON.parse(options.body);
637
+ } catch {
638
+ body = void 0;
241
639
  }
640
+ } else if (options.body && typeof options.body === "object" && !ArrayBuffer.isView(options.body) && !(options.body instanceof ArrayBuffer) && !(options.body instanceof FormData) && !(options.body instanceof URLSearchParams) && !(options.body instanceof Blob) && !(options.body instanceof ReadableStream)) {
641
+ body = options.body;
642
+ }
643
+ try {
644
+ const response = await this.runtimeClient.request({
645
+ endpoint: normalizedEndpoint,
646
+ method,
647
+ operation,
648
+ idempotent: method === "GET" || method === "POST" && (operation === "search" || operation === "query" || operation === "profile"),
649
+ body,
650
+ headers: options.headers || {}
651
+ });
652
+ return response.data;
653
+ } catch (error) {
654
+ if (!(error instanceof RuntimeClientError)) {
655
+ throw error;
656
+ }
657
+ let message = error.message;
658
+ if (error.status === 404 && !isProjectNotFoundMessage(message)) {
659
+ const endpointHint = `${this.baseUrl}${normalizedEndpoint}`;
660
+ message = `Endpoint not found at ${endpointHint}. This deployment may not support this API route.`;
661
+ }
662
+ const { code, retryable } = this.classifyError(error.status, message);
663
+ throw new WhisperError({
664
+ code,
665
+ message,
666
+ status: error.status,
667
+ retryable,
668
+ details: error.details
669
+ });
242
670
  }
243
- throw lastError instanceof Error ? lastError : new WhisperError({ code: "REQUEST_FAILED", message: "Request failed" });
244
671
  }
245
672
  async query(params) {
246
673
  const projectRef = this.getRequiredProject(params.project);
@@ -351,13 +778,18 @@ var WhisperContext = class _WhisperContext {
351
778
  session_id: params.session_id,
352
779
  agent_id: params.agent_id,
353
780
  importance: params.importance,
354
- metadata: params.metadata
781
+ metadata: params.metadata,
782
+ async: params.async,
783
+ write_mode: params.write_mode
355
784
  })
356
785
  });
357
- const id2 = direct?.memory?.id || direct?.id || direct?.memory_id;
786
+ const id2 = direct?.memory?.id || direct?.id || direct?.memory_id || direct?.job_id;
358
787
  if (id2) {
359
788
  return { id: id2, success: true, path: "sota", fallback_used: false };
360
789
  }
790
+ if (direct?.success === true) {
791
+ return { id: "", success: true, path: "sota", fallback_used: false };
792
+ }
361
793
  } catch (error) {
362
794
  if (params.allow_legacy_fallback === false) {
363
795
  throw error;
@@ -387,20 +819,109 @@ var WhisperContext = class _WhisperContext {
387
819
  return { id, success: true, path: "legacy", fallback_used: true };
388
820
  });
389
821
  }
390
- async searchMemories(params) {
822
+ async addMemoriesBulk(params) {
823
+ const projectRef = this.getRequiredProject(params.project);
824
+ return this.withProjectRefFallback(projectRef, async (project) => {
825
+ try {
826
+ return await this.request("/v1/memory/bulk", {
827
+ method: "POST",
828
+ body: JSON.stringify({ ...params, project })
829
+ });
830
+ } catch (error) {
831
+ if (!this.isEndpointNotFoundError(error)) {
832
+ throw error;
833
+ }
834
+ const created = await Promise.all(
835
+ params.memories.map(
836
+ (memory) => this.addMemory({
837
+ project,
838
+ content: memory.content,
839
+ memory_type: memory.memory_type,
840
+ user_id: memory.user_id,
841
+ session_id: memory.session_id,
842
+ agent_id: memory.agent_id,
843
+ importance: memory.importance,
844
+ metadata: memory.metadata,
845
+ allow_legacy_fallback: true
846
+ })
847
+ )
848
+ );
849
+ return {
850
+ success: true,
851
+ created: created.length,
852
+ memories: created,
853
+ path: "legacy",
854
+ fallback_used: true
855
+ };
856
+ }
857
+ });
858
+ }
859
+ async extractMemories(params) {
860
+ const projectRef = this.getRequiredProject(params.project);
861
+ return this.withProjectRefFallback(projectRef, (project) => this.request("/v1/memory/extract", {
862
+ method: "POST",
863
+ body: JSON.stringify({ ...params, project })
864
+ }));
865
+ }
866
+ async extractSessionMemories(params) {
391
867
  const projectRef = this.getRequiredProject(params.project);
392
- return this.withProjectRefFallback(projectRef, (project) => this.request("/v1/memory/search", {
868
+ return this.withProjectRefFallback(projectRef, (project) => this.request("/v1/memory/extract/session", {
393
869
  method: "POST",
394
870
  body: JSON.stringify({
395
- query: params.query,
871
+ ...params,
396
872
  project,
397
- user_id: params.user_id,
398
- session_id: params.session_id,
399
- memory_types: params.memory_type ? [params.memory_type] : void 0,
400
- top_k: params.top_k || 10
873
+ messages: params.messages.map((m) => ({
874
+ ...m,
875
+ timestamp: m.timestamp || (/* @__PURE__ */ new Date()).toISOString()
876
+ }))
401
877
  })
402
878
  }));
403
879
  }
880
+ async searchMemories(params) {
881
+ const projectRef = this.getRequiredProject(params.project);
882
+ return this.withProjectRefFallback(projectRef, async (project) => {
883
+ try {
884
+ return await this.request("/v1/memory/search", {
885
+ method: "POST",
886
+ body: JSON.stringify({
887
+ query: params.query,
888
+ project,
889
+ user_id: params.user_id,
890
+ session_id: params.session_id,
891
+ memory_types: params.memory_type ? [params.memory_type] : void 0,
892
+ top_k: params.top_k || 10,
893
+ profile: params.profile,
894
+ include_pending: params.include_pending
895
+ })
896
+ });
897
+ } catch (error) {
898
+ if (!this.isEndpointNotFoundError(error)) {
899
+ throw error;
900
+ }
901
+ const legacyTypeMap = {
902
+ factual: "factual",
903
+ preference: "semantic",
904
+ event: "episodic",
905
+ relationship: "semantic",
906
+ opinion: "semantic",
907
+ goal: "semantic",
908
+ instruction: "procedural"
909
+ };
910
+ return this.request("/v1/memories/search", {
911
+ method: "POST",
912
+ body: JSON.stringify({
913
+ query: params.query,
914
+ project,
915
+ user_id: params.user_id,
916
+ session_id: params.session_id,
917
+ agent_id: params.agent_id,
918
+ memory_type: params.memory_type ? legacyTypeMap[params.memory_type] : void 0,
919
+ top_k: params.top_k || 10
920
+ })
921
+ });
922
+ }
923
+ });
924
+ }
404
925
  async createApiKey(params) {
405
926
  return this.request("/v1/keys", {
406
927
  method: "POST",
@@ -415,10 +936,29 @@ var WhisperContext = class _WhisperContext {
415
936
  }
416
937
  async searchMemoriesSOTA(params) {
417
938
  const projectRef = this.getRequiredProject(params.project);
418
- return this.withProjectRefFallback(projectRef, (project) => this.request("/v1/memory/search", {
419
- method: "POST",
420
- body: JSON.stringify({ ...params, project })
421
- }));
939
+ return this.withProjectRefFallback(projectRef, async (project) => {
940
+ try {
941
+ return await this.request("/v1/memory/search", {
942
+ method: "POST",
943
+ body: JSON.stringify({ ...params, project })
944
+ });
945
+ } catch (error) {
946
+ if (!this.isEndpointNotFoundError(error)) {
947
+ throw error;
948
+ }
949
+ const firstType = params.memory_types?.[0];
950
+ return this.searchMemories({
951
+ project,
952
+ query: params.query,
953
+ user_id: params.user_id,
954
+ session_id: params.session_id,
955
+ memory_type: firstType,
956
+ top_k: params.top_k,
957
+ profile: params.profile,
958
+ include_pending: params.include_pending
959
+ });
960
+ }
961
+ });
422
962
  }
423
963
  async ingestSession(params) {
424
964
  const projectRef = this.getRequiredProject(params.project);
@@ -428,37 +968,116 @@ var WhisperContext = class _WhisperContext {
428
968
  }));
429
969
  }
430
970
  async getSessionMemories(params) {
431
- const project = await this.resolveProjectId(this.getRequiredProject(params.project));
432
- const query = new URLSearchParams({
433
- project,
434
- ...params.limit && { limit: params.limit.toString() },
435
- ...params.since_date && { since_date: params.since_date }
971
+ const projectRef = this.getRequiredProject(params.project);
972
+ return this.withProjectRefFallback(projectRef, async (project) => {
973
+ const query = new URLSearchParams({
974
+ project,
975
+ ...params.limit && { limit: params.limit.toString() },
976
+ ...params.since_date && { since_date: params.since_date },
977
+ ...params.include_pending !== void 0 && { include_pending: String(params.include_pending) }
978
+ });
979
+ try {
980
+ return await this.request(`/v1/memory/session/${params.session_id}?${query}`);
981
+ } catch (error) {
982
+ if (!this.isEndpointNotFoundError(error)) {
983
+ throw error;
984
+ }
985
+ return { memories: [], count: 0 };
986
+ }
436
987
  });
437
- return this.request(`/v1/memory/session/${params.session_id}?${query}`);
438
988
  }
439
989
  async getUserProfile(params) {
440
- const project = await this.resolveProjectId(this.getRequiredProject(params.project));
441
- const query = new URLSearchParams({
442
- project,
443
- ...params.memory_types && { memory_types: params.memory_types }
990
+ const projectRef = this.getRequiredProject(params.project);
991
+ return this.withProjectRefFallback(projectRef, async (project) => {
992
+ const query = new URLSearchParams({
993
+ project,
994
+ ...params.memory_types && { memory_types: params.memory_types },
995
+ ...params.include_pending !== void 0 && { include_pending: String(params.include_pending) }
996
+ });
997
+ try {
998
+ return await this.request(`/v1/memory/profile/${params.user_id}?${query}`);
999
+ } catch (error) {
1000
+ if (!this.isEndpointNotFoundError(error)) {
1001
+ throw error;
1002
+ }
1003
+ const legacyQuery = new URLSearchParams({
1004
+ project,
1005
+ user_id: params.user_id,
1006
+ limit: "200"
1007
+ });
1008
+ const legacy = await this.request(`/v1/memories?${legacyQuery}`);
1009
+ const memories = Array.isArray(legacy?.memories) ? legacy.memories : [];
1010
+ return {
1011
+ user_id: params.user_id,
1012
+ memories,
1013
+ count: memories.length
1014
+ };
1015
+ }
444
1016
  });
445
- return this.request(`/v1/memory/profile/${params.user_id}?${query}`);
446
1017
  }
447
1018
  async getMemoryVersions(memoryId) {
448
1019
  return this.request(`/v1/memory/${memoryId}/versions`);
449
1020
  }
450
1021
  async updateMemory(memoryId, params) {
451
- return this.request(`/v1/memory/${memoryId}`, {
452
- method: "PUT",
453
- body: JSON.stringify(params)
454
- });
1022
+ try {
1023
+ return await this.request(`/v1/memory/${memoryId}`, {
1024
+ method: "PUT",
1025
+ body: JSON.stringify(params)
1026
+ });
1027
+ } catch (error) {
1028
+ if (!this.isEndpointNotFoundError(error)) {
1029
+ throw error;
1030
+ }
1031
+ const legacy = await this.request(`/v1/memories/${memoryId}`, {
1032
+ method: "PUT",
1033
+ body: JSON.stringify({
1034
+ content: params.content
1035
+ })
1036
+ });
1037
+ return {
1038
+ success: true,
1039
+ new_memory_id: legacy?.id || memoryId,
1040
+ old_memory_id: memoryId
1041
+ };
1042
+ }
455
1043
  }
456
1044
  async deleteMemory(memoryId) {
457
- return this.request(`/v1/memory/${memoryId}`, { method: "DELETE" });
1045
+ try {
1046
+ return await this.request(`/v1/memory/${memoryId}`, { method: "DELETE" });
1047
+ } catch (error) {
1048
+ if (!this.isEndpointNotFoundError(error)) {
1049
+ throw error;
1050
+ }
1051
+ await this.request(`/v1/memories/${memoryId}`, { method: "DELETE" });
1052
+ return {
1053
+ success: true,
1054
+ deleted: memoryId
1055
+ };
1056
+ }
458
1057
  }
459
1058
  async getMemoryRelations(memoryId) {
460
1059
  return this.request(`/v1/memory/${memoryId}/relations`);
461
1060
  }
1061
+ async getMemoryGraph(params) {
1062
+ const project = await this.resolveProjectId(this.getRequiredProject(params.project));
1063
+ const query = new URLSearchParams({
1064
+ project,
1065
+ ...params.user_id && { user_id: params.user_id },
1066
+ ...params.session_id && { session_id: params.session_id },
1067
+ ...params.include_inactive !== void 0 && { include_inactive: String(params.include_inactive) },
1068
+ ...params.limit !== void 0 && { limit: String(params.limit) }
1069
+ });
1070
+ return this.request(`/v1/memory/graph?${query}`);
1071
+ }
1072
+ async getConversationGraph(params) {
1073
+ const project = await this.resolveProjectId(this.getRequiredProject(params.project));
1074
+ const query = new URLSearchParams({
1075
+ project,
1076
+ ...params.include_inactive !== void 0 && { include_inactive: String(params.include_inactive) },
1077
+ ...params.limit !== void 0 && { limit: String(params.limit) }
1078
+ });
1079
+ return this.request(`/v1/memory/graph/conversation/${params.session_id}?${query}`);
1080
+ }
462
1081
  async oracleSearch(params) {
463
1082
  const project = await this.resolveProjectId(this.getRequiredProject(params.project));
464
1083
  return this.request("/v1/oracle/search", {
@@ -543,82 +1162,610 @@ var WhisperContext = class _WhisperContext {
543
1162
  });
544
1163
  return this.request(`/v1/cost/breakdown?${query}`);
545
1164
  }
546
- async getCostSavings(params = {}) {
547
- const resolvedProject = params.project ? await this.resolveProjectId(params.project) : void 0;
548
- const query = new URLSearchParams({
549
- ...resolvedProject && { project: resolvedProject },
550
- ...params.start_date && { start_date: params.start_date },
551
- ...params.end_date && { end_date: params.end_date }
552
- });
553
- return this.request(`/v1/cost/savings?${query}`);
1165
+ /**
1166
+ * Semantic search over raw documents without pre-indexing.
1167
+ * Send file contents/summaries directly — the API embeds them in-memory and ranks by similarity.
1168
+ * Perfect for AI agents to semantically explore a codebase on-the-fly.
1169
+ */
1170
+ async semanticSearch(params) {
1171
+ return this.request("/v1/search/semantic", {
1172
+ method: "POST",
1173
+ body: JSON.stringify(params)
1174
+ });
1175
+ }
1176
+ async searchFiles(params) {
1177
+ return this.request("/v1/search/files", {
1178
+ method: "POST",
1179
+ body: JSON.stringify(params)
1180
+ });
1181
+ }
1182
+ async getCostSavings(params = {}) {
1183
+ const resolvedProject = params.project ? await this.resolveProjectId(params.project) : void 0;
1184
+ const query = new URLSearchParams({
1185
+ ...resolvedProject && { project: resolvedProject },
1186
+ ...params.start_date && { start_date: params.start_date },
1187
+ ...params.end_date && { end_date: params.end_date }
1188
+ });
1189
+ return this.request(`/v1/cost/savings?${query}`);
1190
+ }
1191
+ // Backward-compatible grouped namespaces.
1192
+ projects = {
1193
+ create: (params) => this.createProject(params),
1194
+ list: () => this.listProjects(),
1195
+ get: (id) => this.getProject(id),
1196
+ delete: (id) => this.deleteProject(id)
1197
+ };
1198
+ sources = {
1199
+ add: (projectId, params) => this.addSource(projectId, params),
1200
+ sync: (sourceId) => this.syncSource(sourceId),
1201
+ syncSource: (sourceId) => this.syncSource(sourceId)
1202
+ };
1203
+ memory = {
1204
+ add: (params) => this.addMemory(params),
1205
+ addBulk: (params) => this.addMemoriesBulk(params),
1206
+ extract: (params) => this.extractMemories(params),
1207
+ extractSession: (params) => this.extractSessionMemories(params),
1208
+ search: (params) => this.searchMemories(params),
1209
+ searchSOTA: (params) => this.searchMemoriesSOTA(params),
1210
+ ingestSession: (params) => this.ingestSession(params),
1211
+ getSessionMemories: (params) => this.getSessionMemories(params),
1212
+ getUserProfile: (params) => this.getUserProfile(params),
1213
+ getVersions: (memoryId) => this.getMemoryVersions(memoryId),
1214
+ update: (memoryId, params) => this.updateMemory(memoryId, params),
1215
+ delete: (memoryId) => this.deleteMemory(memoryId),
1216
+ getRelations: (memoryId) => this.getMemoryRelations(memoryId),
1217
+ getGraph: (params) => this.getMemoryGraph(params),
1218
+ getConversationGraph: (params) => this.getConversationGraph(params),
1219
+ consolidate: (params) => this.consolidateMemories(params),
1220
+ updateDecay: (params) => this.updateImportanceDecay(params),
1221
+ getImportanceStats: (project) => this.getImportanceStats(project)
1222
+ };
1223
+ keys = {
1224
+ create: (params) => this.createApiKey(params),
1225
+ list: () => this.listApiKeys(),
1226
+ getUsage: (days) => this.getUsage(days)
1227
+ };
1228
+ oracle = {
1229
+ search: (params) => this.oracleSearch(params)
1230
+ };
1231
+ context = {
1232
+ createShare: (params) => this.createSharedContext(params),
1233
+ loadShare: (shareId) => this.loadSharedContext(shareId),
1234
+ resumeShare: (params) => this.resumeFromSharedContext(params)
1235
+ };
1236
+ optimization = {
1237
+ getCacheStats: () => this.getCacheStats(),
1238
+ warmCache: (params) => this.warmCache(params),
1239
+ clearCache: (params) => this.clearCache(params),
1240
+ getCostSummary: (params) => this.getCostSummary(params),
1241
+ getCostBreakdown: (params) => this.getCostBreakdown(params),
1242
+ getCostSavings: (params) => this.getCostSavings(params)
1243
+ };
1244
+ };
1245
+
1246
+ // ../src/mcp/server.ts
1247
+ var API_KEY = process.env.WHISPER_API_KEY || "";
1248
+ var DEFAULT_PROJECT = process.env.WHISPER_PROJECT || "";
1249
+ var BASE_URL = process.env.WHISPER_BASE_URL;
1250
+ if (!API_KEY) {
1251
+ console.error("Error: WHISPER_API_KEY environment variable is required");
1252
+ process.exit(1);
1253
+ }
1254
+ var whisper = new WhisperContext({
1255
+ apiKey: API_KEY,
1256
+ project: DEFAULT_PROJECT,
1257
+ ...BASE_URL && { baseUrl: BASE_URL }
1258
+ });
1259
+ var server = new McpServer({
1260
+ name: "whisper-context",
1261
+ version: "0.2.8"
1262
+ });
1263
+ var STATE_DIR = join(homedir(), ".whisper-mcp");
1264
+ var STATE_PATH = join(STATE_DIR, "state.json");
1265
+ var AUDIT_LOG_PATH = join(STATE_DIR, "forget-audit.log");
1266
+ function ensureStateDir() {
1267
+ if (!existsSync(STATE_DIR)) {
1268
+ mkdirSync(STATE_DIR, { recursive: true });
1269
+ }
1270
+ }
1271
+ function getWorkspaceId(workspaceId) {
1272
+ if (workspaceId?.trim()) return workspaceId.trim();
1273
+ const seed = `${process.cwd()}|${DEFAULT_PROJECT || "default"}|${API_KEY.slice(0, 12)}`;
1274
+ return createHash("sha256").update(seed).digest("hex").slice(0, 20);
1275
+ }
1276
+ function getWorkspaceIdForPath(path, workspaceId) {
1277
+ if (workspaceId?.trim()) return workspaceId.trim();
1278
+ if (!path) return getWorkspaceId(void 0);
1279
+ const seed = `${path}|${DEFAULT_PROJECT || "default"}|${API_KEY.slice(0, 12)}`;
1280
+ return createHash("sha256").update(seed).digest("hex").slice(0, 20);
1281
+ }
1282
+ function clamp01(value) {
1283
+ if (Number.isNaN(value)) return 0;
1284
+ if (value < 0) return 0;
1285
+ if (value > 1) return 1;
1286
+ return value;
1287
+ }
1288
+ function renderCitation(ev) {
1289
+ return ev.line_end && ev.line_end !== ev.line_start ? `${ev.path}:${ev.line_start}-${ev.line_end}` : `${ev.path}:${ev.line_start}`;
1290
+ }
1291
+ function extractLineStart(metadata) {
1292
+ const raw = metadata.line_start ?? metadata.line ?? metadata.start_line ?? metadata.startLine ?? 1;
1293
+ const parsed = Number(raw);
1294
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 1;
1295
+ }
1296
+ function extractLineEnd(metadata, start) {
1297
+ const raw = metadata.line_end ?? metadata.end_line ?? metadata.endLine;
1298
+ if (raw === void 0 || raw === null) return void 0;
1299
+ const parsed = Number(raw);
1300
+ if (!Number.isFinite(parsed) || parsed < start) return void 0;
1301
+ return Math.floor(parsed);
1302
+ }
1303
+ function toEvidenceRef(source, workspaceId, methodFallback) {
1304
+ const metadata = source.metadata || {};
1305
+ const lineStart = extractLineStart(metadata);
1306
+ const lineEnd = extractLineEnd(metadata, lineStart);
1307
+ const path = String(
1308
+ metadata.file_path ?? metadata.path ?? source.source ?? source.document ?? source.id ?? "unknown"
1309
+ );
1310
+ const rawMethod = String(source.retrieval_source || methodFallback).toLowerCase();
1311
+ const retrievalMethod = rawMethod.includes("graph") ? "graph" : rawMethod.includes("memory") ? "memory" : rawMethod.includes("lex") ? "lexical" : rawMethod.includes("symbol") ? "symbol" : "semantic";
1312
+ return {
1313
+ evidence_id: randomUUID(),
1314
+ source_id: String(source.id || source.document || source.source || randomUUID()),
1315
+ path,
1316
+ line_start: lineStart,
1317
+ ...lineEnd ? { line_end: lineEnd } : {},
1318
+ snippet: String(source.content || metadata.snippet || "").slice(0, 500),
1319
+ score: clamp01(Number(source.score ?? metadata.score ?? 0)),
1320
+ retrieval_method: retrievalMethod,
1321
+ indexed_at: String(metadata.indexed_at || (/* @__PURE__ */ new Date()).toISOString()),
1322
+ ...metadata.commit ? { commit: String(metadata.commit) } : {},
1323
+ workspace_id: workspaceId,
1324
+ metadata: {
1325
+ source: String(source.source || ""),
1326
+ document: String(source.document || "")
1327
+ }
1328
+ };
1329
+ }
1330
+ function loadState() {
1331
+ ensureStateDir();
1332
+ if (!existsSync(STATE_PATH)) {
1333
+ return { workspaces: {} };
1334
+ }
1335
+ try {
1336
+ const parsed = JSON.parse(readFileSync(STATE_PATH, "utf-8"));
1337
+ if (!parsed || typeof parsed !== "object") return { workspaces: {} };
1338
+ return parsed;
1339
+ } catch {
1340
+ return { workspaces: {} };
1341
+ }
1342
+ }
1343
+ function saveState(state) {
1344
+ ensureStateDir();
1345
+ writeFileSync(STATE_PATH, JSON.stringify(state, null, 2), "utf-8");
1346
+ }
1347
+ function getWorkspaceState(state, workspaceId) {
1348
+ if (!state.workspaces[workspaceId]) {
1349
+ state.workspaces[workspaceId] = {
1350
+ decisions: [],
1351
+ failures: [],
1352
+ entities: [],
1353
+ documents: [],
1354
+ annotations: [],
1355
+ session_summaries: [],
1356
+ events: [],
1357
+ index_metadata: {}
1358
+ };
1359
+ }
1360
+ return state.workspaces[workspaceId];
1361
+ }
1362
+ function computeChecksum(value) {
1363
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
1364
+ }
1365
+ var cachedProjectRef = DEFAULT_PROJECT || void 0;
1366
+ async function resolveProjectRef(explicit) {
1367
+ if (explicit?.trim()) return explicit.trim();
1368
+ if (cachedProjectRef) return cachedProjectRef;
1369
+ try {
1370
+ const { projects } = await whisper.listProjects();
1371
+ const first = projects?.[0];
1372
+ if (!first) return void 0;
1373
+ cachedProjectRef = first.slug || first.name || first.id;
1374
+ return cachedProjectRef;
1375
+ } catch {
1376
+ return void 0;
1377
+ }
1378
+ }
1379
+ function buildAbstain(args) {
1380
+ return {
1381
+ status: "abstained",
1382
+ answer: null,
1383
+ reason: args.reason,
1384
+ message: args.message,
1385
+ closest_evidence: args.closest_evidence,
1386
+ recommended_next_calls: ["repo_index_status", "index_workspace", "symbol_search", "get_relevant_context"],
1387
+ diagnostics: {
1388
+ claims_evaluated: args.claims_evaluated,
1389
+ evidence_items_found: args.evidence_items_found,
1390
+ min_required: args.min_required,
1391
+ index_fresh: args.index_fresh
1392
+ }
1393
+ };
1394
+ }
1395
+ function getGitHead(searchPath) {
1396
+ const root = searchPath || process.cwd();
1397
+ const result = spawnSync("git", ["-C", root, "rev-parse", "HEAD"], { encoding: "utf-8" });
1398
+ if (result.status !== 0) return void 0;
1399
+ const out = String(result.stdout || "").trim();
1400
+ return out || void 0;
1401
+ }
1402
+ function getGitPendingCount(searchPath) {
1403
+ const root = searchPath || process.cwd();
1404
+ const result = spawnSync("git", ["-C", root, "status", "--porcelain"], { encoding: "utf-8" });
1405
+ if (result.status !== 0) return void 0;
1406
+ const out = String(result.stdout || "").trim();
1407
+ if (!out) return 0;
1408
+ return out.split("\n").filter(Boolean).length;
1409
+ }
1410
+ function countCodeFiles(searchPath, maxFiles = 5e3) {
1411
+ const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", "build", "__pycache__", ".turbo", "coverage", ".cache"]);
1412
+ const exts = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "py", "go", "rs", "java", "cpp", "c", "cs", "rb", "php", "swift", "kt", "sql", "prisma", "graphql", "json", "yaml", "yml", "toml", "env"]);
1413
+ let total = 0;
1414
+ let skipped = 0;
1415
+ function walk(dir) {
1416
+ if (total >= maxFiles) return;
1417
+ let entries;
1418
+ try {
1419
+ entries = readdirSync(dir, { withFileTypes: true });
1420
+ } catch {
1421
+ return;
1422
+ }
1423
+ for (const e of entries) {
1424
+ if (total >= maxFiles) return;
1425
+ if (skip.has(e.name)) {
1426
+ skipped += 1;
1427
+ continue;
1428
+ }
1429
+ const full = join(dir, e.name);
1430
+ if (e.isDirectory()) walk(full);
1431
+ else if (e.isFile()) {
1432
+ const ext = extname(e.name).replace(".", "");
1433
+ if (exts.has(ext)) total += 1;
1434
+ }
1435
+ }
1436
+ }
1437
+ walk(searchPath);
1438
+ return { total, skipped };
1439
+ }
1440
+ server.tool(
1441
+ "resolve_workspace",
1442
+ "Resolve workspace identity from path + API key and map to a project without mandatory dashboard setup.",
1443
+ {
1444
+ path: z.string().optional().describe("Workspace path. Defaults to current working directory."),
1445
+ workspace_id: z.string().optional(),
1446
+ project: z.string().optional()
1447
+ },
1448
+ async ({ path, workspace_id, project }) => {
1449
+ try {
1450
+ const workspaceId = getWorkspaceIdForPath(path, workspace_id);
1451
+ const state = loadState();
1452
+ const existed = Boolean(state.workspaces[workspaceId]);
1453
+ const workspace = getWorkspaceState(state, workspaceId);
1454
+ const resolvedProject = await resolveProjectRef(project);
1455
+ const resolvedBy = project?.trim() ? "explicit_project" : DEFAULT_PROJECT ? "env_default" : resolvedProject ? "auto_first_project" : "unresolved";
1456
+ saveState(state);
1457
+ const payload = {
1458
+ workspace_id: workspaceId,
1459
+ project_id: resolvedProject || null,
1460
+ created: !existed,
1461
+ resolved_by: resolvedBy,
1462
+ index_state: {
1463
+ last_indexed_at: workspace.index_metadata?.last_indexed_at || null,
1464
+ last_indexed_commit: workspace.index_metadata?.last_indexed_commit || null,
1465
+ coverage: workspace.index_metadata?.coverage ?? 0
1466
+ }
1467
+ };
1468
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
1469
+ } catch (error) {
1470
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
1471
+ }
1472
+ }
1473
+ );
1474
+ server.tool(
1475
+ "repo_index_status",
1476
+ "Check index freshness, coverage, commit, and pending changes before retrieval/edits.",
1477
+ {
1478
+ workspace_id: z.string().optional(),
1479
+ path: z.string().optional()
1480
+ },
1481
+ async ({ workspace_id, path }) => {
1482
+ try {
1483
+ const rootPath = path || process.cwd();
1484
+ const workspaceId = getWorkspaceIdForPath(rootPath, workspace_id);
1485
+ const state = loadState();
1486
+ const workspace = getWorkspaceState(state, workspaceId);
1487
+ const lastIndexedAt = workspace.index_metadata?.last_indexed_at;
1488
+ const ageHours = lastIndexedAt ? (Date.now() - new Date(lastIndexedAt).getTime()) / (60 * 60 * 1e3) : null;
1489
+ const stale = ageHours === null ? true : ageHours > 168;
1490
+ const payload = {
1491
+ workspace_id: workspaceId,
1492
+ freshness: {
1493
+ stale,
1494
+ age_hours: ageHours,
1495
+ last_indexed_at: lastIndexedAt || null
1496
+ },
1497
+ coverage: workspace.index_metadata?.coverage ?? 0,
1498
+ last_indexed_commit: workspace.index_metadata?.last_indexed_commit || null,
1499
+ current_commit: getGitHead(rootPath) || null,
1500
+ pending_changes: getGitPendingCount(rootPath)
1501
+ };
1502
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
1503
+ } catch (error) {
1504
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
1505
+ }
1506
+ }
1507
+ );
1508
+ server.tool(
1509
+ "index_workspace",
1510
+ "Index workspace in full or incremental mode and update index metadata for freshness checks.",
1511
+ {
1512
+ workspace_id: z.string().optional(),
1513
+ path: z.string().optional(),
1514
+ mode: z.enum(["full", "incremental"]).optional().default("incremental"),
1515
+ max_files: z.number().optional().default(1500)
1516
+ },
1517
+ async ({ workspace_id, path, mode, max_files }) => {
1518
+ try {
1519
+ const rootPath = path || process.cwd();
1520
+ const workspaceId = getWorkspaceIdForPath(rootPath, workspace_id);
1521
+ const state = loadState();
1522
+ const workspace = getWorkspaceState(state, workspaceId);
1523
+ const fileStats = countCodeFiles(rootPath, max_files);
1524
+ const coverage = Math.max(0, Math.min(1, fileStats.total / Math.max(1, max_files)));
1525
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1526
+ workspace.index_metadata = {
1527
+ last_indexed_at: now,
1528
+ last_indexed_commit: getGitHead(rootPath),
1529
+ coverage: mode === "full" ? 1 : coverage
1530
+ };
1531
+ saveState(state);
1532
+ const payload = {
1533
+ workspace_id: workspaceId,
1534
+ mode,
1535
+ indexed_files: fileStats.total,
1536
+ skipped_files: fileStats.skipped,
1537
+ duration_ms: 0,
1538
+ warnings: fileStats.total === 0 ? ["No code files discovered for indexing."] : [],
1539
+ index_metadata: workspace.index_metadata
1540
+ };
1541
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
1542
+ } catch (error) {
1543
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
1544
+ }
1545
+ }
1546
+ );
1547
+ server.tool(
1548
+ "get_relevant_context",
1549
+ "Core retrieval. Task goes in, ranked context chunks come out with structured evidence (file:line ready).",
1550
+ {
1551
+ question: z.string().describe("Task/question to retrieve context for"),
1552
+ workspace_id: z.string().optional(),
1553
+ project: z.string().optional(),
1554
+ top_k: z.number().optional().default(12),
1555
+ include_memories: z.boolean().optional().default(true),
1556
+ include_graph: z.boolean().optional().default(true),
1557
+ session_id: z.string().optional(),
1558
+ user_id: z.string().optional()
1559
+ },
1560
+ async ({ question, workspace_id, project, top_k, include_memories, include_graph, session_id, user_id }) => {
1561
+ try {
1562
+ const workspaceId = getWorkspaceId(workspace_id);
1563
+ const resolvedProject = await resolveProjectRef(project);
1564
+ if (!resolvedProject) {
1565
+ const payload2 = {
1566
+ question,
1567
+ workspace_id: workspaceId,
1568
+ total_results: 0,
1569
+ context: "",
1570
+ evidence: [],
1571
+ used_context_ids: [],
1572
+ latency_ms: 0,
1573
+ warning: "No project resolved. Set WHISPER_PROJECT or create one in your account."
1574
+ };
1575
+ return { content: [{ type: "text", text: JSON.stringify(payload2, null, 2) }] };
1576
+ }
1577
+ const response = await whisper.query({
1578
+ project: resolvedProject,
1579
+ query: question,
1580
+ top_k,
1581
+ include_memories,
1582
+ include_graph,
1583
+ session_id,
1584
+ user_id
1585
+ });
1586
+ const evidence = (response.results || []).map((r) => toEvidenceRef(r, workspaceId, "semantic"));
1587
+ const payload = {
1588
+ question,
1589
+ workspace_id: workspaceId,
1590
+ total_results: response.meta?.total || evidence.length,
1591
+ context: response.context || "",
1592
+ evidence,
1593
+ used_context_ids: (response.results || []).map((r) => String(r.id)),
1594
+ latency_ms: response.meta?.latency_ms || 0
1595
+ };
1596
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
1597
+ } catch (error) {
1598
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
1599
+ }
1600
+ }
1601
+ );
1602
+ server.tool(
1603
+ "claim_verifier",
1604
+ "Verify whether a claim is supported by retrieved context. Returns supported/partial/unsupported with evidence.",
1605
+ {
1606
+ claim: z.string().describe("Claim to verify"),
1607
+ workspace_id: z.string().optional(),
1608
+ project: z.string().optional(),
1609
+ context_ids: z.array(z.string()).optional(),
1610
+ strict: z.boolean().optional().default(true)
1611
+ },
1612
+ async ({ claim, workspace_id, project, context_ids, strict }) => {
1613
+ try {
1614
+ const workspaceId = getWorkspaceId(workspace_id);
1615
+ const resolvedProject = await resolveProjectRef(project);
1616
+ if (!resolvedProject) {
1617
+ const payload2 = {
1618
+ verdict: "unsupported",
1619
+ confidence: 0,
1620
+ evidence: [],
1621
+ missing_requirements: ["No project resolved. Set WHISPER_PROJECT or create one in your account."],
1622
+ explanation: "Verifier could not run because no project is configured."
1623
+ };
1624
+ return { content: [{ type: "text", text: JSON.stringify(payload2, null, 2) }] };
1625
+ }
1626
+ const response = await whisper.query({
1627
+ project: resolvedProject,
1628
+ query: claim,
1629
+ top_k: strict ? 8 : 12,
1630
+ include_memories: true,
1631
+ include_graph: true
1632
+ });
1633
+ const filtered = (response.results || []).filter(
1634
+ (r) => !context_ids || context_ids.length === 0 || context_ids.includes(String(r.id))
1635
+ );
1636
+ const evidence = filtered.map((r) => toEvidenceRef(r, workspaceId, "semantic"));
1637
+ const directEvidence = evidence.filter((e) => e.score >= (strict ? 0.7 : 0.6));
1638
+ const weakEvidence = evidence.filter((e) => e.score >= (strict ? 0.45 : 0.35));
1639
+ let verdict = "unsupported";
1640
+ if (directEvidence.length > 0) verdict = "supported";
1641
+ else if (weakEvidence.length > 0) verdict = "partial";
1642
+ const payload = {
1643
+ verdict,
1644
+ confidence: evidence.length ? Math.max(...evidence.map((e) => e.score)) : 0,
1645
+ evidence: verdict === "supported" ? directEvidence : weakEvidence,
1646
+ missing_requirements: verdict === "supported" ? [] : verdict === "partial" ? ["No direct evidence spans met strict threshold."] : ["No sufficient supporting evidence found for the claim."],
1647
+ explanation: verdict === "supported" ? "At least one direct evidence span supports the claim." : verdict === "partial" ? "Some related evidence exists, but direct support is incomplete." : "Retrieved context did not contain sufficient support."
1648
+ };
1649
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
1650
+ } catch (error) {
1651
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
1652
+ }
1653
+ }
1654
+ );
1655
+ server.tool(
1656
+ "evidence_locked_answer",
1657
+ "Answer a question only when evidence requirements are met. Fails closed with an abstain payload when not verifiable.",
1658
+ {
1659
+ question: z.string(),
1660
+ workspace_id: z.string().optional(),
1661
+ project: z.string().optional(),
1662
+ constraints: z.object({
1663
+ require_citations: z.boolean().optional().default(true),
1664
+ min_evidence_items: z.number().optional().default(2),
1665
+ min_confidence: z.number().optional().default(0.65),
1666
+ max_staleness_hours: z.number().optional().default(168)
1667
+ }).optional(),
1668
+ retrieval: z.object({
1669
+ top_k: z.number().optional().default(12),
1670
+ include_symbols: z.boolean().optional().default(true),
1671
+ include_recent_decisions: z.boolean().optional().default(true)
1672
+ }).optional()
1673
+ },
1674
+ async ({ question, workspace_id, project, constraints, retrieval }) => {
1675
+ try {
1676
+ const workspaceId = getWorkspaceId(workspace_id);
1677
+ const requireCitations = constraints?.require_citations ?? true;
1678
+ const minEvidenceItems = constraints?.min_evidence_items ?? 2;
1679
+ const minConfidence = constraints?.min_confidence ?? 0.65;
1680
+ const maxStalenessHours = constraints?.max_staleness_hours ?? 168;
1681
+ const topK = retrieval?.top_k ?? 12;
1682
+ const resolvedProject = await resolveProjectRef(project);
1683
+ if (!resolvedProject) {
1684
+ const abstain = buildAbstain({
1685
+ reason: "no_retrieval_hits",
1686
+ message: "No project resolved. Set WHISPER_PROJECT or create one in your account.",
1687
+ closest_evidence: [],
1688
+ claims_evaluated: 1,
1689
+ evidence_items_found: 0,
1690
+ min_required: minEvidenceItems,
1691
+ index_fresh: true
1692
+ });
1693
+ return { content: [{ type: "text", text: JSON.stringify(abstain, null, 2) }] };
1694
+ }
1695
+ const response = await whisper.query({
1696
+ project: resolvedProject,
1697
+ query: question,
1698
+ top_k: topK,
1699
+ include_memories: true,
1700
+ include_graph: true
1701
+ });
1702
+ const evidence = (response.results || []).map((r) => toEvidenceRef(r, workspaceId, "semantic"));
1703
+ const sorted = evidence.sort((a, b) => b.score - a.score);
1704
+ const confidence = sorted.length ? sorted[0].score : 0;
1705
+ const state = loadState();
1706
+ const workspace = getWorkspaceState(state, workspaceId);
1707
+ const lastIndexedAt = workspace.index_metadata?.last_indexed_at;
1708
+ const indexFresh = !lastIndexedAt || Date.now() - new Date(lastIndexedAt).getTime() <= maxStalenessHours * 60 * 60 * 1e3;
1709
+ if (sorted.length === 0) {
1710
+ const abstain = buildAbstain({
1711
+ reason: "no_retrieval_hits",
1712
+ message: "No retrieval hits were found for this question.",
1713
+ closest_evidence: [],
1714
+ claims_evaluated: 1,
1715
+ evidence_items_found: 0,
1716
+ min_required: minEvidenceItems,
1717
+ index_fresh: indexFresh
1718
+ });
1719
+ return { content: [{ type: "text", text: JSON.stringify(abstain, null, 2) }] };
1720
+ }
1721
+ const supportedEvidence = sorted.filter((e) => e.score >= minConfidence);
1722
+ const verdict = supportedEvidence.length >= 1 ? "supported" : "partial";
1723
+ if (!indexFresh) {
1724
+ const abstain = buildAbstain({
1725
+ reason: "stale_index",
1726
+ message: "Index freshness requirement not met. Re-index before answering.",
1727
+ closest_evidence: sorted.slice(0, 3),
1728
+ claims_evaluated: 1,
1729
+ evidence_items_found: supportedEvidence.length,
1730
+ min_required: minEvidenceItems,
1731
+ index_fresh: false
1732
+ });
1733
+ return { content: [{ type: "text", text: JSON.stringify(abstain, null, 2) }] };
1734
+ }
1735
+ if (requireCitations && (verdict !== "supported" || supportedEvidence.length < minEvidenceItems)) {
1736
+ const abstain = buildAbstain({
1737
+ reason: "insufficient_evidence",
1738
+ message: "Citation or verification thresholds were not met.",
1739
+ closest_evidence: sorted.slice(0, 3),
1740
+ claims_evaluated: 1,
1741
+ evidence_items_found: supportedEvidence.length,
1742
+ min_required: minEvidenceItems,
1743
+ index_fresh: true
1744
+ });
1745
+ return { content: [{ type: "text", text: JSON.stringify(abstain, null, 2) }] };
1746
+ }
1747
+ const citations = supportedEvidence.slice(0, Math.max(minEvidenceItems, 3));
1748
+ const answerLines = citations.map(
1749
+ (ev, idx) => `${idx + 1}. [${renderCitation(ev)}] ${ev.snippet || "Relevant context found."}`
1750
+ );
1751
+ const answered = {
1752
+ status: "answered",
1753
+ answer: answerLines.join("\n"),
1754
+ citations,
1755
+ confidence,
1756
+ verification: {
1757
+ verdict,
1758
+ supported_claims: verdict === "supported" ? 1 : 0,
1759
+ total_claims: 1
1760
+ },
1761
+ used_context_ids: citations.map((c) => c.source_id)
1762
+ };
1763
+ return { content: [{ type: "text", text: JSON.stringify(answered, null, 2) }] };
1764
+ } catch (error) {
1765
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
1766
+ }
554
1767
  }
555
- // Backward-compatible grouped namespaces.
556
- projects = {
557
- create: (params) => this.createProject(params),
558
- list: () => this.listProjects(),
559
- get: (id) => this.getProject(id),
560
- delete: (id) => this.deleteProject(id)
561
- };
562
- sources = {
563
- add: (projectId, params) => this.addSource(projectId, params),
564
- sync: (sourceId) => this.syncSource(sourceId),
565
- syncSource: (sourceId) => this.syncSource(sourceId)
566
- };
567
- memory = {
568
- add: (params) => this.addMemory(params),
569
- search: (params) => this.searchMemories(params),
570
- searchSOTA: (params) => this.searchMemoriesSOTA(params),
571
- ingestSession: (params) => this.ingestSession(params),
572
- getSessionMemories: (params) => this.getSessionMemories(params),
573
- getUserProfile: (params) => this.getUserProfile(params),
574
- getVersions: (memoryId) => this.getMemoryVersions(memoryId),
575
- update: (memoryId, params) => this.updateMemory(memoryId, params),
576
- delete: (memoryId) => this.deleteMemory(memoryId),
577
- getRelations: (memoryId) => this.getMemoryRelations(memoryId),
578
- consolidate: (params) => this.consolidateMemories(params),
579
- updateDecay: (params) => this.updateImportanceDecay(params),
580
- getImportanceStats: (project) => this.getImportanceStats(project)
581
- };
582
- keys = {
583
- create: (params) => this.createApiKey(params),
584
- list: () => this.listApiKeys(),
585
- getUsage: (days) => this.getUsage(days)
586
- };
587
- oracle = {
588
- search: (params) => this.oracleSearch(params)
589
- };
590
- context = {
591
- createShare: (params) => this.createSharedContext(params),
592
- loadShare: (shareId) => this.loadSharedContext(shareId),
593
- resumeShare: (params) => this.resumeFromSharedContext(params)
594
- };
595
- optimization = {
596
- getCacheStats: () => this.getCacheStats(),
597
- warmCache: (params) => this.warmCache(params),
598
- clearCache: (params) => this.clearCache(params),
599
- getCostSummary: (params) => this.getCostSummary(params),
600
- getCostBreakdown: (params) => this.getCostBreakdown(params),
601
- getCostSavings: (params) => this.getCostSavings(params)
602
- };
603
- };
604
-
605
- // ../src/mcp/server.ts
606
- var API_KEY = process.env.WHISPER_API_KEY || "";
607
- var DEFAULT_PROJECT = process.env.WHISPER_PROJECT || "";
608
- var BASE_URL = process.env.WHISPER_BASE_URL;
609
- if (!API_KEY) {
610
- console.error("Error: WHISPER_API_KEY environment variable is required");
611
- process.exit(1);
612
- }
613
- var whisper = new WhisperContext({
614
- apiKey: API_KEY,
615
- project: DEFAULT_PROJECT,
616
- ...BASE_URL && { baseUrl: BASE_URL }
617
- });
618
- var server = new McpServer({
619
- name: "whisper-context",
620
- version: "0.2.8"
621
- });
1768
+ );
622
1769
  server.tool(
623
1770
  "query_context",
624
1771
  "Search your knowledge base for relevant context. Returns packed context ready for LLM consumption. Supports hybrid vector+keyword search, memory inclusion, and knowledge graph traversal.",
@@ -1051,6 +2198,663 @@ server.tool(
1051
2198
  }
1052
2199
  }
1053
2200
  );
2201
+ server.tool(
2202
+ "forget",
2203
+ "Delete or invalidate memories with immutable audit logging.",
2204
+ {
2205
+ workspace_id: z.string().optional(),
2206
+ project: z.string().optional(),
2207
+ target: z.object({
2208
+ memory_id: z.string().optional(),
2209
+ query: z.string().optional()
2210
+ }),
2211
+ mode: z.enum(["delete", "invalidate"]).optional().default("invalidate"),
2212
+ reason: z.string().optional()
2213
+ },
2214
+ async ({ workspace_id, project, target, mode, reason }) => {
2215
+ try {
2216
+ if (!target.memory_id && !target.query) {
2217
+ return { content: [{ type: "text", text: "Error: target.memory_id or target.query is required." }] };
2218
+ }
2219
+ const affectedIds = [];
2220
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2221
+ const actor = process.env.WHISPER_AGENT_ID || process.env.USERNAME || "api_key_principal";
2222
+ const resolvedProject = await resolveProjectRef(project);
2223
+ async function applyToMemory(memoryId) {
2224
+ if (mode === "delete") {
2225
+ await whisper.deleteMemory(memoryId);
2226
+ } else {
2227
+ try {
2228
+ await whisper.updateMemory(memoryId, {
2229
+ content: `[INVALIDATED at ${now}] ${reason || "Invalidated by forget tool."}`,
2230
+ reasoning: reason || "No reason provided"
2231
+ });
2232
+ } catch {
2233
+ if (resolvedProject) {
2234
+ await whisper.addMemory({
2235
+ project: resolvedProject,
2236
+ content: `Invalidated memory ${memoryId} at ${now}. Reason: ${reason || "No reason provided"}`,
2237
+ memory_type: "instruction",
2238
+ importance: 1
2239
+ });
2240
+ }
2241
+ }
2242
+ }
2243
+ affectedIds.push(memoryId);
2244
+ }
2245
+ if (target.memory_id) {
2246
+ await applyToMemory(target.memory_id);
2247
+ } else {
2248
+ if (!resolvedProject) {
2249
+ ensureStateDir();
2250
+ const audit2 = {
2251
+ audit_id: randomUUID(),
2252
+ actor,
2253
+ called_at: now,
2254
+ mode,
2255
+ ...reason ? { reason } : {},
2256
+ affected_ids: affectedIds
2257
+ };
2258
+ appendFileSync(AUDIT_LOG_PATH, `${JSON.stringify(audit2)}
2259
+ `, "utf-8");
2260
+ const payload2 = {
2261
+ status: "completed",
2262
+ affected_ids: affectedIds,
2263
+ audit: {
2264
+ audit_id: audit2.audit_id,
2265
+ actor: audit2.actor,
2266
+ called_at: audit2.called_at,
2267
+ mode: audit2.mode,
2268
+ ...audit2.reason ? { reason: audit2.reason } : {}
2269
+ },
2270
+ warning: "No project resolved for query-based forget. Nothing was changed."
2271
+ };
2272
+ return { content: [{ type: "text", text: JSON.stringify(payload2, null, 2) }] };
2273
+ }
2274
+ const search = await whisper.searchMemoriesSOTA({
2275
+ project: resolvedProject,
2276
+ query: target.query || "",
2277
+ top_k: 25,
2278
+ include_relations: false
2279
+ });
2280
+ const memoryIds = (search.results || []).map((r) => String(r?.memory?.id || "")).filter(Boolean);
2281
+ for (const id of memoryIds) {
2282
+ await applyToMemory(id);
2283
+ }
2284
+ }
2285
+ ensureStateDir();
2286
+ const audit = {
2287
+ audit_id: randomUUID(),
2288
+ actor,
2289
+ called_at: now,
2290
+ mode,
2291
+ ...reason ? { reason } : {},
2292
+ affected_ids: affectedIds
2293
+ };
2294
+ appendFileSync(AUDIT_LOG_PATH, `${JSON.stringify(audit)}
2295
+ `, "utf-8");
2296
+ const payload = {
2297
+ status: "completed",
2298
+ affected_ids: affectedIds,
2299
+ audit: {
2300
+ audit_id: audit.audit_id,
2301
+ actor: audit.actor,
2302
+ called_at: audit.called_at,
2303
+ mode: audit.mode,
2304
+ ...audit.reason ? { reason: audit.reason } : {}
2305
+ }
2306
+ };
2307
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
2308
+ } catch (error) {
2309
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
2310
+ }
2311
+ }
2312
+ );
2313
+ server.tool(
2314
+ "export_context_bundle",
2315
+ "Export project/workspace memory and context to a portable bundle with checksum.",
2316
+ {
2317
+ workspace_id: z.string().optional(),
2318
+ project: z.string().optional()
2319
+ },
2320
+ async ({ workspace_id, project }) => {
2321
+ try {
2322
+ const workspaceId = getWorkspaceId(workspace_id);
2323
+ const state = loadState();
2324
+ const workspace = getWorkspaceState(state, workspaceId);
2325
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2326
+ const resolvedProject = await resolveProjectRef(project);
2327
+ let memories = [];
2328
+ if (resolvedProject) {
2329
+ try {
2330
+ const m = await whisper.searchMemoriesSOTA({
2331
+ project: resolvedProject,
2332
+ query: "memory",
2333
+ top_k: 100,
2334
+ include_relations: true
2335
+ });
2336
+ memories = (m.results || []).map((r) => r.memory || r);
2337
+ } catch {
2338
+ memories = [];
2339
+ }
2340
+ }
2341
+ const contents = {
2342
+ memories,
2343
+ entities: workspace.entities,
2344
+ decisions: workspace.decisions,
2345
+ failures: workspace.failures,
2346
+ annotations: workspace.annotations,
2347
+ session_summaries: workspace.session_summaries,
2348
+ documents: workspace.documents,
2349
+ index_metadata: workspace.index_metadata || {}
2350
+ };
2351
+ const bundleWithoutChecksum = {
2352
+ bundle_version: "1.0",
2353
+ workspace_id: workspaceId,
2354
+ exported_at: now,
2355
+ contents
2356
+ };
2357
+ const checksum = computeChecksum(bundleWithoutChecksum);
2358
+ const bundle = { ...bundleWithoutChecksum, checksum };
2359
+ return { content: [{ type: "text", text: JSON.stringify(bundle, null, 2) }] };
2360
+ } catch (error) {
2361
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
2362
+ }
2363
+ }
2364
+ );
2365
+ server.tool(
2366
+ "import_context_bundle",
2367
+ "Import a portable context bundle with merge/replace modes and checksum verification.",
2368
+ {
2369
+ workspace_id: z.string().optional(),
2370
+ bundle: z.object({
2371
+ bundle_version: z.string(),
2372
+ workspace_id: z.string(),
2373
+ exported_at: z.string(),
2374
+ contents: z.object({
2375
+ memories: z.array(z.any()).default([]),
2376
+ entities: z.array(z.any()).default([]),
2377
+ decisions: z.array(z.any()).default([]),
2378
+ failures: z.array(z.any()).default([]),
2379
+ annotations: z.array(z.any()).default([]),
2380
+ session_summaries: z.array(z.any()).default([]),
2381
+ documents: z.array(z.any()).default([]),
2382
+ index_metadata: z.record(z.any()).default({})
2383
+ }),
2384
+ checksum: z.string()
2385
+ }),
2386
+ mode: z.enum(["merge", "replace"]).optional().default("merge"),
2387
+ dedupe_strategy: z.enum(["semantic", "id", "none"]).optional().default("semantic")
2388
+ },
2389
+ async ({ workspace_id, bundle, mode, dedupe_strategy }) => {
2390
+ try {
2391
+ let dedupe2 = function(existing, incoming) {
2392
+ if (dedupe_strategy === "none") return false;
2393
+ if (dedupe_strategy === "id") return existing.some((e) => e.id === incoming.id);
2394
+ const norm = incoming.content.toLowerCase().trim();
2395
+ return existing.some((e) => e.content.toLowerCase().trim() === norm);
2396
+ };
2397
+ var dedupe = dedupe2;
2398
+ const workspaceId = getWorkspaceId(workspace_id || bundle.workspace_id);
2399
+ const state = loadState();
2400
+ const workspace = getWorkspaceState(state, workspaceId);
2401
+ const { checksum, ...unsignedBundle } = bundle;
2402
+ const checksumVerified = checksum === computeChecksum(unsignedBundle);
2403
+ const conflicts = [];
2404
+ if (!checksumVerified) conflicts.push("checksum_mismatch");
2405
+ const importedCounts = {
2406
+ memories: 0,
2407
+ entities: 0,
2408
+ decisions: 0,
2409
+ failures: 0,
2410
+ annotations: 0,
2411
+ session_summaries: 0,
2412
+ documents: 0
2413
+ };
2414
+ const skippedCounts = {
2415
+ memories: 0,
2416
+ entities: 0,
2417
+ decisions: 0,
2418
+ failures: 0,
2419
+ annotations: 0,
2420
+ session_summaries: 0,
2421
+ documents: 0
2422
+ };
2423
+ if (mode === "replace") {
2424
+ workspace.entities = [];
2425
+ workspace.decisions = [];
2426
+ workspace.failures = [];
2427
+ workspace.annotations = [];
2428
+ workspace.session_summaries = [];
2429
+ workspace.documents = [];
2430
+ workspace.events = [];
2431
+ }
2432
+ const keys = ["entities", "decisions", "failures", "annotations", "session_summaries", "documents"];
2433
+ for (const key of keys) {
2434
+ const sourceItems = bundle.contents[key];
2435
+ for (const incoming of sourceItems || []) {
2436
+ const normalized = {
2437
+ id: incoming.id || randomUUID(),
2438
+ content: incoming.content || "",
2439
+ created_at: incoming.created_at || (/* @__PURE__ */ new Date()).toISOString(),
2440
+ ...incoming.session_id ? { session_id: incoming.session_id } : {},
2441
+ ...incoming.commit ? { commit: incoming.commit } : {},
2442
+ ...incoming.metadata ? { metadata: incoming.metadata } : {}
2443
+ };
2444
+ const targetArray = workspace[key];
2445
+ if (dedupe2(targetArray, normalized)) {
2446
+ skippedCounts[key] += 1;
2447
+ continue;
2448
+ }
2449
+ targetArray.push(normalized);
2450
+ importedCounts[key] += 1;
2451
+ }
2452
+ }
2453
+ workspace.index_metadata = bundle.contents.index_metadata || workspace.index_metadata || {};
2454
+ saveState(state);
2455
+ const payload = {
2456
+ imported_counts: importedCounts,
2457
+ skipped_counts: skippedCounts,
2458
+ conflicts,
2459
+ checksum_verified: checksumVerified
2460
+ };
2461
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
2462
+ } catch (error) {
2463
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
2464
+ }
2465
+ }
2466
+ );
2467
+ server.tool(
2468
+ "diff_context",
2469
+ "Return deterministic context changes from an explicit anchor (session_id, timestamp, or commit).",
2470
+ {
2471
+ workspace_id: z.string().optional(),
2472
+ anchor: z.object({
2473
+ type: z.enum(["session_id", "timestamp", "commit"]),
2474
+ value: z.string()
2475
+ }).optional(),
2476
+ scope: z.object({
2477
+ include: z.array(z.enum(["decisions", "failures", "entities", "documents", "summaries"])).optional().default(["decisions", "failures", "entities", "documents", "summaries"])
2478
+ }).optional()
2479
+ },
2480
+ async ({ workspace_id, anchor, scope }) => {
2481
+ try {
2482
+ const workspaceId = getWorkspaceId(workspace_id);
2483
+ const state = loadState();
2484
+ const workspace = getWorkspaceState(state, workspaceId);
2485
+ const include = new Set(scope?.include || ["decisions", "failures", "entities", "documents", "summaries"]);
2486
+ let anchorTimestamp = null;
2487
+ let fromAnchor = anchor || null;
2488
+ if (anchor?.type === "timestamp") {
2489
+ anchorTimestamp = new Date(anchor.value).toISOString();
2490
+ } else if (anchor?.type === "session_id") {
2491
+ const summary = workspace.session_summaries.filter((s) => s.session_id === anchor.value).sort((a, b) => a.created_at.localeCompare(b.created_at)).at(-1);
2492
+ anchorTimestamp = summary?.created_at || null;
2493
+ } else if (anchor?.type === "commit") {
2494
+ const hit = workspace.events.filter((e) => e.commit === anchor.value).sort((a, b) => a.at.localeCompare(b.at)).at(-1);
2495
+ anchorTimestamp = hit?.at || null;
2496
+ } else {
2497
+ const lastSummary = workspace.session_summaries.slice().sort((a, b) => a.created_at.localeCompare(b.created_at)).at(-1);
2498
+ if (lastSummary) {
2499
+ anchorTimestamp = lastSummary.created_at;
2500
+ fromAnchor = { type: "session_id", value: lastSummary.session_id || lastSummary.id };
2501
+ }
2502
+ }
2503
+ const after = (items) => !anchorTimestamp ? items : items.filter((i) => i.created_at > anchorTimestamp);
2504
+ const changes = {
2505
+ decisions: include.has("decisions") ? after(workspace.decisions) : [],
2506
+ failures: include.has("failures") ? after(workspace.failures) : [],
2507
+ entities: include.has("entities") ? after(workspace.entities) : [],
2508
+ documents: include.has("documents") ? after(workspace.documents) : [],
2509
+ summaries: include.has("summaries") ? after(workspace.session_summaries) : []
2510
+ };
2511
+ const payload = {
2512
+ changes,
2513
+ from_anchor: fromAnchor,
2514
+ to_anchor: { type: "timestamp", value: (/* @__PURE__ */ new Date()).toISOString() },
2515
+ counts: {
2516
+ decisions: changes.decisions.length,
2517
+ failures: changes.failures.length,
2518
+ entities: changes.entities.length,
2519
+ documents: changes.documents.length,
2520
+ summaries: changes.summaries.length
2521
+ }
2522
+ };
2523
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
2524
+ } catch (error) {
2525
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
2526
+ }
2527
+ }
2528
+ );
2529
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", "build", "__pycache__", ".turbo", "coverage", ".cache"]);
2530
+ var CODE_EXTENSIONS = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "py", "go", "rs", "java", "cpp", "c", "cs", "rb", "php", "swift", "kt", "sql", "prisma", "graphql", "json", "yaml", "yml", "toml", "env"]);
2531
+ function extractSignature(filePath, content) {
2532
+ const lines = content.split("\n");
2533
+ const signature = [`// File: ${filePath}`];
2534
+ const head = lines.slice(0, 60);
2535
+ for (const line of head) {
2536
+ const trimmed = line.trim();
2537
+ if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
2538
+ if (/^(import|from|require|use |pub use )/.test(trimmed)) {
2539
+ signature.push(trimmed.slice(0, 120));
2540
+ continue;
2541
+ }
2542
+ if (/^(export|async function|function|class|interface|type |const |let |def |pub fn |fn |struct |impl |enum )/.test(trimmed)) {
2543
+ signature.push(trimmed.slice(0, 120));
2544
+ continue;
2545
+ }
2546
+ if (trimmed.startsWith("@") || trimmed.startsWith("#[")) {
2547
+ signature.push(trimmed.slice(0, 80));
2548
+ }
2549
+ }
2550
+ for (const line of lines.slice(60)) {
2551
+ const trimmed = line.trim();
2552
+ if (/^(export (default |async )?function|export (default )?class|export const|export type|export interface|async function|function |class |def |pub fn |fn )/.test(trimmed)) {
2553
+ signature.push(trimmed.slice(0, 120));
2554
+ }
2555
+ }
2556
+ return signature.join("\n").slice(0, 2e3);
2557
+ }
2558
+ server.tool(
2559
+ "semantic_search_codebase",
2560
+ "Semantically search a local codebase without pre-indexing. Unlike grep/ripgrep, this understands meaning \u2014 so 'find authentication logic' finds auth code even if it doesn't literally say 'auth'. Uses vector embeddings via the Whisper API. Perfect for exploring unfamiliar codebases.",
2561
+ {
2562
+ query: z.string().describe("Natural language description of what you're looking for. E.g. 'authentication and session management', 'database connection pooling', 'error handling middleware'"),
2563
+ path: z.string().optional().describe("Absolute path to the codebase root. Defaults to current working directory."),
2564
+ file_types: z.array(z.string()).optional().describe("Limit to specific extensions e.g. ['ts', 'py']. Defaults to all common code files."),
2565
+ top_k: z.number().optional().default(10).describe("Number of most relevant files to return"),
2566
+ threshold: z.number().optional().default(0.2).describe("Minimum similarity score 0-1. Lower = more results but less precise."),
2567
+ max_files: z.number().optional().default(150).describe("Max files to scan. For large codebases, narrow with file_types instead of raising this.")
2568
+ },
2569
+ async ({ query, path: searchPath, file_types, top_k, threshold, max_files }) => {
2570
+ const rootPath = searchPath || process.cwd();
2571
+ const allowedExts = file_types ? new Set(file_types) : CODE_EXTENSIONS;
2572
+ const files = [];
2573
+ function collect(dir) {
2574
+ if (files.length >= (max_files ?? 300)) return;
2575
+ let entries;
2576
+ try {
2577
+ entries = readdirSync(dir, { withFileTypes: true });
2578
+ } catch {
2579
+ return;
2580
+ }
2581
+ for (const entry of entries) {
2582
+ if (files.length >= (max_files ?? 300)) break;
2583
+ if (SKIP_DIRS.has(entry.name)) continue;
2584
+ const full = join(dir, entry.name);
2585
+ if (entry.isDirectory()) {
2586
+ collect(full);
2587
+ } else if (entry.isFile()) {
2588
+ const ext = extname(entry.name).replace(".", "");
2589
+ if (allowedExts.has(ext)) files.push(full);
2590
+ }
2591
+ }
2592
+ }
2593
+ collect(rootPath);
2594
+ if (files.length === 0) {
2595
+ return { content: [{ type: "text", text: `No code files found in ${rootPath}` }] };
2596
+ }
2597
+ const documents = [];
2598
+ for (const filePath of files) {
2599
+ try {
2600
+ const stat = statSync(filePath);
2601
+ if (stat.size > 500 * 1024) continue;
2602
+ const content = readFileSync(filePath, "utf-8");
2603
+ const relPath = relative(rootPath, filePath);
2604
+ const signature = extractSignature(relPath, content);
2605
+ documents.push({ id: relPath, content: signature });
2606
+ } catch {
2607
+ }
2608
+ }
2609
+ if (documents.length === 0) {
2610
+ return { content: [{ type: "text", text: "Could not read any files." }] };
2611
+ }
2612
+ let response;
2613
+ try {
2614
+ response = await whisper.semanticSearch({
2615
+ query,
2616
+ documents,
2617
+ top_k: top_k ?? 10,
2618
+ threshold: threshold ?? 0.2
2619
+ });
2620
+ } catch (error) {
2621
+ return { content: [{ type: "text", text: `Semantic search failed: ${error.message}` }] };
2622
+ }
2623
+ if (!response.results || response.results.length === 0) {
2624
+ return { content: [{ type: "text", text: `No semantically relevant files found for: "${query}"
2625
+
2626
+ Searched ${documents.length} files in ${rootPath}.
2627
+
2628
+ Try lowering the threshold or rephrasing your query.` }] };
2629
+ }
2630
+ const lines = [
2631
+ `Semantic search: "${query}"`,
2632
+ `Searched ${documents.length} files \u2192 ${response.results.length} relevant (${response.latency_ms}ms)
2633
+ `
2634
+ ];
2635
+ for (const result of response.results) {
2636
+ lines.push(`\u{1F4C4} ${result.id} (score: ${result.score})`);
2637
+ if (result.snippet) {
2638
+ lines.push(` ${result.snippet}`);
2639
+ }
2640
+ if (result.score > 0.5) {
2641
+ try {
2642
+ const fullPath = join(rootPath, result.id);
2643
+ const content = readFileSync(fullPath, "utf-8");
2644
+ const excerpt = content.split("\n").slice(0, 30).join("\n");
2645
+ lines.push(`
2646
+ \`\`\`
2647
+ ${excerpt}
2648
+ \`\`\``);
2649
+ } catch {
2650
+ }
2651
+ }
2652
+ lines.push("");
2653
+ }
2654
+ return { content: [{ type: "text", text: lines.join("\n") }] };
2655
+ }
2656
+ );
2657
+ function* walkDir(dir, fileTypes) {
2658
+ let entries;
2659
+ try {
2660
+ entries = readdirSync(dir, { withFileTypes: true });
2661
+ } catch {
2662
+ return;
2663
+ }
2664
+ for (const entry of entries) {
2665
+ if (["node_modules", ".git", "dist", ".next", "build", "__pycache__"].includes(entry.name)) continue;
2666
+ const full = join(dir, entry.name);
2667
+ if (entry.isDirectory()) {
2668
+ yield* walkDir(full, fileTypes);
2669
+ } else if (entry.isFile()) {
2670
+ if (!fileTypes || fileTypes.length === 0) yield full;
2671
+ else if (fileTypes.includes(extname(entry.name).replace(".", ""))) yield full;
2672
+ }
2673
+ }
2674
+ }
2675
+ server.tool(
2676
+ "search_files",
2677
+ "Search files and content in a local directory without requiring pre-indexing. Uses ripgrep when available, falls back to Node.js. Great for finding files, functions, patterns, or any text across a codebase instantly.",
2678
+ {
2679
+ query: z.string().describe("What to search for \u2014 natural language keyword, function name, pattern, etc."),
2680
+ path: z.string().optional().describe("Absolute path to search in. Defaults to current working directory."),
2681
+ mode: z.enum(["content", "filename", "both"]).optional().default("both").describe("Search file contents, filenames, or both"),
2682
+ file_types: z.array(z.string()).optional().describe("Limit to these file extensions e.g. ['ts', 'js', 'py', 'go']"),
2683
+ max_results: z.number().optional().default(20).describe("Max number of matching files to return"),
2684
+ context_lines: z.number().optional().default(2).describe("Lines of context around each match"),
2685
+ case_sensitive: z.boolean().optional().default(false)
2686
+ },
2687
+ async ({ query, path: searchPath, mode, file_types, max_results, context_lines, case_sensitive }) => {
2688
+ const rootPath = searchPath || process.cwd();
2689
+ const results = [];
2690
+ const rgAvailable = spawnSync("rg", ["--version"], { stdio: "ignore" }).status === 0;
2691
+ if (rgAvailable && (mode === "content" || mode === "both")) {
2692
+ try {
2693
+ const args = [
2694
+ "--json",
2695
+ case_sensitive ? "" : "-i",
2696
+ `-C`,
2697
+ `${context_lines}`,
2698
+ `-m`,
2699
+ `${max_results}`,
2700
+ "--max-filesize",
2701
+ "1M",
2702
+ "--glob",
2703
+ "!node_modules",
2704
+ "--glob",
2705
+ "!.git",
2706
+ "--glob",
2707
+ "!dist",
2708
+ "--glob",
2709
+ "!.next",
2710
+ "--glob",
2711
+ "!build",
2712
+ ...file_types && file_types.length > 0 ? ["--glob", file_types.length === 1 ? `*.${file_types[0]}` : `*.{${file_types.join(",")}}`] : [],
2713
+ query,
2714
+ rootPath
2715
+ ].filter(Boolean);
2716
+ const output = execSync(`rg ${args.join(" ")}`, {
2717
+ maxBuffer: 10 * 1024 * 1024,
2718
+ stdio: ["pipe", "pipe", "ignore"]
2719
+ }).toString().trim();
2720
+ const fileMap = /* @__PURE__ */ new Map();
2721
+ for (const line of output.split("\n")) {
2722
+ if (!line.trim()) continue;
2723
+ try {
2724
+ const entry = JSON.parse(line);
2725
+ if (entry.type === "match") {
2726
+ const filePath = relative(rootPath, entry.data.path.text);
2727
+ if (!fileMap.has(filePath)) fileMap.set(filePath, []);
2728
+ fileMap.get(filePath).push({
2729
+ line: entry.data.line_number,
2730
+ content: entry.data.lines.text.trimEnd(),
2731
+ context_before: [],
2732
+ context_after: []
2733
+ });
2734
+ }
2735
+ } catch {
2736
+ }
2737
+ }
2738
+ for (const [file, matches] of fileMap) {
2739
+ if (results.length >= (max_results ?? 20)) break;
2740
+ results.push({ file, matches });
2741
+ }
2742
+ } catch (err) {
2743
+ if (err.status !== 1) {
2744
+ }
2745
+ }
2746
+ }
2747
+ if (mode === "filename" || mode === "both") {
2748
+ const queryLower = query.toLowerCase();
2749
+ const existingFiles = new Set(results.map((r) => r.file));
2750
+ for (const filePath of walkDir(rootPath, file_types)) {
2751
+ if (results.length >= (max_results ?? 20)) break;
2752
+ const relPath = relative(rootPath, filePath);
2753
+ const check = case_sensitive ? relPath : relPath.toLowerCase();
2754
+ if (check.includes(case_sensitive ? query : queryLower) && !existingFiles.has(relPath)) {
2755
+ results.push({ file: relPath, matches: [] });
2756
+ existingFiles.add(relPath);
2757
+ }
2758
+ }
2759
+ }
2760
+ if (!rgAvailable && (mode === "content" || mode === "both")) {
2761
+ const regex = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), case_sensitive ? "g" : "gi");
2762
+ for (const filePath of walkDir(rootPath, file_types)) {
2763
+ if (results.length >= (max_results ?? 20)) break;
2764
+ try {
2765
+ const stat = statSync(filePath);
2766
+ if (stat.size > 512 * 1024) continue;
2767
+ const text = readFileSync(filePath, "utf-8");
2768
+ const lines2 = text.split("\n");
2769
+ const matches = [];
2770
+ lines2.forEach((line, i) => {
2771
+ regex.lastIndex = 0;
2772
+ if (regex.test(line)) {
2773
+ matches.push({
2774
+ line: i + 1,
2775
+ content: line.trimEnd(),
2776
+ context_before: lines2.slice(Math.max(0, i - (context_lines ?? 2)), i).map((l) => l.trimEnd()),
2777
+ context_after: lines2.slice(i + 1, i + 1 + (context_lines ?? 2)).map((l) => l.trimEnd())
2778
+ });
2779
+ regex.lastIndex = 0;
2780
+ }
2781
+ });
2782
+ if (matches.length > 0) results.push({ file: relative(rootPath, filePath), matches });
2783
+ } catch {
2784
+ }
2785
+ }
2786
+ }
2787
+ if (results.length === 0) {
2788
+ return { content: [{ type: "text", text: `No results found for "${query}" in ${rootPath}` }] };
2789
+ }
2790
+ const totalMatches = results.reduce((s, r) => s + r.matches.length, 0);
2791
+ const lines = [
2792
+ `Found ${results.length} file(s), ${totalMatches} match(es) for "${query}" in ${rootPath}
2793
+ `
2794
+ ];
2795
+ for (const result of results) {
2796
+ lines.push(`\u{1F4C4} ${result.file}`);
2797
+ for (const match of result.matches.slice(0, 5)) {
2798
+ if (match.context_before.length > 0) {
2799
+ lines.push(...match.context_before.map((l) => ` ${l}`));
2800
+ }
2801
+ lines.push(`\u2192 L${match.line}: ${match.content}`);
2802
+ if (match.context_after.length > 0) {
2803
+ lines.push(...match.context_after.map((l) => ` ${l}`));
2804
+ }
2805
+ }
2806
+ if (result.matches.length > 5) {
2807
+ lines.push(` ... and ${result.matches.length - 5} more matches`);
2808
+ }
2809
+ lines.push("");
2810
+ }
2811
+ return { content: [{ type: "text", text: lines.join("\n") }] };
2812
+ }
2813
+ );
2814
+ server.tool(
2815
+ "semantic_search",
2816
+ "Semantic vector search over provided documents. Uses embeddings to find semantically similar content. Perfect for AI code search, finding similar functions, or searching by meaning rather than keywords.",
2817
+ {
2818
+ query: z.string().describe("What to search for semantically (e.g. 'authentication logic', 'database connection')"),
2819
+ documents: z.array(z.object({
2820
+ id: z.string().describe("Unique identifier (file path, URL, or any ID)"),
2821
+ content: z.string().describe("The text content to search in")
2822
+ })).describe("Documents to search over"),
2823
+ top_k: z.number().optional().default(5).describe("Number of results to return"),
2824
+ threshold: z.number().optional().default(0.3).describe("Minimum similarity score 0-1")
2825
+ },
2826
+ async ({ query, documents, top_k, threshold }) => {
2827
+ try {
2828
+ const API_KEY2 = process.env.WHISPER_API_KEY;
2829
+ const BASE_URL2 = process.env.WHISPER_API_BASE_URL || "https://context.usewhisper.dev";
2830
+ const res = await fetch(`${BASE_URL2}/v1/search/semantic`, {
2831
+ method: "POST",
2832
+ headers: {
2833
+ "Authorization": `Bearer ${API_KEY2}`,
2834
+ "Content-Type": "application/json"
2835
+ },
2836
+ body: JSON.stringify({ query, documents, top_k, threshold })
2837
+ });
2838
+ const data = await res.json();
2839
+ if (data.error) {
2840
+ return { content: [{ type: "text", text: `Error: ${data.error}` }] };
2841
+ }
2842
+ if (!data.results || data.results.length === 0) {
2843
+ return { content: [{ type: "text", text: "No semantically similar results found." }] };
2844
+ }
2845
+ const lines = [`Found ${data.results.length} semantically similar results:
2846
+ `];
2847
+ for (const r of data.results) {
2848
+ lines.push(`\u{1F4C4} ${r.id} (score: ${r.score.toFixed(3)})`);
2849
+ lines.push(` ${r.content.slice(0, 200)}${r.content.length > 200 ? "..." : ""}`);
2850
+ lines.push("");
2851
+ }
2852
+ return { content: [{ type: "text", text: lines.join("\n") }] };
2853
+ } catch (error) {
2854
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
2855
+ }
2856
+ }
2857
+ );
1054
2858
  async function main() {
1055
2859
  const transport = new StdioServerTransport();
1056
2860
  await server.connect(transport);