@usewhisper/mcp-server 0.4.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 +1595 -96
  56. package/dist/server.js.map +1 -1
  57. package/package.json +51 -51
package/dist/server.js CHANGED
@@ -5,8 +5,374 @@ 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
7
  import { execSync, spawnSync } from "child_process";
8
- import { readdirSync, readFileSync, statSync } from "fs";
8
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, appendFileSync } from "fs";
9
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
+ };
10
376
 
11
377
  // ../src/sdk/index.ts
12
378
  var WhisperError = class extends Error {
@@ -28,23 +394,42 @@ var DEFAULT_BASE_DELAY_MS = 250;
28
394
  var DEFAULT_MAX_DELAY_MS = 2e3;
29
395
  var DEFAULT_TIMEOUT_MS = 15e3;
30
396
  var PROJECT_CACHE_TTL_MS = 3e4;
31
- function sleep(ms) {
32
- return new Promise((resolve) => setTimeout(resolve, ms));
33
- }
34
- function getBackoffDelay(attempt, base, max) {
35
- const jitter = 0.8 + Math.random() * 0.4;
36
- 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
+ }
37
404
  }
38
405
  function isLikelyProjectId(projectRef) {
39
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);
40
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
+ }
41
426
  var WhisperContext = class _WhisperContext {
42
427
  apiKey;
43
428
  baseUrl;
44
429
  defaultProject;
45
- orgId;
46
430
  timeoutMs;
47
431
  retryConfig;
432
+ runtimeClient;
48
433
  projectRefToId = /* @__PURE__ */ new Map();
49
434
  projectCache = [];
50
435
  projectCacheExpiresAt = 0;
@@ -56,22 +441,49 @@ var WhisperContext = class _WhisperContext {
56
441
  });
57
442
  }
58
443
  this.apiKey = config.apiKey;
59
- this.baseUrl = config.baseUrl || "https://context.usewhisper.dev";
444
+ this.baseUrl = normalizeBaseUrl2(config.baseUrl || "https://context.usewhisper.dev");
60
445
  this.defaultProject = config.project;
61
- this.orgId = config.orgId;
62
446
  this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
63
447
  this.retryConfig = {
64
448
  maxAttempts: config.retry?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
65
449
  baseDelayMs: config.retry?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS,
66
450
  maxDelayMs: config.retry?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS
67
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
+ );
68
481
  }
69
482
  withProject(project) {
70
483
  return new _WhisperContext({
71
484
  apiKey: this.apiKey,
72
485
  baseUrl: this.baseUrl,
73
486
  project,
74
- orgId: this.orgId,
75
487
  timeoutMs: this.timeoutMs,
76
488
  retry: this.retryConfig
77
489
  });
@@ -147,9 +559,17 @@ var WhisperContext = class _WhisperContext {
147
559
  return Array.from(candidates).filter(Boolean);
148
560
  }
149
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
+ }
150
569
  const refs = await this.getProjectRefCandidates(projectRef);
151
570
  let lastError;
152
571
  for (const ref of refs) {
572
+ if (ref === projectRef) continue;
153
573
  try {
154
574
  return await execute(ref);
155
575
  } catch (error) {
@@ -172,7 +592,7 @@ var WhisperContext = class _WhisperContext {
172
592
  if (status === 401 || /api key|unauthorized|forbidden/i.test(message)) {
173
593
  return { code: "INVALID_API_KEY", retryable: false };
174
594
  }
175
- if (status === 404 || /project not found/i.test(message)) {
595
+ if (status === 404 && isProjectNotFoundMessage(message)) {
176
596
  return { code: "PROJECT_NOT_FOUND", retryable: false };
177
597
  }
178
598
  if (status === 408) {
@@ -186,64 +606,68 @@ var WhisperContext = class _WhisperContext {
186
606
  }
187
607
  return { code: "REQUEST_FAILED", retryable: false };
188
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
+ }
189
629
  async request(endpoint, options = {}) {
190
- const maxAttempts = Math.max(1, this.retryConfig.maxAttempts);
191
- let lastError;
192
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
193
- const controller = new AbortController();
194
- 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") {
195
635
  try {
196
- const response = await fetch(`${this.baseUrl}${endpoint}`, {
197
- ...options,
198
- signal: controller.signal,
199
- headers: {
200
- Authorization: `Bearer ${this.apiKey}`,
201
- "Content-Type": "application/json",
202
- ...this.orgId ? { "X-Whisper-Org-Id": this.orgId } : {},
203
- ...options.headers
204
- }
205
- });
206
- clearTimeout(timeout);
207
- if (!response.ok) {
208
- let payload = null;
209
- try {
210
- payload = await response.json();
211
- } catch {
212
- payload = await response.text().catch(() => "");
213
- }
214
- const message = typeof payload === "string" ? payload : payload?.error || payload?.message || `HTTP ${response.status}: ${response.statusText}`;
215
- const { code, retryable } = this.classifyError(response.status, message);
216
- const err = new WhisperError({
217
- code,
218
- message,
219
- status: response.status,
220
- retryable,
221
- details: payload
222
- });
223
- if (!retryable || attempt === maxAttempts - 1) {
224
- throw err;
225
- }
226
- await sleep(getBackoffDelay(attempt, this.retryConfig.baseDelayMs, this.retryConfig.maxDelayMs));
227
- continue;
228
- }
229
- return response.json();
230
- } catch (error) {
231
- clearTimeout(timeout);
232
- const isAbort = error?.name === "AbortError";
233
- const mapped = error instanceof WhisperError ? error : new WhisperError({
234
- code: isAbort ? "TIMEOUT" : "NETWORK_ERROR",
235
- message: isAbort ? "Request timed out" : error?.message || "Network request failed",
236
- retryable: true,
237
- details: error
238
- });
239
- lastError = mapped;
240
- if (!mapped.retryable || attempt === maxAttempts - 1) {
241
- throw mapped;
242
- }
243
- await sleep(getBackoffDelay(attempt, this.retryConfig.baseDelayMs, this.retryConfig.maxDelayMs));
636
+ body = JSON.parse(options.body);
637
+ } catch {
638
+ body = void 0;
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;
244
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
+ });
245
670
  }
246
- throw lastError instanceof Error ? lastError : new WhisperError({ code: "REQUEST_FAILED", message: "Request failed" });
247
671
  }
248
672
  async query(params) {
249
673
  const projectRef = this.getRequiredProject(params.project);
@@ -354,13 +778,18 @@ var WhisperContext = class _WhisperContext {
354
778
  session_id: params.session_id,
355
779
  agent_id: params.agent_id,
356
780
  importance: params.importance,
357
- metadata: params.metadata
781
+ metadata: params.metadata,
782
+ async: params.async,
783
+ write_mode: params.write_mode
358
784
  })
359
785
  });
360
- const id2 = direct?.memory?.id || direct?.id || direct?.memory_id;
786
+ const id2 = direct?.memory?.id || direct?.id || direct?.memory_id || direct?.job_id;
361
787
  if (id2) {
362
788
  return { id: id2, success: true, path: "sota", fallback_used: false };
363
789
  }
790
+ if (direct?.success === true) {
791
+ return { id: "", success: true, path: "sota", fallback_used: false };
792
+ }
364
793
  } catch (error) {
365
794
  if (params.allow_legacy_fallback === false) {
366
795
  throw error;
@@ -390,20 +819,109 @@ var WhisperContext = class _WhisperContext {
390
819
  return { id, success: true, path: "legacy", fallback_used: true };
391
820
  });
392
821
  }
393
- async searchMemories(params) {
822
+ async addMemoriesBulk(params) {
394
823
  const projectRef = this.getRequiredProject(params.project);
395
- return this.withProjectRefFallback(projectRef, (project) => this.request("/v1/memory/search", {
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) {
867
+ const projectRef = this.getRequiredProject(params.project);
868
+ return this.withProjectRefFallback(projectRef, (project) => this.request("/v1/memory/extract/session", {
396
869
  method: "POST",
397
870
  body: JSON.stringify({
398
- query: params.query,
871
+ ...params,
399
872
  project,
400
- user_id: params.user_id,
401
- session_id: params.session_id,
402
- memory_types: params.memory_type ? [params.memory_type] : void 0,
403
- top_k: params.top_k || 10
873
+ messages: params.messages.map((m) => ({
874
+ ...m,
875
+ timestamp: m.timestamp || (/* @__PURE__ */ new Date()).toISOString()
876
+ }))
404
877
  })
405
878
  }));
406
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
+ }
407
925
  async createApiKey(params) {
408
926
  return this.request("/v1/keys", {
409
927
  method: "POST",
@@ -418,10 +936,29 @@ var WhisperContext = class _WhisperContext {
418
936
  }
419
937
  async searchMemoriesSOTA(params) {
420
938
  const projectRef = this.getRequiredProject(params.project);
421
- return this.withProjectRefFallback(projectRef, (project) => this.request("/v1/memory/search", {
422
- method: "POST",
423
- body: JSON.stringify({ ...params, project })
424
- }));
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
+ });
425
962
  }
426
963
  async ingestSession(params) {
427
964
  const projectRef = this.getRequiredProject(params.project);
@@ -431,37 +968,116 @@ var WhisperContext = class _WhisperContext {
431
968
  }));
432
969
  }
433
970
  async getSessionMemories(params) {
434
- const project = await this.resolveProjectId(this.getRequiredProject(params.project));
435
- const query = new URLSearchParams({
436
- project,
437
- ...params.limit && { limit: params.limit.toString() },
438
- ...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
+ }
439
987
  });
440
- return this.request(`/v1/memory/session/${params.session_id}?${query}`);
441
988
  }
442
989
  async getUserProfile(params) {
443
- const project = await this.resolveProjectId(this.getRequiredProject(params.project));
444
- const query = new URLSearchParams({
445
- project,
446
- ...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
+ }
447
1016
  });
448
- return this.request(`/v1/memory/profile/${params.user_id}?${query}`);
449
1017
  }
450
1018
  async getMemoryVersions(memoryId) {
451
1019
  return this.request(`/v1/memory/${memoryId}/versions`);
452
1020
  }
453
1021
  async updateMemory(memoryId, params) {
454
- return this.request(`/v1/memory/${memoryId}`, {
455
- method: "PUT",
456
- body: JSON.stringify(params)
457
- });
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
+ }
458
1043
  }
459
1044
  async deleteMemory(memoryId) {
460
- 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
+ }
461
1057
  }
462
1058
  async getMemoryRelations(memoryId) {
463
1059
  return this.request(`/v1/memory/${memoryId}/relations`);
464
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
+ }
465
1081
  async oracleSearch(params) {
466
1082
  const project = await this.resolveProjectId(this.getRequiredProject(params.project));
467
1083
  return this.request("/v1/oracle/search", {
@@ -586,6 +1202,9 @@ var WhisperContext = class _WhisperContext {
586
1202
  };
587
1203
  memory = {
588
1204
  add: (params) => this.addMemory(params),
1205
+ addBulk: (params) => this.addMemoriesBulk(params),
1206
+ extract: (params) => this.extractMemories(params),
1207
+ extractSession: (params) => this.extractSessionMemories(params),
589
1208
  search: (params) => this.searchMemories(params),
590
1209
  searchSOTA: (params) => this.searchMemoriesSOTA(params),
591
1210
  ingestSession: (params) => this.ingestSession(params),
@@ -595,6 +1214,8 @@ var WhisperContext = class _WhisperContext {
595
1214
  update: (memoryId, params) => this.updateMemory(memoryId, params),
596
1215
  delete: (memoryId) => this.deleteMemory(memoryId),
597
1216
  getRelations: (memoryId) => this.getMemoryRelations(memoryId),
1217
+ getGraph: (params) => this.getMemoryGraph(params),
1218
+ getConversationGraph: (params) => this.getConversationGraph(params),
598
1219
  consolidate: (params) => this.consolidateMemories(params),
599
1220
  updateDecay: (params) => this.updateImportanceDecay(params),
600
1221
  getImportanceStats: (project) => this.getImportanceStats(project)
@@ -639,6 +1260,512 @@ var server = new McpServer({
639
1260
  name: "whisper-context",
640
1261
  version: "0.2.8"
641
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
+ }
1767
+ }
1768
+ );
642
1769
  server.tool(
643
1770
  "query_context",
644
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.",
@@ -1071,6 +2198,334 @@ server.tool(
1071
2198
  }
1072
2199
  }
1073
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
+ );
1074
2529
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", "build", "__pycache__", ".turbo", "coverage", ".cache"]);
1075
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"]);
1076
2531
  function extractSignature(filePath, content) {
@@ -1109,7 +2564,7 @@ server.tool(
1109
2564
  file_types: z.array(z.string()).optional().describe("Limit to specific extensions e.g. ['ts', 'py']. Defaults to all common code files."),
1110
2565
  top_k: z.number().optional().default(10).describe("Number of most relevant files to return"),
1111
2566
  threshold: z.number().optional().default(0.2).describe("Minimum similarity score 0-1. Lower = more results but less precise."),
1112
- max_files: z.number().optional().default(300).describe("Max files to scan. For large codebases, increase this or narrow with file_types.")
2567
+ max_files: z.number().optional().default(150).describe("Max files to scan. For large codebases, narrow with file_types instead of raising this.")
1113
2568
  },
1114
2569
  async ({ query, path: searchPath, file_types, top_k, threshold, max_files }) => {
1115
2570
  const rootPath = searchPath || process.cwd();
@@ -1356,6 +2811,50 @@ server.tool(
1356
2811
  return { content: [{ type: "text", text: lines.join("\n") }] };
1357
2812
  }
1358
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
+ );
1359
2858
  async function main() {
1360
2859
  const transport = new StdioServerTransport();
1361
2860
  await server.connect(transport);