@usewhisper/mcp-server 0.4.0 → 1.0.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 +79 -146
  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 +2264 -197
  56. package/dist/server.js.map +1 -1
  57. package/package.json +51 -51
package/dist/server.js CHANGED
@@ -5,8 +5,386 @@ 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_API_KEY_ONLY_PREFIXES = ["/v1/memory", "/v1/context/query"];
98
+ var DEFAULT_RETRY_ATTEMPTS = {
99
+ search: 3,
100
+ writeAck: 2,
101
+ bulk: 2,
102
+ profile: 2,
103
+ session: 2,
104
+ query: 3,
105
+ get: 2
106
+ };
107
+ function isObject(value) {
108
+ return typeof value === "object" && value !== null;
109
+ }
110
+ function toMessage(payload, status, statusText) {
111
+ if (typeof payload === "string" && payload.trim()) return payload;
112
+ if (isObject(payload)) {
113
+ const maybeError = payload.error;
114
+ const maybeMessage = payload.message;
115
+ if (typeof maybeError === "string" && maybeError.trim()) return maybeError;
116
+ if (typeof maybeMessage === "string" && maybeMessage.trim()) return maybeMessage;
117
+ if (isObject(maybeError) && typeof maybeError.message === "string") return maybeError.message;
118
+ }
119
+ return `HTTP ${status}: ${statusText}`;
120
+ }
121
+ var RuntimeClientError = class extends Error {
122
+ status;
123
+ retryable;
124
+ code;
125
+ details;
126
+ traceId;
127
+ constructor(args) {
128
+ super(args.message);
129
+ this.name = "RuntimeClientError";
130
+ this.status = args.status;
131
+ this.retryable = args.retryable;
132
+ this.code = args.code;
133
+ this.details = args.details;
134
+ this.traceId = args.traceId;
135
+ }
136
+ };
137
+ var RuntimeClient = class {
138
+ apiKey;
139
+ baseUrl;
140
+ sdkVersion;
141
+ compatMode;
142
+ retryPolicy;
143
+ timeouts;
144
+ diagnostics;
145
+ inFlight = /* @__PURE__ */ new Map();
146
+ sendApiKeyHeader;
147
+ constructor(options, diagnostics) {
148
+ if (!options.apiKey) {
149
+ throw new RuntimeClientError({
150
+ code: "INVALID_API_KEY",
151
+ message: "API key is required",
152
+ retryable: false
153
+ });
154
+ }
155
+ this.apiKey = options.apiKey;
156
+ this.baseUrl = normalizeBaseUrl(options.baseUrl || "https://context.usewhisper.dev");
157
+ this.sdkVersion = options.sdkVersion || "2.x-runtime";
158
+ this.compatMode = options.compatMode || "fallback";
159
+ this.retryPolicy = {
160
+ retryableStatusCodes: options.retryPolicy?.retryableStatusCodes || DEFAULT_RETRYABLE_STATUS,
161
+ retryOnNetworkError: options.retryPolicy?.retryOnNetworkError ?? true,
162
+ maxBackoffMs: options.retryPolicy?.maxBackoffMs ?? 1200,
163
+ baseBackoffMs: options.retryPolicy?.baseBackoffMs ?? 250,
164
+ maxAttemptsByOperation: options.retryPolicy?.maxAttemptsByOperation || {}
165
+ };
166
+ this.timeouts = {
167
+ ...DEFAULT_TIMEOUTS,
168
+ ...options.timeouts || {}
169
+ };
170
+ this.sendApiKeyHeader = process.env.WHISPER_SEND_X_API_KEY === "1";
171
+ this.diagnostics = diagnostics || new DiagnosticsStore(1e3);
172
+ }
173
+ getDiagnosticsStore() {
174
+ return this.diagnostics;
175
+ }
176
+ getCompatMode() {
177
+ return this.compatMode;
178
+ }
179
+ timeoutFor(operation) {
180
+ switch (operation) {
181
+ case "search":
182
+ return this.timeouts.searchMs;
183
+ case "writeAck":
184
+ return this.timeouts.writeAckMs;
185
+ case "bulk":
186
+ return this.timeouts.bulkMs;
187
+ case "profile":
188
+ return this.timeouts.profileMs;
189
+ case "session":
190
+ return this.timeouts.sessionMs;
191
+ case "query":
192
+ case "get":
193
+ default:
194
+ return this.timeouts.searchMs;
195
+ }
196
+ }
197
+ maxAttemptsFor(operation) {
198
+ const override = this.retryPolicy.maxAttemptsByOperation?.[operation];
199
+ return Math.max(1, override ?? DEFAULT_RETRY_ATTEMPTS[operation]);
200
+ }
201
+ shouldRetryStatus(status) {
202
+ return status !== void 0 && this.retryPolicy.retryableStatusCodes?.includes(status) === true;
203
+ }
204
+ backoff(attempt) {
205
+ const base = this.retryPolicy.baseBackoffMs ?? 250;
206
+ const max = this.retryPolicy.maxBackoffMs ?? 1200;
207
+ const jitter = 0.8 + Math.random() * 0.4;
208
+ return Math.min(max, Math.floor(base * Math.pow(2, attempt) * jitter));
209
+ }
210
+ runtimeName() {
211
+ const maybeWindow = globalThis.window;
212
+ return maybeWindow && typeof maybeWindow === "object" ? "browser" : "node";
213
+ }
214
+ apiKeyOnlyPrefixes() {
215
+ const raw = process.env.WHISPER_API_KEY_ONLY_PREFIXES;
216
+ if (!raw || !raw.trim()) return DEFAULT_API_KEY_ONLY_PREFIXES;
217
+ return raw.split(",").map((p) => p.trim()).filter((p) => p.length > 0);
218
+ }
219
+ shouldAttachApiKeyHeader(endpoint) {
220
+ if (this.sendApiKeyHeader) return true;
221
+ const prefixes = this.apiKeyOnlyPrefixes();
222
+ return prefixes.some((prefix) => endpoint === prefix || endpoint.startsWith(`${prefix}/`));
223
+ }
224
+ createRequestFingerprint(options) {
225
+ const normalizedEndpoint = normalizeEndpoint(options.endpoint);
226
+ const authFingerprint = stableHash(this.apiKey.replace(/^Bearer\s+/i, ""));
227
+ const payload = JSON.stringify({
228
+ method: options.method || "GET",
229
+ endpoint: normalizedEndpoint,
230
+ body: options.body || null,
231
+ extra: options.dedupeKeyExtra || "",
232
+ authFingerprint
233
+ });
234
+ return stableHash(payload);
235
+ }
236
+ async request(options) {
237
+ const dedupeKey = options.idempotent ? this.createRequestFingerprint(options) : null;
238
+ if (dedupeKey) {
239
+ const inFlight = this.inFlight.get(dedupeKey);
240
+ if (inFlight) {
241
+ const data = await inFlight;
242
+ this.diagnostics.add({
243
+ id: randomId("diag"),
244
+ startedAt: nowIso(),
245
+ endedAt: nowIso(),
246
+ traceId: data.traceId,
247
+ spanId: randomId("span"),
248
+ operation: options.operation,
249
+ method: options.method || "GET",
250
+ endpoint: normalizeEndpoint(options.endpoint),
251
+ status: data.status,
252
+ durationMs: 0,
253
+ success: true,
254
+ deduped: true
255
+ });
256
+ const cloned = {
257
+ data: data.data,
258
+ status: data.status,
259
+ traceId: data.traceId
260
+ };
261
+ return cloned;
262
+ }
263
+ }
264
+ const runner = this.performRequest(options).then((data) => {
265
+ if (dedupeKey) this.inFlight.delete(dedupeKey);
266
+ return data;
267
+ }).catch((error) => {
268
+ if (dedupeKey) this.inFlight.delete(dedupeKey);
269
+ throw error;
270
+ });
271
+ if (dedupeKey) {
272
+ this.inFlight.set(dedupeKey, runner);
273
+ }
274
+ return runner;
275
+ }
276
+ async performRequest(options) {
277
+ const method = options.method || "GET";
278
+ const normalizedEndpoint = normalizeEndpoint(options.endpoint);
279
+ const operation = options.operation;
280
+ const maxAttempts = this.maxAttemptsFor(operation);
281
+ const timeoutMs = this.timeoutFor(operation);
282
+ const traceId = options.traceId || randomId("trace");
283
+ let lastError = null;
284
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
285
+ const spanId = randomId("span");
286
+ const startedAt = Date.now();
287
+ const controller = new AbortController();
288
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
289
+ try {
290
+ const attachApiKeyHeader = this.shouldAttachApiKeyHeader(normalizedEndpoint);
291
+ const response = await fetch(`${this.baseUrl}${normalizedEndpoint}`, {
292
+ method,
293
+ signal: controller.signal,
294
+ keepalive: method !== "GET",
295
+ headers: {
296
+ "Content-Type": "application/json",
297
+ Authorization: this.apiKey.startsWith("Bearer ") ? this.apiKey : `Bearer ${this.apiKey}`,
298
+ ...attachApiKeyHeader ? { "X-API-Key": this.apiKey.replace(/^Bearer\s+/i, "") } : {},
299
+ "x-trace-id": traceId,
300
+ "x-span-id": spanId,
301
+ "x-sdk-version": this.sdkVersion,
302
+ "x-sdk-runtime": this.runtimeName(),
303
+ ...options.headers || {}
304
+ },
305
+ body: method === "GET" || method === "DELETE" ? void 0 : JSON.stringify(options.body || {})
306
+ });
307
+ clearTimeout(timeout);
308
+ let payload = null;
309
+ try {
310
+ payload = await response.json();
311
+ } catch {
312
+ payload = await response.text().catch(() => "");
313
+ }
314
+ const durationMs = Date.now() - startedAt;
315
+ const record = {
316
+ id: randomId("diag"),
317
+ startedAt: new Date(startedAt).toISOString(),
318
+ endedAt: nowIso(),
319
+ traceId,
320
+ spanId,
321
+ operation,
322
+ method,
323
+ endpoint: normalizedEndpoint,
324
+ status: response.status,
325
+ durationMs,
326
+ success: response.ok
327
+ };
328
+ this.diagnostics.add(record);
329
+ if (response.ok) {
330
+ return {
331
+ data: payload,
332
+ status: response.status,
333
+ traceId
334
+ };
335
+ }
336
+ const message = toMessage(payload, response.status, response.statusText);
337
+ const retryable = this.shouldRetryStatus(response.status);
338
+ const error = new RuntimeClientError({
339
+ message,
340
+ status: response.status,
341
+ retryable,
342
+ code: response.status === 404 ? "NOT_FOUND" : "REQUEST_FAILED",
343
+ details: payload,
344
+ traceId
345
+ });
346
+ lastError = error;
347
+ if (!retryable || attempt === maxAttempts - 1) {
348
+ throw error;
349
+ }
350
+ } catch (error) {
351
+ clearTimeout(timeout);
352
+ const durationMs = Date.now() - startedAt;
353
+ const isAbort = isObject(error) && error.name === "AbortError";
354
+ const mapped = error instanceof RuntimeClientError ? error : new RuntimeClientError({
355
+ message: isAbort ? "Request timed out" : error instanceof Error ? error.message : "Network error",
356
+ retryable: this.retryPolicy.retryOnNetworkError ?? true,
357
+ code: isAbort ? "TIMEOUT" : "NETWORK_ERROR",
358
+ traceId
359
+ });
360
+ lastError = mapped;
361
+ this.diagnostics.add({
362
+ id: randomId("diag"),
363
+ startedAt: new Date(startedAt).toISOString(),
364
+ endedAt: nowIso(),
365
+ traceId,
366
+ spanId,
367
+ operation,
368
+ method,
369
+ endpoint: normalizedEndpoint,
370
+ durationMs,
371
+ success: false,
372
+ errorCode: mapped.code,
373
+ errorMessage: mapped.message
374
+ });
375
+ if (!mapped.retryable || attempt === maxAttempts - 1) {
376
+ throw mapped;
377
+ }
378
+ }
379
+ await new Promise((resolve) => setTimeout(resolve, this.backoff(attempt)));
380
+ }
381
+ throw lastError || new RuntimeClientError({
382
+ message: "Request failed",
383
+ retryable: false,
384
+ code: "REQUEST_FAILED"
385
+ });
386
+ }
387
+ };
10
388
 
11
389
  // ../src/sdk/index.ts
12
390
  var WhisperError = class extends Error {
@@ -28,23 +406,42 @@ var DEFAULT_BASE_DELAY_MS = 250;
28
406
  var DEFAULT_MAX_DELAY_MS = 2e3;
29
407
  var DEFAULT_TIMEOUT_MS = 15e3;
30
408
  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));
409
+ var DEPRECATION_WARNINGS = /* @__PURE__ */ new Set();
410
+ function warnDeprecatedOnce(key, message) {
411
+ if (DEPRECATION_WARNINGS.has(key)) return;
412
+ DEPRECATION_WARNINGS.add(key);
413
+ if (typeof console !== "undefined" && typeof console.warn === "function") {
414
+ console.warn(message);
415
+ }
37
416
  }
38
417
  function isLikelyProjectId(projectRef) {
39
418
  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
419
  }
420
+ function normalizeBaseUrl2(url) {
421
+ let normalized = url.trim().replace(/\/+$/, "");
422
+ normalized = normalized.replace(/\/api\/v1$/i, "");
423
+ normalized = normalized.replace(/\/v1$/i, "");
424
+ normalized = normalized.replace(/\/api$/i, "");
425
+ return normalized;
426
+ }
427
+ function normalizeEndpoint2(endpoint) {
428
+ const withLeadingSlash = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
429
+ if (/^\/api\/v1(\/|$)/i.test(withLeadingSlash)) {
430
+ return withLeadingSlash.replace(/^\/api/i, "");
431
+ }
432
+ return withLeadingSlash;
433
+ }
434
+ function isProjectNotFoundMessage(message) {
435
+ const normalized = message.toLowerCase();
436
+ return normalized.includes("project not found") || normalized.includes("no project found") || normalized.includes("project does not exist");
437
+ }
41
438
  var WhisperContext = class _WhisperContext {
42
439
  apiKey;
43
440
  baseUrl;
44
441
  defaultProject;
45
- orgId;
46
442
  timeoutMs;
47
443
  retryConfig;
444
+ runtimeClient;
48
445
  projectRefToId = /* @__PURE__ */ new Map();
49
446
  projectCache = [];
50
447
  projectCacheExpiresAt = 0;
@@ -56,22 +453,49 @@ var WhisperContext = class _WhisperContext {
56
453
  });
57
454
  }
58
455
  this.apiKey = config.apiKey;
59
- this.baseUrl = config.baseUrl || "https://context.usewhisper.dev";
456
+ this.baseUrl = normalizeBaseUrl2(config.baseUrl || "https://context.usewhisper.dev");
60
457
  this.defaultProject = config.project;
61
- this.orgId = config.orgId;
62
458
  this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
63
459
  this.retryConfig = {
64
460
  maxAttempts: config.retry?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
65
461
  baseDelayMs: config.retry?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS,
66
462
  maxDelayMs: config.retry?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS
67
463
  };
464
+ this.runtimeClient = new RuntimeClient({
465
+ apiKey: this.apiKey,
466
+ baseUrl: this.baseUrl,
467
+ compatMode: "fallback",
468
+ timeouts: {
469
+ searchMs: this.timeoutMs,
470
+ writeAckMs: this.timeoutMs,
471
+ bulkMs: Math.max(this.timeoutMs, 1e4),
472
+ profileMs: this.timeoutMs,
473
+ sessionMs: this.timeoutMs
474
+ },
475
+ retryPolicy: {
476
+ baseBackoffMs: this.retryConfig.baseDelayMs,
477
+ maxBackoffMs: this.retryConfig.maxDelayMs,
478
+ maxAttemptsByOperation: {
479
+ search: this.retryConfig.maxAttempts,
480
+ writeAck: this.retryConfig.maxAttempts,
481
+ bulk: this.retryConfig.maxAttempts,
482
+ profile: this.retryConfig.maxAttempts,
483
+ session: this.retryConfig.maxAttempts,
484
+ query: this.retryConfig.maxAttempts,
485
+ get: this.retryConfig.maxAttempts
486
+ }
487
+ }
488
+ });
489
+ warnDeprecatedOnce(
490
+ "whisper_context_class",
491
+ "[Whisper SDK] WhisperContext remains supported in v2 but is legacy. Prefer WhisperClient for runtime features (queue/cache/session/diagnostics)."
492
+ );
68
493
  }
69
494
  withProject(project) {
70
495
  return new _WhisperContext({
71
496
  apiKey: this.apiKey,
72
497
  baseUrl: this.baseUrl,
73
498
  project,
74
- orgId: this.orgId,
75
499
  timeoutMs: this.timeoutMs,
76
500
  retry: this.retryConfig
77
501
  });
@@ -147,9 +571,17 @@ var WhisperContext = class _WhisperContext {
147
571
  return Array.from(candidates).filter(Boolean);
148
572
  }
149
573
  async withProjectRefFallback(projectRef, execute) {
574
+ try {
575
+ return await execute(projectRef);
576
+ } catch (error) {
577
+ if (!(error instanceof WhisperError) || error.code !== "PROJECT_NOT_FOUND") {
578
+ throw error;
579
+ }
580
+ }
150
581
  const refs = await this.getProjectRefCandidates(projectRef);
151
582
  let lastError;
152
583
  for (const ref of refs) {
584
+ if (ref === projectRef) continue;
153
585
  try {
154
586
  return await execute(ref);
155
587
  } catch (error) {
@@ -172,7 +604,7 @@ var WhisperContext = class _WhisperContext {
172
604
  if (status === 401 || /api key|unauthorized|forbidden/i.test(message)) {
173
605
  return { code: "INVALID_API_KEY", retryable: false };
174
606
  }
175
- if (status === 404 || /project not found/i.test(message)) {
607
+ if (status === 404 && isProjectNotFoundMessage(message)) {
176
608
  return { code: "PROJECT_NOT_FOUND", retryable: false };
177
609
  }
178
610
  if (status === 408) {
@@ -186,64 +618,68 @@ var WhisperContext = class _WhisperContext {
186
618
  }
187
619
  return { code: "REQUEST_FAILED", retryable: false };
188
620
  }
621
+ isEndpointNotFoundError(error) {
622
+ if (!(error instanceof WhisperError)) {
623
+ return false;
624
+ }
625
+ if (error.status !== 404) {
626
+ return false;
627
+ }
628
+ const message = (error.message || "").toLowerCase();
629
+ return !isProjectNotFoundMessage(message);
630
+ }
631
+ inferOperation(endpoint, method) {
632
+ const normalized = normalizeEndpoint2(endpoint).toLowerCase();
633
+ if (normalized.includes("/memory/search")) return "search";
634
+ if (normalized.includes("/memory/bulk")) return "bulk";
635
+ if (normalized.includes("/memory/profile") || normalized.includes("/memory/session")) return "profile";
636
+ if (normalized.includes("/memory/ingest/session")) return "session";
637
+ if (normalized.includes("/context/query")) return "query";
638
+ if (method === "GET") return "get";
639
+ return "writeAck";
640
+ }
189
641
  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);
642
+ const method = String(options.method || "GET").toUpperCase();
643
+ const normalizedEndpoint = normalizeEndpoint2(endpoint);
644
+ const operation = this.inferOperation(normalizedEndpoint, method);
645
+ let body;
646
+ if (typeof options.body === "string") {
195
647
  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));
648
+ body = JSON.parse(options.body);
649
+ } catch {
650
+ body = void 0;
651
+ }
652
+ } 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)) {
653
+ body = options.body;
654
+ }
655
+ try {
656
+ const response = await this.runtimeClient.request({
657
+ endpoint: normalizedEndpoint,
658
+ method,
659
+ operation,
660
+ idempotent: method === "GET" || method === "POST" && (operation === "search" || operation === "query" || operation === "profile"),
661
+ body,
662
+ headers: options.headers || {}
663
+ });
664
+ return response.data;
665
+ } catch (error) {
666
+ if (!(error instanceof RuntimeClientError)) {
667
+ throw error;
668
+ }
669
+ let message = error.message;
670
+ if (error.status === 404 && !isProjectNotFoundMessage(message)) {
671
+ const endpointHint = `${this.baseUrl}${normalizedEndpoint}`;
672
+ message = `Endpoint not found at ${endpointHint}. This deployment may not support this API route.`;
244
673
  }
674
+ const { code, retryable } = this.classifyError(error.status, message);
675
+ throw new WhisperError({
676
+ code,
677
+ message,
678
+ status: error.status,
679
+ retryable,
680
+ details: error.details
681
+ });
245
682
  }
246
- throw lastError instanceof Error ? lastError : new WhisperError({ code: "REQUEST_FAILED", message: "Request failed" });
247
683
  }
248
684
  async query(params) {
249
685
  const projectRef = this.getRequiredProject(params.project);
@@ -296,6 +732,69 @@ var WhisperContext = class _WhisperContext {
296
732
  async syncSource(sourceId) {
297
733
  return this.request(`/v1/sources/${sourceId}/sync`, { method: "POST" });
298
734
  }
735
+ async addSourceByType(projectId, params) {
736
+ const resolvedProjectId = await this.resolveProjectId(projectId);
737
+ return this.request(`/v1/projects/${resolvedProjectId}/add_source`, {
738
+ method: "POST",
739
+ body: JSON.stringify(params)
740
+ });
741
+ }
742
+ async getSourceStatus(sourceId) {
743
+ return this.request(`/v1/sources/${sourceId}/status`, { method: "GET" });
744
+ }
745
+ async createCanonicalSource(project, params) {
746
+ const connector_type = params.type === "github" ? "github" : params.type === "web" ? "website" : params.type === "pdf" ? "pdf" : params.type === "local" ? "local-folder" : "slack";
747
+ const config = {};
748
+ if (params.type === "github") {
749
+ if (!params.owner || !params.repo) throw new WhisperError({ code: "REQUEST_FAILED", message: "github source requires owner and repo" });
750
+ config.owner = params.owner;
751
+ config.repo = params.repo;
752
+ if (params.branch) config.branch = params.branch;
753
+ if (params.paths) config.paths = params.paths;
754
+ } else if (params.type === "web") {
755
+ if (!params.url) throw new WhisperError({ code: "REQUEST_FAILED", message: "web source requires url" });
756
+ config.url = params.url;
757
+ if (params.crawl_depth !== void 0) config.crawl_depth = params.crawl_depth;
758
+ if (params.include_paths) config.include_paths = params.include_paths;
759
+ if (params.exclude_paths) config.exclude_paths = params.exclude_paths;
760
+ } else if (params.type === "pdf") {
761
+ if (!params.url && !params.file_path) throw new WhisperError({ code: "REQUEST_FAILED", message: "pdf source requires url or file_path" });
762
+ if (params.url) config.url = params.url;
763
+ if (params.file_path) config.file_path = params.file_path;
764
+ } else if (params.type === "local") {
765
+ if (!params.path) throw new WhisperError({ code: "REQUEST_FAILED", message: "local source requires path" });
766
+ config.path = params.path;
767
+ if (params.glob) config.glob = params.glob;
768
+ if (params.max_files !== void 0) config.max_files = params.max_files;
769
+ } else {
770
+ config.channel_ids = params.channel_ids || [];
771
+ if (params.since) config.since = params.since;
772
+ if (params.workspace_id) config.workspace_id = params.workspace_id;
773
+ if (params.token) config.token = params.token;
774
+ if (params.auth_ref) config.auth_ref = params.auth_ref;
775
+ }
776
+ if (params.metadata) config.metadata = params.metadata;
777
+ config.auto_index = params.auto_index ?? true;
778
+ const created = await this.addSource(project, {
779
+ name: params.name || `${params.type}-source-${Date.now()}`,
780
+ connector_type,
781
+ config
782
+ });
783
+ let status = "queued";
784
+ let jobId = null;
785
+ if (params.auto_index ?? true) {
786
+ const syncRes = await this.syncSource(created.id);
787
+ status = "indexing";
788
+ jobId = String(syncRes?.id || syncRes?.job_id || "");
789
+ }
790
+ return {
791
+ source_id: created.id,
792
+ status,
793
+ job_id: jobId,
794
+ index_started: params.auto_index ?? true,
795
+ warnings: []
796
+ };
797
+ }
299
798
  async ingest(projectId, documents) {
300
799
  const resolvedProjectId = await this.resolveProjectId(projectId);
301
800
  return this.request(`/v1/projects/${resolvedProjectId}/ingest`, {
@@ -354,13 +853,18 @@ var WhisperContext = class _WhisperContext {
354
853
  session_id: params.session_id,
355
854
  agent_id: params.agent_id,
356
855
  importance: params.importance,
357
- metadata: params.metadata
856
+ metadata: params.metadata,
857
+ async: params.async,
858
+ write_mode: params.write_mode
358
859
  })
359
860
  });
360
- const id2 = direct?.memory?.id || direct?.id || direct?.memory_id;
861
+ const id2 = direct?.memory?.id || direct?.id || direct?.memory_id || direct?.job_id;
361
862
  if (id2) {
362
863
  return { id: id2, success: true, path: "sota", fallback_used: false };
363
864
  }
865
+ if (direct?.success === true) {
866
+ return { id: "", success: true, path: "sota", fallback_used: false };
867
+ }
364
868
  } catch (error) {
365
869
  if (params.allow_legacy_fallback === false) {
366
870
  throw error;
@@ -390,20 +894,109 @@ var WhisperContext = class _WhisperContext {
390
894
  return { id, success: true, path: "legacy", fallback_used: true };
391
895
  });
392
896
  }
393
- async searchMemories(params) {
897
+ async addMemoriesBulk(params) {
898
+ const projectRef = this.getRequiredProject(params.project);
899
+ return this.withProjectRefFallback(projectRef, async (project) => {
900
+ try {
901
+ return await this.request("/v1/memory/bulk", {
902
+ method: "POST",
903
+ body: JSON.stringify({ ...params, project })
904
+ });
905
+ } catch (error) {
906
+ if (!this.isEndpointNotFoundError(error)) {
907
+ throw error;
908
+ }
909
+ const created = await Promise.all(
910
+ params.memories.map(
911
+ (memory) => this.addMemory({
912
+ project,
913
+ content: memory.content,
914
+ memory_type: memory.memory_type,
915
+ user_id: memory.user_id,
916
+ session_id: memory.session_id,
917
+ agent_id: memory.agent_id,
918
+ importance: memory.importance,
919
+ metadata: memory.metadata,
920
+ allow_legacy_fallback: true
921
+ })
922
+ )
923
+ );
924
+ return {
925
+ success: true,
926
+ created: created.length,
927
+ memories: created,
928
+ path: "legacy",
929
+ fallback_used: true
930
+ };
931
+ }
932
+ });
933
+ }
934
+ async extractMemories(params) {
935
+ const projectRef = this.getRequiredProject(params.project);
936
+ return this.withProjectRefFallback(projectRef, (project) => this.request("/v1/memory/extract", {
937
+ method: "POST",
938
+ body: JSON.stringify({ ...params, project })
939
+ }));
940
+ }
941
+ async extractSessionMemories(params) {
394
942
  const projectRef = this.getRequiredProject(params.project);
395
- return this.withProjectRefFallback(projectRef, (project) => this.request("/v1/memory/search", {
943
+ return this.withProjectRefFallback(projectRef, (project) => this.request("/v1/memory/extract/session", {
396
944
  method: "POST",
397
945
  body: JSON.stringify({
398
- query: params.query,
946
+ ...params,
399
947
  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
948
+ messages: params.messages.map((m) => ({
949
+ ...m,
950
+ timestamp: m.timestamp || (/* @__PURE__ */ new Date()).toISOString()
951
+ }))
404
952
  })
405
953
  }));
406
954
  }
955
+ async searchMemories(params) {
956
+ const projectRef = this.getRequiredProject(params.project);
957
+ return this.withProjectRefFallback(projectRef, async (project) => {
958
+ try {
959
+ return await this.request("/v1/memory/search", {
960
+ method: "POST",
961
+ body: JSON.stringify({
962
+ query: params.query,
963
+ project,
964
+ user_id: params.user_id,
965
+ session_id: params.session_id,
966
+ memory_types: params.memory_type ? [params.memory_type] : void 0,
967
+ top_k: params.top_k || 10,
968
+ profile: params.profile,
969
+ include_pending: params.include_pending
970
+ })
971
+ });
972
+ } catch (error) {
973
+ if (!this.isEndpointNotFoundError(error)) {
974
+ throw error;
975
+ }
976
+ const legacyTypeMap = {
977
+ factual: "factual",
978
+ preference: "semantic",
979
+ event: "episodic",
980
+ relationship: "semantic",
981
+ opinion: "semantic",
982
+ goal: "semantic",
983
+ instruction: "procedural"
984
+ };
985
+ return this.request("/v1/memories/search", {
986
+ method: "POST",
987
+ body: JSON.stringify({
988
+ query: params.query,
989
+ project,
990
+ user_id: params.user_id,
991
+ session_id: params.session_id,
992
+ agent_id: params.agent_id,
993
+ memory_type: params.memory_type ? legacyTypeMap[params.memory_type] : void 0,
994
+ top_k: params.top_k || 10
995
+ })
996
+ });
997
+ }
998
+ });
999
+ }
407
1000
  async createApiKey(params) {
408
1001
  return this.request("/v1/keys", {
409
1002
  method: "POST",
@@ -418,10 +1011,29 @@ var WhisperContext = class _WhisperContext {
418
1011
  }
419
1012
  async searchMemoriesSOTA(params) {
420
1013
  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
- }));
1014
+ return this.withProjectRefFallback(projectRef, async (project) => {
1015
+ try {
1016
+ return await this.request("/v1/memory/search", {
1017
+ method: "POST",
1018
+ body: JSON.stringify({ ...params, project })
1019
+ });
1020
+ } catch (error) {
1021
+ if (!this.isEndpointNotFoundError(error)) {
1022
+ throw error;
1023
+ }
1024
+ const firstType = params.memory_types?.[0];
1025
+ return this.searchMemories({
1026
+ project,
1027
+ query: params.query,
1028
+ user_id: params.user_id,
1029
+ session_id: params.session_id,
1030
+ memory_type: firstType,
1031
+ top_k: params.top_k,
1032
+ profile: params.profile,
1033
+ include_pending: params.include_pending
1034
+ });
1035
+ }
1036
+ });
425
1037
  }
426
1038
  async ingestSession(params) {
427
1039
  const projectRef = this.getRequiredProject(params.project);
@@ -431,37 +1043,116 @@ var WhisperContext = class _WhisperContext {
431
1043
  }));
432
1044
  }
433
1045
  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 }
1046
+ const projectRef = this.getRequiredProject(params.project);
1047
+ return this.withProjectRefFallback(projectRef, async (project) => {
1048
+ const query = new URLSearchParams({
1049
+ project,
1050
+ ...params.limit && { limit: params.limit.toString() },
1051
+ ...params.since_date && { since_date: params.since_date },
1052
+ ...params.include_pending !== void 0 && { include_pending: String(params.include_pending) }
1053
+ });
1054
+ try {
1055
+ return await this.request(`/v1/memory/session/${params.session_id}?${query}`);
1056
+ } catch (error) {
1057
+ if (!this.isEndpointNotFoundError(error)) {
1058
+ throw error;
1059
+ }
1060
+ return { memories: [], count: 0 };
1061
+ }
439
1062
  });
440
- return this.request(`/v1/memory/session/${params.session_id}?${query}`);
441
1063
  }
442
1064
  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 }
1065
+ const projectRef = this.getRequiredProject(params.project);
1066
+ return this.withProjectRefFallback(projectRef, async (project) => {
1067
+ const query = new URLSearchParams({
1068
+ project,
1069
+ ...params.memory_types && { memory_types: params.memory_types },
1070
+ ...params.include_pending !== void 0 && { include_pending: String(params.include_pending) }
1071
+ });
1072
+ try {
1073
+ return await this.request(`/v1/memory/profile/${params.user_id}?${query}`);
1074
+ } catch (error) {
1075
+ if (!this.isEndpointNotFoundError(error)) {
1076
+ throw error;
1077
+ }
1078
+ const legacyQuery = new URLSearchParams({
1079
+ project,
1080
+ user_id: params.user_id,
1081
+ limit: "200"
1082
+ });
1083
+ const legacy = await this.request(`/v1/memories?${legacyQuery}`);
1084
+ const memories = Array.isArray(legacy?.memories) ? legacy.memories : [];
1085
+ return {
1086
+ user_id: params.user_id,
1087
+ memories,
1088
+ count: memories.length
1089
+ };
1090
+ }
447
1091
  });
448
- return this.request(`/v1/memory/profile/${params.user_id}?${query}`);
449
1092
  }
450
1093
  async getMemoryVersions(memoryId) {
451
1094
  return this.request(`/v1/memory/${memoryId}/versions`);
452
1095
  }
453
1096
  async updateMemory(memoryId, params) {
454
- return this.request(`/v1/memory/${memoryId}`, {
455
- method: "PUT",
456
- body: JSON.stringify(params)
457
- });
1097
+ try {
1098
+ return await this.request(`/v1/memory/${memoryId}`, {
1099
+ method: "PUT",
1100
+ body: JSON.stringify(params)
1101
+ });
1102
+ } catch (error) {
1103
+ if (!this.isEndpointNotFoundError(error)) {
1104
+ throw error;
1105
+ }
1106
+ const legacy = await this.request(`/v1/memories/${memoryId}`, {
1107
+ method: "PUT",
1108
+ body: JSON.stringify({
1109
+ content: params.content
1110
+ })
1111
+ });
1112
+ return {
1113
+ success: true,
1114
+ new_memory_id: legacy?.id || memoryId,
1115
+ old_memory_id: memoryId
1116
+ };
1117
+ }
458
1118
  }
459
1119
  async deleteMemory(memoryId) {
460
- return this.request(`/v1/memory/${memoryId}`, { method: "DELETE" });
1120
+ try {
1121
+ return await this.request(`/v1/memory/${memoryId}`, { method: "DELETE" });
1122
+ } catch (error) {
1123
+ if (!this.isEndpointNotFoundError(error)) {
1124
+ throw error;
1125
+ }
1126
+ await this.request(`/v1/memories/${memoryId}`, { method: "DELETE" });
1127
+ return {
1128
+ success: true,
1129
+ deleted: memoryId
1130
+ };
1131
+ }
461
1132
  }
462
1133
  async getMemoryRelations(memoryId) {
463
1134
  return this.request(`/v1/memory/${memoryId}/relations`);
464
1135
  }
1136
+ async getMemoryGraph(params) {
1137
+ const project = await this.resolveProjectId(this.getRequiredProject(params.project));
1138
+ const query = new URLSearchParams({
1139
+ project,
1140
+ ...params.user_id && { user_id: params.user_id },
1141
+ ...params.session_id && { session_id: params.session_id },
1142
+ ...params.include_inactive !== void 0 && { include_inactive: String(params.include_inactive) },
1143
+ ...params.limit !== void 0 && { limit: String(params.limit) }
1144
+ });
1145
+ return this.request(`/v1/memory/graph?${query}`);
1146
+ }
1147
+ async getConversationGraph(params) {
1148
+ const project = await this.resolveProjectId(this.getRequiredProject(params.project));
1149
+ const query = new URLSearchParams({
1150
+ project,
1151
+ ...params.include_inactive !== void 0 && { include_inactive: String(params.include_inactive) },
1152
+ ...params.limit !== void 0 && { limit: String(params.limit) }
1153
+ });
1154
+ return this.request(`/v1/memory/graph/conversation/${params.session_id}?${query}`);
1155
+ }
465
1156
  async oracleSearch(params) {
466
1157
  const project = await this.resolveProjectId(this.getRequiredProject(params.project));
467
1158
  return this.request("/v1/oracle/search", {
@@ -557,90 +1248,928 @@ var WhisperContext = class _WhisperContext {
557
1248
  body: JSON.stringify(params)
558
1249
  });
559
1250
  }
560
- async searchFiles(params) {
561
- return this.request("/v1/search/files", {
562
- method: "POST",
563
- body: JSON.stringify(params)
564
- });
1251
+ async searchFiles(params) {
1252
+ return this.request("/v1/search/files", {
1253
+ method: "POST",
1254
+ body: JSON.stringify(params)
1255
+ });
1256
+ }
1257
+ async getCostSavings(params = {}) {
1258
+ const resolvedProject = params.project ? await this.resolveProjectId(params.project) : void 0;
1259
+ const query = new URLSearchParams({
1260
+ ...resolvedProject && { project: resolvedProject },
1261
+ ...params.start_date && { start_date: params.start_date },
1262
+ ...params.end_date && { end_date: params.end_date }
1263
+ });
1264
+ return this.request(`/v1/cost/savings?${query}`);
1265
+ }
1266
+ // Backward-compatible grouped namespaces.
1267
+ projects = {
1268
+ create: (params) => this.createProject(params),
1269
+ list: () => this.listProjects(),
1270
+ get: (id) => this.getProject(id),
1271
+ delete: (id) => this.deleteProject(id)
1272
+ };
1273
+ sources = {
1274
+ add: (projectId, params) => this.addSource(projectId, params),
1275
+ addSource: (projectId, params) => this.addSourceByType(projectId, params),
1276
+ sync: (sourceId) => this.syncSource(sourceId),
1277
+ syncSource: (sourceId) => this.syncSource(sourceId),
1278
+ status: (sourceId) => this.getSourceStatus(sourceId),
1279
+ getStatus: (sourceId) => this.getSourceStatus(sourceId)
1280
+ };
1281
+ memory = {
1282
+ add: (params) => this.addMemory(params),
1283
+ addBulk: (params) => this.addMemoriesBulk(params),
1284
+ extract: (params) => this.extractMemories(params),
1285
+ extractSession: (params) => this.extractSessionMemories(params),
1286
+ search: (params) => this.searchMemories(params),
1287
+ searchSOTA: (params) => this.searchMemoriesSOTA(params),
1288
+ ingestSession: (params) => this.ingestSession(params),
1289
+ getSessionMemories: (params) => this.getSessionMemories(params),
1290
+ getUserProfile: (params) => this.getUserProfile(params),
1291
+ getVersions: (memoryId) => this.getMemoryVersions(memoryId),
1292
+ update: (memoryId, params) => this.updateMemory(memoryId, params),
1293
+ delete: (memoryId) => this.deleteMemory(memoryId),
1294
+ getRelations: (memoryId) => this.getMemoryRelations(memoryId),
1295
+ getGraph: (params) => this.getMemoryGraph(params),
1296
+ getConversationGraph: (params) => this.getConversationGraph(params),
1297
+ consolidate: (params) => this.consolidateMemories(params),
1298
+ updateDecay: (params) => this.updateImportanceDecay(params),
1299
+ getImportanceStats: (project) => this.getImportanceStats(project)
1300
+ };
1301
+ keys = {
1302
+ create: (params) => this.createApiKey(params),
1303
+ list: () => this.listApiKeys(),
1304
+ getUsage: (days) => this.getUsage(days)
1305
+ };
1306
+ oracle = {
1307
+ search: (params) => this.oracleSearch(params)
1308
+ };
1309
+ context = {
1310
+ createShare: (params) => this.createSharedContext(params),
1311
+ loadShare: (shareId) => this.loadSharedContext(shareId),
1312
+ resumeShare: (params) => this.resumeFromSharedContext(params)
1313
+ };
1314
+ optimization = {
1315
+ getCacheStats: () => this.getCacheStats(),
1316
+ warmCache: (params) => this.warmCache(params),
1317
+ clearCache: (params) => this.clearCache(params),
1318
+ getCostSummary: (params) => this.getCostSummary(params),
1319
+ getCostBreakdown: (params) => this.getCostBreakdown(params),
1320
+ getCostSavings: (params) => this.getCostSavings(params)
1321
+ };
1322
+ };
1323
+
1324
+ // ../src/mcp/server.ts
1325
+ var API_KEY = process.env.WHISPER_API_KEY || "";
1326
+ var DEFAULT_PROJECT = process.env.WHISPER_PROJECT || "";
1327
+ var BASE_URL = process.env.WHISPER_BASE_URL;
1328
+ var RUNTIME_MODE = (process.env.WHISPER_MCP_MODE || "remote").toLowerCase();
1329
+ var CLI_ARGS = process.argv.slice(2);
1330
+ var IS_MANAGEMENT_ONLY = CLI_ARGS.includes("--print-tool-map") || CLI_ARGS[0] === "scope";
1331
+ var whisper = !IS_MANAGEMENT_ONLY && API_KEY ? new WhisperContext({
1332
+ apiKey: API_KEY,
1333
+ project: DEFAULT_PROJECT,
1334
+ ...BASE_URL && { baseUrl: BASE_URL }
1335
+ }) : null;
1336
+ var server = new McpServer({
1337
+ name: "whisper-context",
1338
+ version: "0.2.8"
1339
+ });
1340
+ var STATE_DIR = join(homedir(), ".whisper-mcp");
1341
+ var STATE_PATH = join(STATE_DIR, "state.json");
1342
+ var AUDIT_LOG_PATH = join(STATE_DIR, "forget-audit.log");
1343
+ var LOCAL_INGEST_MANIFEST_PATH = join(STATE_DIR, "local-ingest-manifest.json");
1344
+ var TOOL_MIGRATION_MAP = [
1345
+ { old: "list_projects", next: "context.list_projects" },
1346
+ { old: "list_sources", next: "context.list_sources" },
1347
+ { old: "add_source", next: "context.add_source" },
1348
+ { old: "source_status", next: "context.source_status" },
1349
+ { old: "query_context", next: "context.query" },
1350
+ { old: "get_relevant_context", next: "context.get_relevant" },
1351
+ { old: "claim_verifier", next: "context.claim_verify" },
1352
+ { old: "evidence_locked_answer", next: "context.evidence_answer" },
1353
+ { old: "export_context_bundle", next: "context.export_bundle" },
1354
+ { old: "import_context_bundle", next: "context.import_bundle" },
1355
+ { old: "diff_context", next: "context.diff" },
1356
+ { old: "share_context", next: "context.share" },
1357
+ { old: "add_memory", next: "memory.add" },
1358
+ { old: "search_memories", next: "memory.search" },
1359
+ { old: "forget", next: "memory.forget" },
1360
+ { old: "consolidate_memories", next: "memory.consolidate" },
1361
+ { old: "oracle_search", next: "research.oracle" },
1362
+ { old: "repo_index_status", next: "index.workspace_status" },
1363
+ { old: "index_workspace", next: "index.workspace_run" },
1364
+ { old: "autosubscribe_dependencies", next: "index.autosubscribe_deps" },
1365
+ { old: "search_files", next: "code.search_text" },
1366
+ { old: "semantic_search_codebase", next: "code.search_semantic" }
1367
+ ];
1368
+ function ensureStateDir() {
1369
+ if (!existsSync(STATE_DIR)) {
1370
+ mkdirSync(STATE_DIR, { recursive: true });
1371
+ }
1372
+ }
1373
+ function getWorkspaceId(workspaceId) {
1374
+ if (workspaceId?.trim()) return workspaceId.trim();
1375
+ const seed = `${process.cwd()}|${DEFAULT_PROJECT || "default"}|${API_KEY.slice(0, 12)}`;
1376
+ return createHash("sha256").update(seed).digest("hex").slice(0, 20);
1377
+ }
1378
+ function getWorkspaceIdForPath(path, workspaceId) {
1379
+ if (workspaceId?.trim()) return workspaceId.trim();
1380
+ if (!path) return getWorkspaceId(void 0);
1381
+ const seed = `${path}|${DEFAULT_PROJECT || "default"}|${API_KEY.slice(0, 12)}`;
1382
+ return createHash("sha256").update(seed).digest("hex").slice(0, 20);
1383
+ }
1384
+ function clamp01(value) {
1385
+ if (Number.isNaN(value)) return 0;
1386
+ if (value < 0) return 0;
1387
+ if (value > 1) return 1;
1388
+ return value;
1389
+ }
1390
+ function renderCitation(ev) {
1391
+ const videoUrl = ev.metadata?.video_url;
1392
+ const tsRaw = ev.metadata?.timestamp_start_ms;
1393
+ const ts = tsRaw ? Number(tsRaw) : NaN;
1394
+ if (videoUrl && Number.isFinite(ts) && ts >= 0) {
1395
+ const totalSeconds = Math.floor(ts / 1e3);
1396
+ const minutes = Math.floor(totalSeconds / 60);
1397
+ const seconds = totalSeconds % 60;
1398
+ return `${videoUrl} @ ${minutes}:${String(seconds).padStart(2, "0")}`;
1399
+ }
1400
+ return ev.line_end && ev.line_end !== ev.line_start ? `${ev.path}:${ev.line_start}-${ev.line_end}` : `${ev.path}:${ev.line_start}`;
1401
+ }
1402
+ function extractLineStart(metadata) {
1403
+ const raw = metadata.line_start ?? metadata.line ?? metadata.start_line ?? metadata.startLine ?? 1;
1404
+ const parsed = Number(raw);
1405
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 1;
1406
+ }
1407
+ function extractLineEnd(metadata, start) {
1408
+ const raw = metadata.line_end ?? metadata.end_line ?? metadata.endLine;
1409
+ if (raw === void 0 || raw === null) return void 0;
1410
+ const parsed = Number(raw);
1411
+ if (!Number.isFinite(parsed) || parsed < start) return void 0;
1412
+ return Math.floor(parsed);
1413
+ }
1414
+ function toEvidenceRef(source, workspaceId, methodFallback) {
1415
+ const metadata = source.metadata || {};
1416
+ const lineStart = extractLineStart(metadata);
1417
+ const lineEnd = extractLineEnd(metadata, lineStart);
1418
+ const path = String(
1419
+ metadata.file_path ?? metadata.path ?? source.source ?? source.document ?? source.id ?? "unknown"
1420
+ );
1421
+ const rawMethod = String(source.retrieval_source || methodFallback).toLowerCase();
1422
+ const retrievalMethod = rawMethod.includes("graph") ? "graph" : rawMethod.includes("memory") ? "memory" : rawMethod.includes("lex") ? "lexical" : rawMethod.includes("symbol") ? "symbol" : "semantic";
1423
+ return {
1424
+ evidence_id: randomUUID(),
1425
+ source_id: String(source.id || source.document || source.source || randomUUID()),
1426
+ path,
1427
+ line_start: lineStart,
1428
+ ...lineEnd ? { line_end: lineEnd } : {},
1429
+ snippet: String(source.content || metadata.snippet || "").slice(0, 500),
1430
+ score: clamp01(Number(source.score ?? metadata.score ?? 0)),
1431
+ retrieval_method: retrievalMethod,
1432
+ indexed_at: String(metadata.indexed_at || (/* @__PURE__ */ new Date()).toISOString()),
1433
+ ...metadata.commit ? { commit: String(metadata.commit) } : {},
1434
+ workspace_id: workspaceId,
1435
+ metadata: {
1436
+ source: String(source.source || ""),
1437
+ document: String(source.document || ""),
1438
+ video_url: String(metadata.video_url || ""),
1439
+ timestamp_start_ms: String(metadata.timestamp_start_ms ?? ""),
1440
+ timestamp_end_ms: String(metadata.timestamp_end_ms ?? ""),
1441
+ citation: String(metadata.citation || "")
1442
+ }
1443
+ };
1444
+ }
1445
+ function loadState() {
1446
+ ensureStateDir();
1447
+ if (!existsSync(STATE_PATH)) {
1448
+ return { workspaces: {} };
1449
+ }
1450
+ try {
1451
+ const parsed = JSON.parse(readFileSync(STATE_PATH, "utf-8"));
1452
+ if (!parsed || typeof parsed !== "object") return { workspaces: {} };
1453
+ return parsed;
1454
+ } catch {
1455
+ return { workspaces: {} };
1456
+ }
1457
+ }
1458
+ function saveState(state) {
1459
+ ensureStateDir();
1460
+ writeFileSync(STATE_PATH, JSON.stringify(state, null, 2), "utf-8");
1461
+ }
1462
+ function getWorkspaceState(state, workspaceId) {
1463
+ if (!state.workspaces[workspaceId]) {
1464
+ state.workspaces[workspaceId] = {
1465
+ decisions: [],
1466
+ failures: [],
1467
+ entities: [],
1468
+ documents: [],
1469
+ annotations: [],
1470
+ session_summaries: [],
1471
+ events: [],
1472
+ index_metadata: {}
1473
+ };
1474
+ }
1475
+ return state.workspaces[workspaceId];
1476
+ }
1477
+ function computeChecksum(value) {
1478
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
1479
+ }
1480
+ var cachedProjectRef = DEFAULT_PROJECT || void 0;
1481
+ async function resolveProjectRef(explicit) {
1482
+ if (explicit?.trim()) return explicit.trim();
1483
+ if (cachedProjectRef) return cachedProjectRef;
1484
+ try {
1485
+ const { projects } = await whisper.listProjects();
1486
+ const first = projects?.[0];
1487
+ if (!first) return void 0;
1488
+ cachedProjectRef = first.slug || first.name || first.id;
1489
+ return cachedProjectRef;
1490
+ } catch {
1491
+ return void 0;
1492
+ }
1493
+ }
1494
+ function buildAbstain(args) {
1495
+ return {
1496
+ status: "abstained",
1497
+ answer: null,
1498
+ reason: args.reason,
1499
+ message: args.message,
1500
+ closest_evidence: args.closest_evidence,
1501
+ recommended_next_calls: ["index.workspace_status", "index.workspace_run", "symbol_search", "context.get_relevant"],
1502
+ diagnostics: {
1503
+ claims_evaluated: args.claims_evaluated,
1504
+ evidence_items_found: args.evidence_items_found,
1505
+ min_required: args.min_required,
1506
+ index_fresh: args.index_fresh
1507
+ }
1508
+ };
1509
+ }
1510
+ function getGitHead(searchPath) {
1511
+ const root = searchPath || process.cwd();
1512
+ const result = spawnSync("git", ["-C", root, "rev-parse", "HEAD"], { encoding: "utf-8" });
1513
+ if (result.status !== 0) return void 0;
1514
+ const out = String(result.stdout || "").trim();
1515
+ return out || void 0;
1516
+ }
1517
+ function getGitPendingCount(searchPath) {
1518
+ const root = searchPath || process.cwd();
1519
+ const result = spawnSync("git", ["-C", root, "status", "--porcelain"], { encoding: "utf-8" });
1520
+ if (result.status !== 0) return void 0;
1521
+ const out = String(result.stdout || "").trim();
1522
+ if (!out) return 0;
1523
+ return out.split("\n").filter(Boolean).length;
1524
+ }
1525
+ function countCodeFiles(searchPath, maxFiles = 5e3) {
1526
+ const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", "build", "__pycache__", ".turbo", "coverage", ".cache"]);
1527
+ 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"]);
1528
+ let total = 0;
1529
+ let skipped = 0;
1530
+ function walk(dir) {
1531
+ if (total >= maxFiles) return;
1532
+ let entries;
1533
+ try {
1534
+ entries = readdirSync(dir, { withFileTypes: true });
1535
+ } catch {
1536
+ return;
1537
+ }
1538
+ for (const e of entries) {
1539
+ if (total >= maxFiles) return;
1540
+ if (skip.has(e.name)) {
1541
+ skipped += 1;
1542
+ continue;
1543
+ }
1544
+ const full = join(dir, e.name);
1545
+ if (e.isDirectory()) walk(full);
1546
+ else if (e.isFile()) {
1547
+ const ext = extname(e.name).replace(".", "");
1548
+ if (exts.has(ext)) total += 1;
1549
+ }
1550
+ }
1551
+ }
1552
+ walk(searchPath);
1553
+ return { total, skipped };
1554
+ }
1555
+ function toTextResult(payload) {
1556
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
1557
+ }
1558
+ function likelyEmbeddingFailure(error) {
1559
+ const message = String(error?.message || error || "").toLowerCase();
1560
+ return message.includes("embedding") || message.includes("vector") || message.includes("timeout") || message.includes("timed out") || message.includes("temporarily unavailable");
1561
+ }
1562
+ async function queryWithDegradedFallback(params) {
1563
+ try {
1564
+ const response = await whisper.query({
1565
+ project: params.project,
1566
+ query: params.query,
1567
+ top_k: params.top_k,
1568
+ include_memories: params.include_memories,
1569
+ include_graph: params.include_graph,
1570
+ hybrid: true,
1571
+ rerank: true
1572
+ });
1573
+ return { response, degraded_mode: false };
1574
+ } catch (error) {
1575
+ if (!likelyEmbeddingFailure(error)) throw error;
1576
+ const response = await whisper.query({
1577
+ project: params.project,
1578
+ query: params.query,
1579
+ top_k: params.top_k,
1580
+ include_memories: params.include_memories,
1581
+ include_graph: false,
1582
+ hybrid: false,
1583
+ rerank: false,
1584
+ vector_weight: 0,
1585
+ bm25_weight: 1
1586
+ });
1587
+ return {
1588
+ response,
1589
+ degraded_mode: true,
1590
+ degraded_reason: "Embedding/graph path unavailable; lexical fallback used.",
1591
+ recommendation: "Check embedding service health, then re-run for full hybrid quality."
1592
+ };
1593
+ }
1594
+ }
1595
+ function getLocalAllowlistRoots() {
1596
+ const fromEnv = (process.env.WHISPER_LOCAL_ALLOWLIST || "").split(",").map((v) => v.trim()).filter(Boolean);
1597
+ if (fromEnv.length > 0) return fromEnv;
1598
+ return [process.cwd()];
1599
+ }
1600
+ function isPathAllowed(targetPath) {
1601
+ const normalized = targetPath.replace(/\\/g, "/").toLowerCase();
1602
+ const allowlist = getLocalAllowlistRoots();
1603
+ const allowed = allowlist.some((root) => normalized.startsWith(root.replace(/\\/g, "/").toLowerCase()));
1604
+ return { allowed, allowlist };
1605
+ }
1606
+ function shouldSkipSensitivePath(filePath) {
1607
+ const p = filePath.replace(/\\/g, "/").toLowerCase();
1608
+ const denySnippets = [
1609
+ "/node_modules/",
1610
+ "/.git/",
1611
+ "/dist/",
1612
+ "/build/",
1613
+ "/.next/",
1614
+ "/.aws/",
1615
+ "/.ssh/",
1616
+ ".pem",
1617
+ ".key",
1618
+ ".env",
1619
+ "credentials"
1620
+ ];
1621
+ return denySnippets.some((s) => p.includes(s));
1622
+ }
1623
+ function redactLikelySecrets(content) {
1624
+ return content.replace(/(api[_-]?key\s*[=:]\s*)[^\s"'`]+/gi, "$1[REDACTED]").replace(/(token\s*[=:]\s*)[^\s"'`]+/gi, "$1[REDACTED]").replace(/(secret\s*[=:]\s*)[^\s"'`]+/gi, "$1[REDACTED]");
1625
+ }
1626
+ function loadIngestManifest() {
1627
+ ensureStateDir();
1628
+ if (!existsSync(LOCAL_INGEST_MANIFEST_PATH)) return {};
1629
+ try {
1630
+ const parsed = JSON.parse(readFileSync(LOCAL_INGEST_MANIFEST_PATH, "utf-8"));
1631
+ return parsed && typeof parsed === "object" ? parsed : {};
1632
+ } catch {
1633
+ return {};
1634
+ }
1635
+ }
1636
+ function saveIngestManifest(manifest) {
1637
+ ensureStateDir();
1638
+ writeFileSync(LOCAL_INGEST_MANIFEST_PATH, JSON.stringify(manifest, null, 2), "utf-8");
1639
+ }
1640
+ async function ingestLocalPath(params) {
1641
+ if (RUNTIME_MODE === "remote") {
1642
+ throw new Error("Local ingestion is disabled in remote mode. Set WHISPER_MCP_MODE=auto or local.");
1643
+ }
1644
+ const rootPath = params.path || process.cwd();
1645
+ const gate = isPathAllowed(rootPath);
1646
+ if (!gate.allowed) {
1647
+ throw new Error(`Path not allowed by WHISPER_LOCAL_ALLOWLIST. Allowed roots: ${gate.allowlist.join(", ")}`);
1648
+ }
1649
+ const maxFiles = Math.max(1, params.max_files || 200);
1650
+ const maxBytesPerFile = 512 * 1024;
1651
+ const files = [];
1652
+ function collect(dir) {
1653
+ if (files.length >= maxFiles) return;
1654
+ let entries;
1655
+ try {
1656
+ entries = readdirSync(dir, { withFileTypes: true });
1657
+ } catch {
1658
+ return;
1659
+ }
1660
+ for (const entry of entries) {
1661
+ if (files.length >= maxFiles) return;
1662
+ const full = join(dir, entry.name);
1663
+ if (entry.isDirectory()) {
1664
+ if (shouldSkipSensitivePath(full)) continue;
1665
+ collect(full);
1666
+ } else if (entry.isFile()) {
1667
+ if (shouldSkipSensitivePath(full)) continue;
1668
+ files.push(full);
1669
+ }
1670
+ }
1671
+ }
1672
+ collect(rootPath);
1673
+ const manifest = loadIngestManifest();
1674
+ const workspaceId = getWorkspaceIdForPath(rootPath);
1675
+ if (!manifest[workspaceId]) manifest[workspaceId] = { last_run_at: (/* @__PURE__ */ new Date(0)).toISOString(), files: {} };
1676
+ const docs = [];
1677
+ const skipped = [];
1678
+ for (const fullPath of files) {
1679
+ try {
1680
+ const st = statSync(fullPath);
1681
+ if (st.size > maxBytesPerFile) {
1682
+ skipped.push(`${relative(rootPath, fullPath)} (size>${maxBytesPerFile})`);
1683
+ continue;
1684
+ }
1685
+ const mtime = String(st.mtimeMs);
1686
+ const rel = relative(rootPath, fullPath);
1687
+ if (manifest[workspaceId].files[rel] === mtime) continue;
1688
+ const raw = readFileSync(fullPath, "utf-8");
1689
+ const content = redactLikelySecrets(raw).slice(0, params.chunk_chars || 2e4);
1690
+ docs.push({
1691
+ title: rel,
1692
+ content,
1693
+ file_path: rel,
1694
+ metadata: { source_type: "local", path: rel, ingested_at: (/* @__PURE__ */ new Date()).toISOString() }
1695
+ });
1696
+ manifest[workspaceId].files[rel] = mtime;
1697
+ } catch {
1698
+ skipped.push(relative(rootPath, fullPath));
1699
+ }
1700
+ }
1701
+ let ingested = 0;
1702
+ const batchSize = 25;
1703
+ for (let i = 0; i < docs.length; i += batchSize) {
1704
+ const batch = docs.slice(i, i + batchSize);
1705
+ const result = await whisper.ingest(params.project, batch);
1706
+ ingested += Number(result.ingested || batch.length);
1707
+ }
1708
+ manifest[workspaceId].last_run_at = (/* @__PURE__ */ new Date()).toISOString();
1709
+ saveIngestManifest(manifest);
1710
+ appendFileSync(
1711
+ AUDIT_LOG_PATH,
1712
+ `${(/* @__PURE__ */ new Date()).toISOString()} local_ingest workspace=${workspaceId} root_hash=${createHash("sha256").update(rootPath).digest("hex").slice(0, 16)} files=${docs.length}
1713
+ `
1714
+ );
1715
+ return { ingested, scanned: files.length, queued: docs.length, skipped, workspace_id: workspaceId };
1716
+ }
1717
+ async function createSourceByType(params) {
1718
+ const connector_type = params.type === "github" ? "github" : params.type === "web" ? "website" : params.type === "pdf" ? "pdf" : params.type === "local" ? "local-folder" : "slack";
1719
+ const config = {};
1720
+ if (params.type === "github") {
1721
+ if (!params.owner || !params.repo) throw new Error("github source requires owner and repo");
1722
+ config.owner = params.owner;
1723
+ config.repo = params.repo;
1724
+ if (params.branch) config.branch = params.branch;
1725
+ if (params.paths) config.paths = params.paths;
1726
+ } else if (params.type === "web") {
1727
+ if (!params.url) throw new Error("web source requires url");
1728
+ config.url = params.url;
1729
+ if (params.crawl_depth !== void 0) config.crawl_depth = params.crawl_depth;
1730
+ if (params.include_paths) config.include_paths = params.include_paths;
1731
+ if (params.exclude_paths) config.exclude_paths = params.exclude_paths;
1732
+ } else if (params.type === "pdf") {
1733
+ if (!params.url && !params.file_path) throw new Error("pdf source requires url or file_path");
1734
+ if (params.url) config.url = params.url;
1735
+ if (params.file_path) config.file_path = params.file_path;
1736
+ } else if (params.type === "local") {
1737
+ if (!params.path) throw new Error("local source requires path");
1738
+ const ingest = await ingestLocalPath({
1739
+ project: params.project,
1740
+ path: params.path,
1741
+ glob: params.glob,
1742
+ max_files: params.max_files
1743
+ });
1744
+ return {
1745
+ source_id: `local_${ingest.workspace_id}`,
1746
+ status: "ready",
1747
+ job_id: `local_ingest_${Date.now()}`,
1748
+ index_started: true,
1749
+ warnings: ingest.skipped.slice(0, 20),
1750
+ details: ingest
1751
+ };
1752
+ } else if (params.type === "slack") {
1753
+ config.channel_ids = params.channel_ids || [];
1754
+ if (params.since) config.since = params.since;
1755
+ if (params.workspace_id) config.workspace_id = params.workspace_id;
1756
+ if (params.token) config.token = params.token;
1757
+ if (params.auth_ref) config.auth_ref = params.auth_ref;
1758
+ }
1759
+ if (params.metadata) config.metadata = params.metadata;
1760
+ config.auto_index = params.auto_index ?? true;
1761
+ const created = await whisper.addSource(params.project, {
1762
+ name: params.name || `${params.type}-source-${Date.now()}`,
1763
+ connector_type,
1764
+ config
1765
+ });
1766
+ let jobId;
1767
+ let status = "queued";
1768
+ if (params.auto_index ?? true) {
1769
+ const syncRes = await whisper.syncSource(created.id);
1770
+ jobId = String(syncRes?.id || syncRes?.job_id || "");
1771
+ status = "indexing";
1772
+ }
1773
+ return {
1774
+ source_id: created.id,
1775
+ status,
1776
+ job_id: jobId || null,
1777
+ index_started: params.auto_index ?? true,
1778
+ warnings: []
1779
+ };
1780
+ }
1781
+ function scopeConfigJson(project, source, client) {
1782
+ const serverDef = {
1783
+ command: "npx",
1784
+ args: ["-y", "@usewhisper/mcp-server"],
1785
+ env: {
1786
+ WHISPER_API_KEY: "wctx_...",
1787
+ WHISPER_PROJECT: project,
1788
+ WHISPER_SCOPE_SOURCE: source
1789
+ }
1790
+ };
1791
+ if (client === "json") {
1792
+ return JSON.stringify({ mcpServers: { "whisper-context-scoped": serverDef } }, null, 2);
1793
+ }
1794
+ return JSON.stringify({ mcpServers: { "whisper-context-scoped": serverDef } }, null, 2);
1795
+ }
1796
+ function printToolMap() {
1797
+ console.log("Legacy -> canonical MCP tool names:");
1798
+ for (const row of TOOL_MIGRATION_MAP) {
1799
+ console.log(`- ${row.old} => ${row.next}`);
1800
+ }
1801
+ }
1802
+ server.tool(
1803
+ "index.workspace_resolve",
1804
+ "Resolve workspace identity from path + API key and map to a project without mandatory dashboard setup.",
1805
+ {
1806
+ path: z.string().optional().describe("Workspace path. Defaults to current working directory."),
1807
+ workspace_id: z.string().optional(),
1808
+ project: z.string().optional()
1809
+ },
1810
+ async ({ path, workspace_id, project }) => {
1811
+ try {
1812
+ const workspaceId = getWorkspaceIdForPath(path, workspace_id);
1813
+ const state = loadState();
1814
+ const existed = Boolean(state.workspaces[workspaceId]);
1815
+ const workspace = getWorkspaceState(state, workspaceId);
1816
+ const resolvedProject = await resolveProjectRef(project);
1817
+ const resolvedBy = project?.trim() ? "explicit_project" : DEFAULT_PROJECT ? "env_default" : resolvedProject ? "auto_first_project" : "unresolved";
1818
+ saveState(state);
1819
+ const payload = {
1820
+ workspace_id: workspaceId,
1821
+ project_id: resolvedProject || null,
1822
+ created: !existed,
1823
+ resolved_by: resolvedBy,
1824
+ index_state: {
1825
+ last_indexed_at: workspace.index_metadata?.last_indexed_at || null,
1826
+ last_indexed_commit: workspace.index_metadata?.last_indexed_commit || null,
1827
+ coverage: workspace.index_metadata?.coverage ?? 0
1828
+ }
1829
+ };
1830
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
1831
+ } catch (error) {
1832
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
1833
+ }
1834
+ }
1835
+ );
1836
+ server.tool(
1837
+ "index.workspace_status",
1838
+ "Check index freshness, coverage, commit, and pending changes before retrieval/edits.",
1839
+ {
1840
+ workspace_id: z.string().optional(),
1841
+ path: z.string().optional()
1842
+ },
1843
+ async ({ workspace_id, path }) => {
1844
+ try {
1845
+ const rootPath = path || process.cwd();
1846
+ const workspaceId = getWorkspaceIdForPath(rootPath, workspace_id);
1847
+ const state = loadState();
1848
+ const workspace = getWorkspaceState(state, workspaceId);
1849
+ const lastIndexedAt = workspace.index_metadata?.last_indexed_at;
1850
+ const ageHours = lastIndexedAt ? (Date.now() - new Date(lastIndexedAt).getTime()) / (60 * 60 * 1e3) : null;
1851
+ const stale = ageHours === null ? true : ageHours > 168;
1852
+ const payload = {
1853
+ workspace_id: workspaceId,
1854
+ freshness: {
1855
+ stale,
1856
+ age_hours: ageHours,
1857
+ last_indexed_at: lastIndexedAt || null
1858
+ },
1859
+ coverage: workspace.index_metadata?.coverage ?? 0,
1860
+ last_indexed_commit: workspace.index_metadata?.last_indexed_commit || null,
1861
+ current_commit: getGitHead(rootPath) || null,
1862
+ pending_changes: getGitPendingCount(rootPath)
1863
+ };
1864
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
1865
+ } catch (error) {
1866
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
1867
+ }
1868
+ }
1869
+ );
1870
+ server.tool(
1871
+ "index.workspace_run",
1872
+ "Index workspace in full or incremental mode and update index metadata for freshness checks.",
1873
+ {
1874
+ workspace_id: z.string().optional(),
1875
+ path: z.string().optional(),
1876
+ mode: z.enum(["full", "incremental"]).optional().default("incremental"),
1877
+ max_files: z.number().optional().default(1500)
1878
+ },
1879
+ async ({ workspace_id, path, mode, max_files }) => {
1880
+ try {
1881
+ const rootPath = path || process.cwd();
1882
+ const workspaceId = getWorkspaceIdForPath(rootPath, workspace_id);
1883
+ const state = loadState();
1884
+ const workspace = getWorkspaceState(state, workspaceId);
1885
+ const fileStats = countCodeFiles(rootPath, max_files);
1886
+ const coverage = Math.max(0, Math.min(1, fileStats.total / Math.max(1, max_files)));
1887
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1888
+ workspace.index_metadata = {
1889
+ last_indexed_at: now,
1890
+ last_indexed_commit: getGitHead(rootPath),
1891
+ coverage: mode === "full" ? 1 : coverage
1892
+ };
1893
+ saveState(state);
1894
+ const payload = {
1895
+ workspace_id: workspaceId,
1896
+ mode,
1897
+ indexed_files: fileStats.total,
1898
+ skipped_files: fileStats.skipped,
1899
+ duration_ms: 0,
1900
+ warnings: fileStats.total === 0 ? ["No code files discovered for indexing."] : [],
1901
+ index_metadata: workspace.index_metadata
1902
+ };
1903
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
1904
+ } catch (error) {
1905
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
1906
+ }
1907
+ }
1908
+ );
1909
+ server.tool(
1910
+ "index.local_scan_ingest",
1911
+ "Scan a local folder safely (allowlist + secret filters), ingest changed files, and persist incremental manifest.",
1912
+ {
1913
+ project: z.string().optional().describe("Project name or slug"),
1914
+ path: z.string().optional().describe("Local path to ingest. Defaults to current working directory."),
1915
+ glob: z.string().optional().describe("Optional include glob"),
1916
+ max_files: z.number().optional().default(200),
1917
+ chunk_chars: z.number().optional().default(2e4)
1918
+ },
1919
+ async ({ project, path, glob, max_files, chunk_chars }) => {
1920
+ try {
1921
+ const resolvedProject = await resolveProjectRef(project);
1922
+ if (!resolvedProject) {
1923
+ return { content: [{ type: "text", text: "Error: No project resolved. Set WHISPER_PROJECT or provide project." }] };
1924
+ }
1925
+ const result = await ingestLocalPath({
1926
+ project: resolvedProject,
1927
+ path: path || process.cwd(),
1928
+ glob,
1929
+ max_files,
1930
+ chunk_chars
1931
+ });
1932
+ return toTextResult({
1933
+ source_id: `local_${result.workspace_id}`,
1934
+ status: "ready",
1935
+ job_id: `local_ingest_${Date.now()}`,
1936
+ index_started: true,
1937
+ warnings: result.skipped.slice(0, 20),
1938
+ details: result
1939
+ });
1940
+ } catch (error) {
1941
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
1942
+ }
1943
+ }
1944
+ );
1945
+ server.tool(
1946
+ "context.get_relevant",
1947
+ "Core retrieval. Task goes in, ranked context chunks come out with structured evidence (file:line ready).",
1948
+ {
1949
+ question: z.string().describe("Task/question to retrieve context for"),
1950
+ workspace_id: z.string().optional(),
1951
+ project: z.string().optional(),
1952
+ top_k: z.number().optional().default(12),
1953
+ include_memories: z.boolean().optional().default(true),
1954
+ include_graph: z.boolean().optional().default(true),
1955
+ session_id: z.string().optional(),
1956
+ user_id: z.string().optional()
1957
+ },
1958
+ async ({ question, workspace_id, project, top_k, include_memories, include_graph, session_id, user_id }) => {
1959
+ try {
1960
+ const workspaceId = getWorkspaceId(workspace_id);
1961
+ const resolvedProject = await resolveProjectRef(project);
1962
+ if (!resolvedProject) {
1963
+ const payload2 = {
1964
+ question,
1965
+ workspace_id: workspaceId,
1966
+ total_results: 0,
1967
+ context: "",
1968
+ evidence: [],
1969
+ used_context_ids: [],
1970
+ latency_ms: 0,
1971
+ warning: "No project resolved. Set WHISPER_PROJECT or create one in your account."
1972
+ };
1973
+ return { content: [{ type: "text", text: JSON.stringify(payload2, null, 2) }] };
1974
+ }
1975
+ const queryResult = await queryWithDegradedFallback({
1976
+ project: resolvedProject,
1977
+ query: question,
1978
+ top_k,
1979
+ include_memories,
1980
+ include_graph,
1981
+ session_id,
1982
+ user_id
1983
+ });
1984
+ const response = queryResult.response;
1985
+ const evidence = (response.results || []).map((r) => toEvidenceRef(r, workspaceId, "semantic"));
1986
+ const payload = {
1987
+ question,
1988
+ workspace_id: workspaceId,
1989
+ total_results: response.meta?.total || evidence.length,
1990
+ context: response.context || "",
1991
+ evidence,
1992
+ used_context_ids: (response.results || []).map((r) => String(r.id)),
1993
+ latency_ms: response.meta?.latency_ms || 0,
1994
+ degraded_mode: queryResult.degraded_mode,
1995
+ degraded_reason: queryResult.degraded_reason,
1996
+ recommendation: queryResult.recommendation
1997
+ };
1998
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
1999
+ } catch (error) {
2000
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
2001
+ }
2002
+ }
2003
+ );
2004
+ server.tool(
2005
+ "context.claim_verify",
2006
+ "Verify whether a claim is supported by retrieved context. Returns supported/partial/unsupported with evidence.",
2007
+ {
2008
+ claim: z.string().describe("Claim to verify"),
2009
+ workspace_id: z.string().optional(),
2010
+ project: z.string().optional(),
2011
+ context_ids: z.array(z.string()).optional(),
2012
+ strict: z.boolean().optional().default(true)
2013
+ },
2014
+ async ({ claim, workspace_id, project, context_ids, strict }) => {
2015
+ try {
2016
+ const workspaceId = getWorkspaceId(workspace_id);
2017
+ const resolvedProject = await resolveProjectRef(project);
2018
+ if (!resolvedProject) {
2019
+ const payload2 = {
2020
+ verdict: "unsupported",
2021
+ confidence: 0,
2022
+ evidence: [],
2023
+ missing_requirements: ["No project resolved. Set WHISPER_PROJECT or create one in your account."],
2024
+ explanation: "Verifier could not run because no project is configured."
2025
+ };
2026
+ return { content: [{ type: "text", text: JSON.stringify(payload2, null, 2) }] };
2027
+ }
2028
+ const response = await whisper.query({
2029
+ project: resolvedProject,
2030
+ query: claim,
2031
+ top_k: strict ? 8 : 12,
2032
+ include_memories: true,
2033
+ include_graph: true
2034
+ });
2035
+ const filtered = (response.results || []).filter(
2036
+ (r) => !context_ids || context_ids.length === 0 || context_ids.includes(String(r.id))
2037
+ );
2038
+ const evidence = filtered.map((r) => toEvidenceRef(r, workspaceId, "semantic"));
2039
+ const directEvidence = evidence.filter((e) => e.score >= (strict ? 0.7 : 0.6));
2040
+ const weakEvidence = evidence.filter((e) => e.score >= (strict ? 0.45 : 0.35));
2041
+ let verdict = "unsupported";
2042
+ if (directEvidence.length > 0) verdict = "supported";
2043
+ else if (weakEvidence.length > 0) verdict = "partial";
2044
+ const payload = {
2045
+ verdict,
2046
+ confidence: evidence.length ? Math.max(...evidence.map((e) => e.score)) : 0,
2047
+ evidence: verdict === "supported" ? directEvidence : weakEvidence,
2048
+ missing_requirements: verdict === "supported" ? [] : verdict === "partial" ? ["No direct evidence spans met strict threshold."] : ["No sufficient supporting evidence found for the claim."],
2049
+ 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."
2050
+ };
2051
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
2052
+ } catch (error) {
2053
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
2054
+ }
565
2055
  }
566
- async getCostSavings(params = {}) {
567
- const resolvedProject = params.project ? await this.resolveProjectId(params.project) : void 0;
568
- const query = new URLSearchParams({
569
- ...resolvedProject && { project: resolvedProject },
570
- ...params.start_date && { start_date: params.start_date },
571
- ...params.end_date && { end_date: params.end_date }
572
- });
573
- return this.request(`/v1/cost/savings?${query}`);
2056
+ );
2057
+ server.tool(
2058
+ "context.evidence_answer",
2059
+ "Answer a question only when evidence requirements are met. Fails closed with an abstain payload when not verifiable.",
2060
+ {
2061
+ question: z.string(),
2062
+ workspace_id: z.string().optional(),
2063
+ project: z.string().optional(),
2064
+ constraints: z.object({
2065
+ require_citations: z.boolean().optional().default(true),
2066
+ min_evidence_items: z.number().optional().default(2),
2067
+ min_confidence: z.number().optional().default(0.65),
2068
+ max_staleness_hours: z.number().optional().default(168)
2069
+ }).optional(),
2070
+ retrieval: z.object({
2071
+ top_k: z.number().optional().default(12),
2072
+ include_symbols: z.boolean().optional().default(true),
2073
+ include_recent_decisions: z.boolean().optional().default(true)
2074
+ }).optional()
2075
+ },
2076
+ async ({ question, workspace_id, project, constraints, retrieval }) => {
2077
+ try {
2078
+ const workspaceId = getWorkspaceId(workspace_id);
2079
+ const requireCitations = constraints?.require_citations ?? true;
2080
+ const minEvidenceItems = constraints?.min_evidence_items ?? 2;
2081
+ const minConfidence = constraints?.min_confidence ?? 0.65;
2082
+ const maxStalenessHours = constraints?.max_staleness_hours ?? 168;
2083
+ const topK = retrieval?.top_k ?? 12;
2084
+ const resolvedProject = await resolveProjectRef(project);
2085
+ if (!resolvedProject) {
2086
+ const abstain = buildAbstain({
2087
+ reason: "no_retrieval_hits",
2088
+ message: "No project resolved. Set WHISPER_PROJECT or create one in your account.",
2089
+ closest_evidence: [],
2090
+ claims_evaluated: 1,
2091
+ evidence_items_found: 0,
2092
+ min_required: minEvidenceItems,
2093
+ index_fresh: true
2094
+ });
2095
+ return { content: [{ type: "text", text: JSON.stringify(abstain, null, 2) }] };
2096
+ }
2097
+ const response = await whisper.query({
2098
+ project: resolvedProject,
2099
+ query: question,
2100
+ top_k: topK,
2101
+ include_memories: true,
2102
+ include_graph: true
2103
+ });
2104
+ const evidence = (response.results || []).map((r) => toEvidenceRef(r, workspaceId, "semantic"));
2105
+ const sorted = evidence.sort((a, b) => b.score - a.score);
2106
+ const confidence = sorted.length ? sorted[0].score : 0;
2107
+ const state = loadState();
2108
+ const workspace = getWorkspaceState(state, workspaceId);
2109
+ const lastIndexedAt = workspace.index_metadata?.last_indexed_at;
2110
+ const indexFresh = !lastIndexedAt || Date.now() - new Date(lastIndexedAt).getTime() <= maxStalenessHours * 60 * 60 * 1e3;
2111
+ if (sorted.length === 0) {
2112
+ const abstain = buildAbstain({
2113
+ reason: "no_retrieval_hits",
2114
+ message: "No retrieval hits were found for this question.",
2115
+ closest_evidence: [],
2116
+ claims_evaluated: 1,
2117
+ evidence_items_found: 0,
2118
+ min_required: minEvidenceItems,
2119
+ index_fresh: indexFresh
2120
+ });
2121
+ return { content: [{ type: "text", text: JSON.stringify(abstain, null, 2) }] };
2122
+ }
2123
+ const supportedEvidence = sorted.filter((e) => e.score >= minConfidence);
2124
+ const verdict = supportedEvidence.length >= 1 ? "supported" : "partial";
2125
+ if (!indexFresh) {
2126
+ const abstain = buildAbstain({
2127
+ reason: "stale_index",
2128
+ message: "Index freshness requirement not met. Re-index before answering.",
2129
+ closest_evidence: sorted.slice(0, 3),
2130
+ claims_evaluated: 1,
2131
+ evidence_items_found: supportedEvidence.length,
2132
+ min_required: minEvidenceItems,
2133
+ index_fresh: false
2134
+ });
2135
+ return { content: [{ type: "text", text: JSON.stringify(abstain, null, 2) }] };
2136
+ }
2137
+ if (requireCitations && (verdict !== "supported" || supportedEvidence.length < minEvidenceItems)) {
2138
+ const abstain = buildAbstain({
2139
+ reason: "insufficient_evidence",
2140
+ message: "Citation or verification thresholds were not met.",
2141
+ closest_evidence: sorted.slice(0, 3),
2142
+ claims_evaluated: 1,
2143
+ evidence_items_found: supportedEvidence.length,
2144
+ min_required: minEvidenceItems,
2145
+ index_fresh: true
2146
+ });
2147
+ return { content: [{ type: "text", text: JSON.stringify(abstain, null, 2) }] };
2148
+ }
2149
+ const citations = supportedEvidence.slice(0, Math.max(minEvidenceItems, 3));
2150
+ const answerLines = citations.map(
2151
+ (ev, idx) => `${idx + 1}. [${renderCitation(ev)}] ${ev.snippet || "Relevant context found."}`
2152
+ );
2153
+ const answered = {
2154
+ status: "answered",
2155
+ answer: answerLines.join("\n"),
2156
+ citations,
2157
+ confidence,
2158
+ verification: {
2159
+ verdict,
2160
+ supported_claims: verdict === "supported" ? 1 : 0,
2161
+ total_claims: 1
2162
+ },
2163
+ used_context_ids: citations.map((c) => c.source_id)
2164
+ };
2165
+ return { content: [{ type: "text", text: JSON.stringify(answered, null, 2) }] };
2166
+ } catch (error) {
2167
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
2168
+ }
574
2169
  }
575
- // Backward-compatible grouped namespaces.
576
- projects = {
577
- create: (params) => this.createProject(params),
578
- list: () => this.listProjects(),
579
- get: (id) => this.getProject(id),
580
- delete: (id) => this.deleteProject(id)
581
- };
582
- sources = {
583
- add: (projectId, params) => this.addSource(projectId, params),
584
- sync: (sourceId) => this.syncSource(sourceId),
585
- syncSource: (sourceId) => this.syncSource(sourceId)
586
- };
587
- memory = {
588
- add: (params) => this.addMemory(params),
589
- search: (params) => this.searchMemories(params),
590
- searchSOTA: (params) => this.searchMemoriesSOTA(params),
591
- ingestSession: (params) => this.ingestSession(params),
592
- getSessionMemories: (params) => this.getSessionMemories(params),
593
- getUserProfile: (params) => this.getUserProfile(params),
594
- getVersions: (memoryId) => this.getMemoryVersions(memoryId),
595
- update: (memoryId, params) => this.updateMemory(memoryId, params),
596
- delete: (memoryId) => this.deleteMemory(memoryId),
597
- getRelations: (memoryId) => this.getMemoryRelations(memoryId),
598
- consolidate: (params) => this.consolidateMemories(params),
599
- updateDecay: (params) => this.updateImportanceDecay(params),
600
- getImportanceStats: (project) => this.getImportanceStats(project)
601
- };
602
- keys = {
603
- create: (params) => this.createApiKey(params),
604
- list: () => this.listApiKeys(),
605
- getUsage: (days) => this.getUsage(days)
606
- };
607
- oracle = {
608
- search: (params) => this.oracleSearch(params)
609
- };
610
- context = {
611
- createShare: (params) => this.createSharedContext(params),
612
- loadShare: (shareId) => this.loadSharedContext(shareId),
613
- resumeShare: (params) => this.resumeFromSharedContext(params)
614
- };
615
- optimization = {
616
- getCacheStats: () => this.getCacheStats(),
617
- warmCache: (params) => this.warmCache(params),
618
- clearCache: (params) => this.clearCache(params),
619
- getCostSummary: (params) => this.getCostSummary(params),
620
- getCostBreakdown: (params) => this.getCostBreakdown(params),
621
- getCostSavings: (params) => this.getCostSavings(params)
622
- };
623
- };
624
-
625
- // ../src/mcp/server.ts
626
- var API_KEY = process.env.WHISPER_API_KEY || "";
627
- var DEFAULT_PROJECT = process.env.WHISPER_PROJECT || "";
628
- var BASE_URL = process.env.WHISPER_BASE_URL;
629
- if (!API_KEY) {
630
- console.error("Error: WHISPER_API_KEY environment variable is required");
631
- process.exit(1);
632
- }
633
- var whisper = new WhisperContext({
634
- apiKey: API_KEY,
635
- project: DEFAULT_PROJECT,
636
- ...BASE_URL && { baseUrl: BASE_URL }
637
- });
638
- var server = new McpServer({
639
- name: "whisper-context",
640
- version: "0.2.8"
641
- });
2170
+ );
642
2171
  server.tool(
643
- "query_context",
2172
+ "context.query",
644
2173
  "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.",
645
2174
  {
646
2175
  project: z.string().optional().describe("Project name or slug (optional if WHISPER_PROJECT is set)"),
@@ -655,31 +2184,38 @@ server.tool(
655
2184
  },
656
2185
  async ({ project, query, top_k, chunk_types, include_memories, include_graph, user_id, session_id, max_tokens }) => {
657
2186
  try {
658
- const response = await whisper.query({
659
- project,
2187
+ const resolvedProject = await resolveProjectRef(project);
2188
+ if (!resolvedProject) {
2189
+ return { content: [{ type: "text", text: "Error: No project resolved. Set WHISPER_PROJECT or pass project." }] };
2190
+ }
2191
+ const queryResult = await queryWithDegradedFallback({
2192
+ project: resolvedProject,
660
2193
  query,
661
2194
  top_k,
662
- chunk_types,
663
2195
  include_memories,
664
2196
  include_graph,
665
2197
  user_id,
666
- session_id,
667
- max_tokens
2198
+ session_id
668
2199
  });
2200
+ const response = queryResult.response;
669
2201
  if (response.results.length === 0) {
670
2202
  return { content: [{ type: "text", text: "No relevant context found." }] };
671
2203
  }
672
2204
  const header = `Found ${response.meta.total} results (${response.meta.latency_ms}ms${response.meta.cache_hit ? ", cached" : ""}):
673
2205
 
674
2206
  `;
675
- return { content: [{ type: "text", text: header + response.context }] };
2207
+ const suffix = queryResult.degraded_mode ? `
2208
+
2209
+ [degraded_mode=true] ${queryResult.degraded_reason}
2210
+ Recommendation: ${queryResult.recommendation}` : "";
2211
+ return { content: [{ type: "text", text: header + response.context + suffix }] };
676
2212
  } catch (error) {
677
2213
  return { content: [{ type: "text", text: `Error: ${error.message}` }] };
678
2214
  }
679
2215
  }
680
2216
  );
681
2217
  server.tool(
682
- "add_memory",
2218
+ "memory.add",
683
2219
  "Store a memory (fact, preference, decision) that persists across conversations. Memories can be scoped to a user, session, or agent.",
684
2220
  {
685
2221
  project: z.string().optional().describe("Project name or slug"),
@@ -708,7 +2244,7 @@ server.tool(
708
2244
  }
709
2245
  );
710
2246
  server.tool(
711
- "search_memories",
2247
+ "memory.search",
712
2248
  "Search stored memories by semantic similarity. Recall facts, preferences, past decisions from previous interactions.",
713
2249
  {
714
2250
  project: z.string().optional().describe("Project name or slug"),
@@ -740,7 +2276,7 @@ ${r.content}`).join("\n\n");
740
2276
  }
741
2277
  );
742
2278
  server.tool(
743
- "list_projects",
2279
+ "context.list_projects",
744
2280
  "List all available context projects.",
745
2281
  {},
746
2282
  async () => {
@@ -754,7 +2290,7 @@ server.tool(
754
2290
  }
755
2291
  );
756
2292
  server.tool(
757
- "list_sources",
2293
+ "context.list_sources",
758
2294
  "List all data sources connected to a project.",
759
2295
  { project: z.string().optional().describe("Project name or slug") },
760
2296
  async ({ project }) => {
@@ -769,7 +2305,85 @@ server.tool(
769
2305
  }
770
2306
  );
771
2307
  server.tool(
772
- "add_context",
2308
+ "context.add_source",
2309
+ "Add a source to a project with normalized source contract and auto-index by default.",
2310
+ {
2311
+ project: z.string().optional().describe("Project name or slug"),
2312
+ type: z.enum(["github", "web", "pdf", "local", "slack"]).default("github"),
2313
+ name: z.string().optional(),
2314
+ auto_index: z.boolean().optional().default(true),
2315
+ metadata: z.record(z.string()).optional(),
2316
+ owner: z.string().optional(),
2317
+ repo: z.string().optional(),
2318
+ branch: z.string().optional(),
2319
+ paths: z.array(z.string()).optional(),
2320
+ url: z.string().url().optional(),
2321
+ crawl_depth: z.number().optional(),
2322
+ include_paths: z.array(z.string()).optional(),
2323
+ exclude_paths: z.array(z.string()).optional(),
2324
+ file_path: z.string().optional(),
2325
+ path: z.string().optional(),
2326
+ glob: z.string().optional(),
2327
+ max_files: z.number().optional(),
2328
+ workspace_id: z.string().optional(),
2329
+ channel_ids: z.array(z.string()).optional(),
2330
+ since: z.string().optional(),
2331
+ token: z.string().optional(),
2332
+ auth_ref: z.string().optional()
2333
+ },
2334
+ async (input) => {
2335
+ try {
2336
+ const resolvedProject = await resolveProjectRef(input.project);
2337
+ if (!resolvedProject) {
2338
+ return { content: [{ type: "text", text: "Error: No project resolved. Set WHISPER_PROJECT or provide project." }] };
2339
+ }
2340
+ const result = await createSourceByType({
2341
+ project: resolvedProject,
2342
+ type: input.type,
2343
+ name: input.name,
2344
+ auto_index: input.auto_index,
2345
+ metadata: input.metadata,
2346
+ owner: input.owner,
2347
+ repo: input.repo,
2348
+ branch: input.branch,
2349
+ paths: input.paths,
2350
+ url: input.url,
2351
+ crawl_depth: input.crawl_depth,
2352
+ include_paths: input.include_paths,
2353
+ exclude_paths: input.exclude_paths,
2354
+ file_path: input.file_path,
2355
+ path: input.path,
2356
+ glob: input.glob,
2357
+ max_files: input.max_files,
2358
+ workspace_id: input.workspace_id,
2359
+ channel_ids: input.channel_ids,
2360
+ since: input.since,
2361
+ token: input.token,
2362
+ auth_ref: input.auth_ref
2363
+ });
2364
+ return toTextResult(result);
2365
+ } catch (error) {
2366
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
2367
+ }
2368
+ }
2369
+ );
2370
+ server.tool(
2371
+ "context.source_status",
2372
+ "Get status and stage/progress details for a source sync job.",
2373
+ {
2374
+ source_id: z.string().describe("Source id")
2375
+ },
2376
+ async ({ source_id }) => {
2377
+ try {
2378
+ const result = await whisper.getSourceStatus(source_id);
2379
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2380
+ } catch (error) {
2381
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
2382
+ }
2383
+ }
2384
+ );
2385
+ server.tool(
2386
+ "context.add_text",
773
2387
  "Add text content to a project's knowledge base.",
774
2388
  {
775
2389
  project: z.string().optional().describe("Project name or slug"),
@@ -790,7 +2404,56 @@ server.tool(
790
2404
  }
791
2405
  );
792
2406
  server.tool(
793
- "memory_search_sota",
2407
+ "context.add_document",
2408
+ "Ingest a document into project knowledge. Supports plain text and video URLs.",
2409
+ {
2410
+ project: z.string().optional().describe("Project name or slug"),
2411
+ source_type: z.enum(["text", "video"]).default("text"),
2412
+ title: z.string().optional().describe("Title for text documents"),
2413
+ content: z.string().optional().describe("Text document content"),
2414
+ url: z.string().url().optional().describe("Video URL when source_type=video"),
2415
+ auto_sync: z.boolean().optional().default(true),
2416
+ tags: z.array(z.string()).optional(),
2417
+ platform: z.enum(["youtube", "loom", "generic"]).optional(),
2418
+ language: z.string().optional()
2419
+ },
2420
+ async ({ project, source_type, title, content, url, auto_sync, tags, platform, language }) => {
2421
+ try {
2422
+ const resolvedProject = await resolveProjectRef(project);
2423
+ if (!resolvedProject) {
2424
+ return { content: [{ type: "text", text: "Error: No project resolved. Set WHISPER_PROJECT or provide project." }] };
2425
+ }
2426
+ if (source_type === "video") {
2427
+ if (!url) {
2428
+ return { content: [{ type: "text", text: "Error: url is required when source_type=video." }] };
2429
+ }
2430
+ const result = await whisper.addSourceByType(resolvedProject, {
2431
+ type: "video",
2432
+ url,
2433
+ auto_sync,
2434
+ tags,
2435
+ platform,
2436
+ language
2437
+ });
2438
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2439
+ }
2440
+ if (!content?.trim()) {
2441
+ return { content: [{ type: "text", text: "Error: content is required when source_type=text." }] };
2442
+ }
2443
+ await whisper.addContext({
2444
+ project: resolvedProject,
2445
+ title: title || "Document",
2446
+ content,
2447
+ metadata: { source: "mcp:add_document", tags: tags || [] }
2448
+ });
2449
+ return { content: [{ type: "text", text: `Indexed "${title || "Document"}" (${content.length} chars).` }] };
2450
+ } catch (error) {
2451
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
2452
+ }
2453
+ }
2454
+ );
2455
+ server.tool(
2456
+ "memory.search_sota",
794
2457
  "SOTA memory search with temporal reasoning and relation graphs. Searches memories with support for temporal queries ('what did I say yesterday?'), type filtering, and knowledge graph traversal.",
795
2458
  {
796
2459
  project: z.string().optional().describe("Project name or slug"),
@@ -835,7 +2498,7 @@ server.tool(
835
2498
  }
836
2499
  );
837
2500
  server.tool(
838
- "ingest_conversation",
2501
+ "memory.ingest_conversation",
839
2502
  "Extract memories from a conversation session. Automatically handles disambiguation, temporal grounding, and relation detection.",
840
2503
  {
841
2504
  project: z.string().optional().describe("Project name or slug"),
@@ -871,7 +2534,7 @@ server.tool(
871
2534
  }
872
2535
  );
873
2536
  server.tool(
874
- "oracle_search",
2537
+ "research.oracle",
875
2538
  "Oracle Research Mode - Tree-guided document navigation with multi-step reasoning. More precise than standard search, especially for bleeding-edge features.",
876
2539
  {
877
2540
  project: z.string().optional().describe("Project name or slug"),
@@ -919,7 +2582,7 @@ ${r.content.slice(0, 200)}...`
919
2582
  }
920
2583
  );
921
2584
  server.tool(
922
- "autosubscribe_dependencies",
2585
+ "index.autosubscribe_deps",
923
2586
  "Automatically index a project's dependencies (package.json, requirements.txt, etc.). Resolves docs URLs and indexes documentation.",
924
2587
  {
925
2588
  project: z.string().optional().describe("Project name or slug"),
@@ -952,7 +2615,7 @@ server.tool(
952
2615
  }
953
2616
  );
954
2617
  server.tool(
955
- "share_context",
2618
+ "context.share",
956
2619
  "Create a shareable snapshot of a conversation with memories. Returns a URL that can be shared or resumed later.",
957
2620
  {
958
2621
  project: z.string().optional().describe("Project name or slug"),
@@ -986,7 +2649,7 @@ Share URL: ${result.share_url}`
986
2649
  }
987
2650
  );
988
2651
  server.tool(
989
- "consolidate_memories",
2652
+ "memory.consolidate",
990
2653
  "Find and merge duplicate memories to reduce bloat. Uses vector similarity + LLM merging.",
991
2654
  {
992
2655
  project: z.string().optional().describe("Project name or slug"),
@@ -1029,7 +2692,7 @@ Run without dry_run to merge.`
1029
2692
  }
1030
2693
  );
1031
2694
  server.tool(
1032
- "get_cost_summary",
2695
+ "context.cost_summary",
1033
2696
  "Get cost tracking summary showing spending by model and task. Includes savings vs always-Opus.",
1034
2697
  {
1035
2698
  project: z.string().optional().describe("Project name or slug (optional for org-wide)"),
@@ -1071,6 +2734,334 @@ server.tool(
1071
2734
  }
1072
2735
  }
1073
2736
  );
2737
+ server.tool(
2738
+ "memory.forget",
2739
+ "Delete or invalidate memories with immutable audit logging.",
2740
+ {
2741
+ workspace_id: z.string().optional(),
2742
+ project: z.string().optional(),
2743
+ target: z.object({
2744
+ memory_id: z.string().optional(),
2745
+ query: z.string().optional()
2746
+ }),
2747
+ mode: z.enum(["delete", "invalidate"]).optional().default("invalidate"),
2748
+ reason: z.string().optional()
2749
+ },
2750
+ async ({ workspace_id, project, target, mode, reason }) => {
2751
+ try {
2752
+ if (!target.memory_id && !target.query) {
2753
+ return { content: [{ type: "text", text: "Error: target.memory_id or target.query is required." }] };
2754
+ }
2755
+ const affectedIds = [];
2756
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2757
+ const actor = process.env.WHISPER_AGENT_ID || process.env.USERNAME || "api_key_principal";
2758
+ const resolvedProject = await resolveProjectRef(project);
2759
+ async function applyToMemory(memoryId) {
2760
+ if (mode === "delete") {
2761
+ await whisper.deleteMemory(memoryId);
2762
+ } else {
2763
+ try {
2764
+ await whisper.updateMemory(memoryId, {
2765
+ content: `[INVALIDATED at ${now}] ${reason || "Invalidated by forget tool."}`,
2766
+ reasoning: reason || "No reason provided"
2767
+ });
2768
+ } catch {
2769
+ if (resolvedProject) {
2770
+ await whisper.addMemory({
2771
+ project: resolvedProject,
2772
+ content: `Invalidated memory ${memoryId} at ${now}. Reason: ${reason || "No reason provided"}`,
2773
+ memory_type: "instruction",
2774
+ importance: 1
2775
+ });
2776
+ }
2777
+ }
2778
+ }
2779
+ affectedIds.push(memoryId);
2780
+ }
2781
+ if (target.memory_id) {
2782
+ await applyToMemory(target.memory_id);
2783
+ } else {
2784
+ if (!resolvedProject) {
2785
+ ensureStateDir();
2786
+ const audit2 = {
2787
+ audit_id: randomUUID(),
2788
+ actor,
2789
+ called_at: now,
2790
+ mode,
2791
+ ...reason ? { reason } : {},
2792
+ affected_ids: affectedIds
2793
+ };
2794
+ appendFileSync(AUDIT_LOG_PATH, `${JSON.stringify(audit2)}
2795
+ `, "utf-8");
2796
+ const payload2 = {
2797
+ status: "completed",
2798
+ affected_ids: affectedIds,
2799
+ audit: {
2800
+ audit_id: audit2.audit_id,
2801
+ actor: audit2.actor,
2802
+ called_at: audit2.called_at,
2803
+ mode: audit2.mode,
2804
+ ...audit2.reason ? { reason: audit2.reason } : {}
2805
+ },
2806
+ warning: "No project resolved for query-based forget. Nothing was changed."
2807
+ };
2808
+ return { content: [{ type: "text", text: JSON.stringify(payload2, null, 2) }] };
2809
+ }
2810
+ const search = await whisper.searchMemoriesSOTA({
2811
+ project: resolvedProject,
2812
+ query: target.query || "",
2813
+ top_k: 25,
2814
+ include_relations: false
2815
+ });
2816
+ const memoryIds = (search.results || []).map((r) => String(r?.memory?.id || "")).filter(Boolean);
2817
+ for (const id of memoryIds) {
2818
+ await applyToMemory(id);
2819
+ }
2820
+ }
2821
+ ensureStateDir();
2822
+ const audit = {
2823
+ audit_id: randomUUID(),
2824
+ actor,
2825
+ called_at: now,
2826
+ mode,
2827
+ ...reason ? { reason } : {},
2828
+ affected_ids: affectedIds
2829
+ };
2830
+ appendFileSync(AUDIT_LOG_PATH, `${JSON.stringify(audit)}
2831
+ `, "utf-8");
2832
+ const payload = {
2833
+ status: "completed",
2834
+ affected_ids: affectedIds,
2835
+ audit: {
2836
+ audit_id: audit.audit_id,
2837
+ actor: audit.actor,
2838
+ called_at: audit.called_at,
2839
+ mode: audit.mode,
2840
+ ...audit.reason ? { reason: audit.reason } : {}
2841
+ }
2842
+ };
2843
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
2844
+ } catch (error) {
2845
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
2846
+ }
2847
+ }
2848
+ );
2849
+ server.tool(
2850
+ "context.export_bundle",
2851
+ "Export project/workspace memory and context to a portable bundle with checksum.",
2852
+ {
2853
+ workspace_id: z.string().optional(),
2854
+ project: z.string().optional()
2855
+ },
2856
+ async ({ workspace_id, project }) => {
2857
+ try {
2858
+ const workspaceId = getWorkspaceId(workspace_id);
2859
+ const state = loadState();
2860
+ const workspace = getWorkspaceState(state, workspaceId);
2861
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2862
+ const resolvedProject = await resolveProjectRef(project);
2863
+ let memories = [];
2864
+ if (resolvedProject) {
2865
+ try {
2866
+ const m = await whisper.searchMemoriesSOTA({
2867
+ project: resolvedProject,
2868
+ query: "memory",
2869
+ top_k: 100,
2870
+ include_relations: true
2871
+ });
2872
+ memories = (m.results || []).map((r) => r.memory || r);
2873
+ } catch {
2874
+ memories = [];
2875
+ }
2876
+ }
2877
+ const contents = {
2878
+ memories,
2879
+ entities: workspace.entities,
2880
+ decisions: workspace.decisions,
2881
+ failures: workspace.failures,
2882
+ annotations: workspace.annotations,
2883
+ session_summaries: workspace.session_summaries,
2884
+ documents: workspace.documents,
2885
+ index_metadata: workspace.index_metadata || {}
2886
+ };
2887
+ const bundleWithoutChecksum = {
2888
+ bundle_version: "1.0",
2889
+ workspace_id: workspaceId,
2890
+ exported_at: now,
2891
+ contents
2892
+ };
2893
+ const checksum = computeChecksum(bundleWithoutChecksum);
2894
+ const bundle = { ...bundleWithoutChecksum, checksum };
2895
+ return { content: [{ type: "text", text: JSON.stringify(bundle, null, 2) }] };
2896
+ } catch (error) {
2897
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
2898
+ }
2899
+ }
2900
+ );
2901
+ server.tool(
2902
+ "context.import_bundle",
2903
+ "Import a portable context bundle with merge/replace modes and checksum verification.",
2904
+ {
2905
+ workspace_id: z.string().optional(),
2906
+ bundle: z.object({
2907
+ bundle_version: z.string(),
2908
+ workspace_id: z.string(),
2909
+ exported_at: z.string(),
2910
+ contents: z.object({
2911
+ memories: z.array(z.any()).default([]),
2912
+ entities: z.array(z.any()).default([]),
2913
+ decisions: z.array(z.any()).default([]),
2914
+ failures: z.array(z.any()).default([]),
2915
+ annotations: z.array(z.any()).default([]),
2916
+ session_summaries: z.array(z.any()).default([]),
2917
+ documents: z.array(z.any()).default([]),
2918
+ index_metadata: z.record(z.any()).default({})
2919
+ }),
2920
+ checksum: z.string()
2921
+ }),
2922
+ mode: z.enum(["merge", "replace"]).optional().default("merge"),
2923
+ dedupe_strategy: z.enum(["semantic", "id", "none"]).optional().default("semantic")
2924
+ },
2925
+ async ({ workspace_id, bundle, mode, dedupe_strategy }) => {
2926
+ try {
2927
+ let dedupe2 = function(existing, incoming) {
2928
+ if (dedupe_strategy === "none") return false;
2929
+ if (dedupe_strategy === "id") return existing.some((e) => e.id === incoming.id);
2930
+ const norm = incoming.content.toLowerCase().trim();
2931
+ return existing.some((e) => e.content.toLowerCase().trim() === norm);
2932
+ };
2933
+ var dedupe = dedupe2;
2934
+ const workspaceId = getWorkspaceId(workspace_id || bundle.workspace_id);
2935
+ const state = loadState();
2936
+ const workspace = getWorkspaceState(state, workspaceId);
2937
+ const { checksum, ...unsignedBundle } = bundle;
2938
+ const checksumVerified = checksum === computeChecksum(unsignedBundle);
2939
+ const conflicts = [];
2940
+ if (!checksumVerified) conflicts.push("checksum_mismatch");
2941
+ const importedCounts = {
2942
+ memories: 0,
2943
+ entities: 0,
2944
+ decisions: 0,
2945
+ failures: 0,
2946
+ annotations: 0,
2947
+ session_summaries: 0,
2948
+ documents: 0
2949
+ };
2950
+ const skippedCounts = {
2951
+ memories: 0,
2952
+ entities: 0,
2953
+ decisions: 0,
2954
+ failures: 0,
2955
+ annotations: 0,
2956
+ session_summaries: 0,
2957
+ documents: 0
2958
+ };
2959
+ if (mode === "replace") {
2960
+ workspace.entities = [];
2961
+ workspace.decisions = [];
2962
+ workspace.failures = [];
2963
+ workspace.annotations = [];
2964
+ workspace.session_summaries = [];
2965
+ workspace.documents = [];
2966
+ workspace.events = [];
2967
+ }
2968
+ const keys = ["entities", "decisions", "failures", "annotations", "session_summaries", "documents"];
2969
+ for (const key of keys) {
2970
+ const sourceItems = bundle.contents[key];
2971
+ for (const incoming of sourceItems || []) {
2972
+ const normalized = {
2973
+ id: incoming.id || randomUUID(),
2974
+ content: incoming.content || "",
2975
+ created_at: incoming.created_at || (/* @__PURE__ */ new Date()).toISOString(),
2976
+ ...incoming.session_id ? { session_id: incoming.session_id } : {},
2977
+ ...incoming.commit ? { commit: incoming.commit } : {},
2978
+ ...incoming.metadata ? { metadata: incoming.metadata } : {}
2979
+ };
2980
+ const targetArray = workspace[key];
2981
+ if (dedupe2(targetArray, normalized)) {
2982
+ skippedCounts[key] += 1;
2983
+ continue;
2984
+ }
2985
+ targetArray.push(normalized);
2986
+ importedCounts[key] += 1;
2987
+ }
2988
+ }
2989
+ workspace.index_metadata = bundle.contents.index_metadata || workspace.index_metadata || {};
2990
+ saveState(state);
2991
+ const payload = {
2992
+ imported_counts: importedCounts,
2993
+ skipped_counts: skippedCounts,
2994
+ conflicts,
2995
+ checksum_verified: checksumVerified
2996
+ };
2997
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
2998
+ } catch (error) {
2999
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
3000
+ }
3001
+ }
3002
+ );
3003
+ server.tool(
3004
+ "context.diff",
3005
+ "Return deterministic context changes from an explicit anchor (session_id, timestamp, or commit).",
3006
+ {
3007
+ workspace_id: z.string().optional(),
3008
+ anchor: z.object({
3009
+ type: z.enum(["session_id", "timestamp", "commit"]),
3010
+ value: z.string()
3011
+ }).optional(),
3012
+ scope: z.object({
3013
+ include: z.array(z.enum(["decisions", "failures", "entities", "documents", "summaries"])).optional().default(["decisions", "failures", "entities", "documents", "summaries"])
3014
+ }).optional()
3015
+ },
3016
+ async ({ workspace_id, anchor, scope }) => {
3017
+ try {
3018
+ const workspaceId = getWorkspaceId(workspace_id);
3019
+ const state = loadState();
3020
+ const workspace = getWorkspaceState(state, workspaceId);
3021
+ const include = new Set(scope?.include || ["decisions", "failures", "entities", "documents", "summaries"]);
3022
+ let anchorTimestamp = null;
3023
+ let fromAnchor = anchor || null;
3024
+ if (anchor?.type === "timestamp") {
3025
+ anchorTimestamp = new Date(anchor.value).toISOString();
3026
+ } else if (anchor?.type === "session_id") {
3027
+ const summary = workspace.session_summaries.filter((s) => s.session_id === anchor.value).sort((a, b) => a.created_at.localeCompare(b.created_at)).at(-1);
3028
+ anchorTimestamp = summary?.created_at || null;
3029
+ } else if (anchor?.type === "commit") {
3030
+ const hit = workspace.events.filter((e) => e.commit === anchor.value).sort((a, b) => a.at.localeCompare(b.at)).at(-1);
3031
+ anchorTimestamp = hit?.at || null;
3032
+ } else {
3033
+ const lastSummary = workspace.session_summaries.slice().sort((a, b) => a.created_at.localeCompare(b.created_at)).at(-1);
3034
+ if (lastSummary) {
3035
+ anchorTimestamp = lastSummary.created_at;
3036
+ fromAnchor = { type: "session_id", value: lastSummary.session_id || lastSummary.id };
3037
+ }
3038
+ }
3039
+ const after = (items) => !anchorTimestamp ? items : items.filter((i) => i.created_at > anchorTimestamp);
3040
+ const changes = {
3041
+ decisions: include.has("decisions") ? after(workspace.decisions) : [],
3042
+ failures: include.has("failures") ? after(workspace.failures) : [],
3043
+ entities: include.has("entities") ? after(workspace.entities) : [],
3044
+ documents: include.has("documents") ? after(workspace.documents) : [],
3045
+ summaries: include.has("summaries") ? after(workspace.session_summaries) : []
3046
+ };
3047
+ const payload = {
3048
+ changes,
3049
+ from_anchor: fromAnchor,
3050
+ to_anchor: { type: "timestamp", value: (/* @__PURE__ */ new Date()).toISOString() },
3051
+ counts: {
3052
+ decisions: changes.decisions.length,
3053
+ failures: changes.failures.length,
3054
+ entities: changes.entities.length,
3055
+ documents: changes.documents.length,
3056
+ summaries: changes.summaries.length
3057
+ }
3058
+ };
3059
+ return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
3060
+ } catch (error) {
3061
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
3062
+ }
3063
+ }
3064
+ );
1074
3065
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next", "build", "__pycache__", ".turbo", "coverage", ".cache"]);
1075
3066
  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
3067
  function extractSignature(filePath, content) {
@@ -1101,7 +3092,7 @@ function extractSignature(filePath, content) {
1101
3092
  return signature.join("\n").slice(0, 2e3);
1102
3093
  }
1103
3094
  server.tool(
1104
- "semantic_search_codebase",
3095
+ "code.search_semantic",
1105
3096
  "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.",
1106
3097
  {
1107
3098
  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'"),
@@ -1109,7 +3100,7 @@ server.tool(
1109
3100
  file_types: z.array(z.string()).optional().describe("Limit to specific extensions e.g. ['ts', 'py']. Defaults to all common code files."),
1110
3101
  top_k: z.number().optional().default(10).describe("Number of most relevant files to return"),
1111
3102
  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.")
3103
+ max_files: z.number().optional().default(150).describe("Max files to scan. For large codebases, narrow with file_types instead of raising this.")
1113
3104
  },
1114
3105
  async ({ query, path: searchPath, file_types, top_k, threshold, max_files }) => {
1115
3106
  const rootPath = searchPath || process.cwd();
@@ -1218,7 +3209,7 @@ function* walkDir(dir, fileTypes) {
1218
3209
  }
1219
3210
  }
1220
3211
  server.tool(
1221
- "search_files",
3212
+ "code.search_text",
1222
3213
  "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.",
1223
3214
  {
1224
3215
  query: z.string().describe("What to search for \u2014 natural language keyword, function name, pattern, etc."),
@@ -1356,7 +3347,83 @@ server.tool(
1356
3347
  return { content: [{ type: "text", text: lines.join("\n") }] };
1357
3348
  }
1358
3349
  );
3350
+ server.tool(
3351
+ "code.semantic_documents",
3352
+ "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.",
3353
+ {
3354
+ query: z.string().describe("What to search for semantically (e.g. 'authentication logic', 'database connection')"),
3355
+ documents: z.array(z.object({
3356
+ id: z.string().describe("Unique identifier (file path, URL, or any ID)"),
3357
+ content: z.string().describe("The text content to search in")
3358
+ })).describe("Documents to search over"),
3359
+ top_k: z.number().optional().default(5).describe("Number of results to return"),
3360
+ threshold: z.number().optional().default(0.3).describe("Minimum similarity score 0-1")
3361
+ },
3362
+ async ({ query, documents, top_k, threshold }) => {
3363
+ try {
3364
+ const API_KEY2 = process.env.WHISPER_API_KEY;
3365
+ const BASE_URL2 = process.env.WHISPER_API_BASE_URL || "https://context.usewhisper.dev";
3366
+ const res = await fetch(`${BASE_URL2}/v1/search/semantic`, {
3367
+ method: "POST",
3368
+ headers: {
3369
+ "Authorization": `Bearer ${API_KEY2}`,
3370
+ "Content-Type": "application/json"
3371
+ },
3372
+ body: JSON.stringify({ query, documents, top_k, threshold })
3373
+ });
3374
+ const data = await res.json();
3375
+ if (data.error) {
3376
+ return { content: [{ type: "text", text: `Error: ${data.error}` }] };
3377
+ }
3378
+ if (!data.results || data.results.length === 0) {
3379
+ return { content: [{ type: "text", text: "No semantically similar results found." }] };
3380
+ }
3381
+ const lines = [`Found ${data.results.length} semantically similar results:
3382
+ `];
3383
+ for (const r of data.results) {
3384
+ lines.push(`\u{1F4C4} ${r.id} (score: ${r.score.toFixed(3)})`);
3385
+ lines.push(` ${r.content.slice(0, 200)}${r.content.length > 200 ? "..." : ""}`);
3386
+ lines.push("");
3387
+ }
3388
+ return { content: [{ type: "text", text: lines.join("\n") }] };
3389
+ } catch (error) {
3390
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
3391
+ }
3392
+ }
3393
+ );
1359
3394
  async function main() {
3395
+ const args = process.argv.slice(2);
3396
+ if (args.includes("--print-tool-map")) {
3397
+ printToolMap();
3398
+ return;
3399
+ }
3400
+ if (args[0] === "scope") {
3401
+ const readArg = (name) => {
3402
+ const idx = args.indexOf(name);
3403
+ if (idx === -1) return void 0;
3404
+ return args[idx + 1];
3405
+ };
3406
+ const project = readArg("--project") || DEFAULT_PROJECT || "my-project";
3407
+ const source = readArg("--source") || "source-or-type";
3408
+ const client = readArg("--client") || "json";
3409
+ const outPath = readArg("--write");
3410
+ const rendered = scopeConfigJson(project, source, client);
3411
+ if (outPath) {
3412
+ const backup = existsSync(outPath) ? `${outPath}.bak-${Date.now()}` : void 0;
3413
+ if (backup) writeFileSync(backup, readFileSync(outPath, "utf-8"), "utf-8");
3414
+ writeFileSync(outPath, `${rendered}
3415
+ `, "utf-8");
3416
+ console.log(JSON.stringify({ ok: true, path: outPath, backup: backup || null, client }, null, 2));
3417
+ return;
3418
+ }
3419
+ console.log(rendered);
3420
+ return;
3421
+ }
3422
+ if (!API_KEY && !IS_MANAGEMENT_ONLY) {
3423
+ console.error("Error: WHISPER_API_KEY environment variable is required");
3424
+ process.exit(1);
3425
+ }
3426
+ console.error("[whisper-context-mcp] Breaking change: canonical namespaced tool names are active. Run with --print-tool-map for migration mapping.");
1360
3427
  const transport = new StdioServerTransport();
1361
3428
  await server.connect(transport);
1362
3429
  console.error("Whisper Context MCP server running on stdio");