@usewhisper/mcp-server 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -1,801 +1,629 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- ingestDocument
4
- } from "./chunk-X7HNNNJJ.js";
5
- import {
6
- embedSingle,
7
- prisma
8
- } from "./chunk-3WGYBAYR.js";
9
- import "./chunk-QGM4M3NI.js";
10
2
 
11
3
  // ../src/mcp/server.ts
12
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
6
  import { z } from "zod";
15
7
 
16
- // ../src/engine/compressor.ts
17
- import OpenAI from "openai";
18
- import { createHash } from "crypto";
19
- var openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
20
- var deltaCache = /* @__PURE__ */ new Map();
21
- var DELTA_CACHE_TTL = 6e5;
22
- function hashContext(text) {
23
- return createHash("sha256").update(text).digest("hex").slice(0, 16);
24
- }
25
- async function compressContext(rawContext, opts = {}) {
26
- const {
27
- maxTokens = 4e3,
28
- strategy = "adaptive",
29
- previousContextHash,
30
- previousContext,
31
- targetReduction = 0.5
32
- } = opts;
33
- const originalTokens = estimateTokens(rawContext);
34
- if (originalTokens <= maxTokens) {
35
- return {
36
- context: rawContext,
37
- originalTokens,
38
- compressedTokens: originalTokens,
39
- reductionPercent: 0,
40
- strategy: "none"
41
- };
42
- }
43
- switch (strategy) {
44
- case "delta":
45
- return deltaCompress(rawContext, originalTokens, maxTokens, previousContextHash, previousContext);
46
- case "summarize":
47
- return summarizeCompress(rawContext, originalTokens, maxTokens);
48
- case "extract":
49
- return extractCompress(rawContext, originalTokens, maxTokens);
50
- case "adaptive":
51
- default:
52
- return adaptiveCompress(rawContext, originalTokens, maxTokens, previousContextHash, previousContext);
8
+ // ../src/sdk/index.ts
9
+ var WhisperError = class extends Error {
10
+ code;
11
+ status;
12
+ retryable;
13
+ details;
14
+ constructor(args) {
15
+ super(args.message);
16
+ this.name = "WhisperError";
17
+ this.code = args.code;
18
+ this.status = args.status;
19
+ this.retryable = args.retryable ?? false;
20
+ this.details = args.details;
53
21
  }
22
+ };
23
+ var DEFAULT_MAX_ATTEMPTS = 3;
24
+ var DEFAULT_BASE_DELAY_MS = 250;
25
+ var DEFAULT_MAX_DELAY_MS = 2e3;
26
+ var DEFAULT_TIMEOUT_MS = 15e3;
27
+ var PROJECT_CACHE_TTL_MS = 3e4;
28
+ function sleep(ms) {
29
+ return new Promise((resolve) => setTimeout(resolve, ms));
54
30
  }
55
- async function adaptiveCompress(rawContext, originalTokens, maxTokens, previousHash, previousCtx) {
56
- const ratio = originalTokens / maxTokens;
57
- if (previousHash || previousCtx) {
58
- const delta = await deltaCompress(rawContext, originalTokens, maxTokens, previousHash, previousCtx);
59
- if (delta.compressedTokens <= maxTokens) return delta;
60
- }
61
- if (ratio < 2) {
62
- return extractCompress(rawContext, originalTokens, maxTokens);
63
- }
64
- return summarizeCompress(rawContext, originalTokens, maxTokens);
31
+ function getBackoffDelay(attempt, base, max) {
32
+ const jitter = 0.8 + Math.random() * 0.4;
33
+ return Math.min(max, Math.floor(base * Math.pow(2, attempt) * jitter));
65
34
  }
66
- async function deltaCompress(rawContext, originalTokens, maxTokens, previousHash, previousCtx) {
67
- const currentHash = hashContext(rawContext);
68
- if (previousHash && previousHash === currentHash) {
69
- return {
70
- context: "[No changes since last context]",
71
- originalTokens,
72
- compressedTokens: 8,
73
- reductionPercent: 99,
74
- strategy: "delta-identical"
35
+ function isLikelyProjectId(projectRef) {
36
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(projectRef);
37
+ }
38
+ var WhisperContext = class _WhisperContext {
39
+ apiKey;
40
+ baseUrl;
41
+ defaultProject;
42
+ orgId;
43
+ timeoutMs;
44
+ retryConfig;
45
+ projectRefToId = /* @__PURE__ */ new Map();
46
+ projectCache = [];
47
+ projectCacheExpiresAt = 0;
48
+ constructor(config) {
49
+ if (!config.apiKey) {
50
+ throw new WhisperError({
51
+ code: "INVALID_API_KEY",
52
+ message: "API key is required"
53
+ });
54
+ }
55
+ this.apiKey = config.apiKey;
56
+ this.baseUrl = config.baseUrl || "https://context.usewhisper.dev";
57
+ this.defaultProject = config.project;
58
+ this.orgId = config.orgId;
59
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
60
+ this.retryConfig = {
61
+ maxAttempts: config.retry?.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
62
+ baseDelayMs: config.retry?.baseDelayMs ?? DEFAULT_BASE_DELAY_MS,
63
+ maxDelayMs: config.retry?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS
75
64
  };
76
65
  }
77
- let prevCtx = previousCtx;
78
- if (!prevCtx && previousHash) {
79
- const cached = deltaCache.get(previousHash);
80
- if (cached && Date.now() - cached.timestamp < DELTA_CACHE_TTL) {
81
- prevCtx = cached.context;
82
- }
66
+ withProject(project) {
67
+ return new _WhisperContext({
68
+ apiKey: this.apiKey,
69
+ baseUrl: this.baseUrl,
70
+ project,
71
+ orgId: this.orgId,
72
+ timeoutMs: this.timeoutMs,
73
+ retry: this.retryConfig
74
+ });
83
75
  }
84
- if (!prevCtx) {
85
- return extractCompress(rawContext, originalTokens, maxTokens);
76
+ getRequiredProject(project) {
77
+ const resolved = project || this.defaultProject;
78
+ if (!resolved) {
79
+ throw new WhisperError({
80
+ code: "MISSING_PROJECT",
81
+ message: "Project is required. Pass project in params or set a default project in WhisperContext config."
82
+ });
83
+ }
84
+ return resolved;
86
85
  }
87
- const prevBlocks = new Set(prevCtx.split("\n---\n").map((b) => b.trim()));
88
- const currentBlocks = rawContext.split("\n---\n").map((b) => b.trim());
89
- const newBlocks = [];
90
- const unchangedCount = { count: 0 };
91
- for (const block of currentBlocks) {
92
- if (prevBlocks.has(block)) {
93
- unchangedCount.count++;
94
- } else {
95
- newBlocks.push(block);
86
+ async refreshProjectCache(force = false) {
87
+ if (!force && Date.now() < this.projectCacheExpiresAt && this.projectCache.length > 0) {
88
+ return this.projectCache;
96
89
  }
90
+ const response = await this.request("/v1/projects", { method: "GET" });
91
+ this.projectRefToId.clear();
92
+ this.projectCache = response.projects || [];
93
+ for (const p of this.projectCache) {
94
+ this.projectRefToId.set(p.id, p.id);
95
+ this.projectRefToId.set(p.slug, p.id);
96
+ this.projectRefToId.set(p.name, p.id);
97
+ }
98
+ this.projectCacheExpiresAt = Date.now() + PROJECT_CACHE_TTL_MS;
99
+ return this.projectCache;
97
100
  }
98
- let deltaContext;
99
- if (newBlocks.length === 0) {
100
- deltaContext = "[No new information since last query]";
101
- } else {
102
- const header = `[${unchangedCount.count} unchanged results omitted, ${newBlocks.length} new/updated]
103
-
104
- `;
105
- deltaContext = header + newBlocks.join("\n---\n");
101
+ async resolveProjectId(projectRef) {
102
+ if (this.projectRefToId.has(projectRef)) {
103
+ return this.projectRefToId.get(projectRef);
104
+ }
105
+ const projects = await this.refreshProjectCache(true);
106
+ const byDirect = projects.find((p) => p.id === projectRef);
107
+ if (byDirect) return byDirect.id;
108
+ const matches = projects.filter((p) => p.slug === projectRef || p.name === projectRef);
109
+ if (matches.length === 1) {
110
+ return matches[0].id;
111
+ }
112
+ if (matches.length > 1) {
113
+ throw new WhisperError({
114
+ code: "PROJECT_AMBIGUOUS",
115
+ message: `Project reference '${projectRef}' matched multiple projects. Use project id instead.`
116
+ });
117
+ }
118
+ if (isLikelyProjectId(projectRef)) {
119
+ return projectRef;
120
+ }
121
+ throw new WhisperError({
122
+ code: "PROJECT_NOT_FOUND",
123
+ message: `Project '${projectRef}' not found`
124
+ });
106
125
  }
107
- const deltaTokens = estimateTokens(deltaContext);
108
- if (deltaTokens > maxTokens) {
109
- const truncated = truncateToTokens(deltaContext, maxTokens);
110
- deltaCache.set(currentHash, { context: rawContext, hash: currentHash, timestamp: Date.now() });
111
- return {
112
- context: truncated,
113
- originalTokens,
114
- compressedTokens: estimateTokens(truncated),
115
- reductionPercent: Math.round((1 - estimateTokens(truncated) / originalTokens) * 100),
116
- strategy: "delta-truncated"
117
- };
126
+ async getProjectRefCandidates(projectRef) {
127
+ const candidates = /* @__PURE__ */ new Set([projectRef]);
128
+ try {
129
+ const projects = await this.refreshProjectCache(false);
130
+ const match = projects.find((p) => p.id === projectRef || p.slug === projectRef || p.name === projectRef);
131
+ if (match) {
132
+ candidates.add(match.id);
133
+ candidates.add(match.slug);
134
+ candidates.add(match.name);
135
+ } else if (isLikelyProjectId(projectRef)) {
136
+ const byId = projects.find((p) => p.id === projectRef);
137
+ if (byId) {
138
+ candidates.add(byId.slug);
139
+ candidates.add(byId.name);
140
+ }
141
+ }
142
+ } catch {
143
+ }
144
+ return Array.from(candidates).filter(Boolean);
118
145
  }
119
- deltaCache.set(currentHash, { context: rawContext, hash: currentHash, timestamp: Date.now() });
120
- return {
121
- context: deltaContext,
122
- originalTokens,
123
- compressedTokens: estimateTokens(deltaContext),
124
- reductionPercent: Math.round((1 - estimateTokens(deltaContext) / originalTokens) * 100),
125
- strategy: "delta"
126
- };
127
- }
128
- async function extractCompress(rawContext, originalTokens, maxTokens) {
129
- try {
130
- const res = await openai.chat.completions.create({
131
- model: "gpt-4o-mini",
132
- // Fixed: was "gpt-4.1-nano" which doesn't exist
133
- messages: [
134
- {
135
- role: "system",
136
- content: `You are a context compressor. Extract and preserve ONLY the most important information from the provided context. Remove redundancy, boilerplate, and low-value content. Keep code snippets, key facts, API signatures, and important relationships. Output should be ${maxTokens} tokens or less. Do NOT add commentary \u2014 just output the compressed context.`
137
- },
138
- { role: "user", content: rawContext }
139
- ],
140
- max_tokens: maxTokens,
141
- temperature: 0
146
+ async withProjectRefFallback(projectRef, execute) {
147
+ const refs = await this.getProjectRefCandidates(projectRef);
148
+ let lastError;
149
+ for (const ref of refs) {
150
+ try {
151
+ return await execute(ref);
152
+ } catch (error) {
153
+ lastError = error;
154
+ if (error instanceof WhisperError && error.code === "PROJECT_NOT_FOUND") {
155
+ continue;
156
+ }
157
+ throw error;
158
+ }
159
+ }
160
+ if (lastError instanceof Error) {
161
+ throw lastError;
162
+ }
163
+ throw new WhisperError({
164
+ code: "PROJECT_NOT_FOUND",
165
+ message: `Project '${projectRef}' not found`
142
166
  });
143
- const compressed = res.choices[0]?.message?.content?.trim() || rawContext;
144
- const compressedTokens = estimateTokens(compressed);
145
- return {
146
- context: compressed,
147
- originalTokens,
148
- compressedTokens,
149
- reductionPercent: Math.round((1 - compressedTokens / originalTokens) * 100),
150
- strategy: "extract"
151
- };
152
- } catch {
153
- const truncated = truncateToTokens(rawContext, maxTokens);
154
- return {
155
- context: truncated,
156
- originalTokens,
157
- compressedTokens: estimateTokens(truncated),
158
- reductionPercent: Math.round((1 - estimateTokens(truncated) / originalTokens) * 100),
159
- strategy: "truncate-fallback"
160
- };
161
167
  }
162
- }
163
- async function summarizeCompress(rawContext, originalTokens, maxTokens) {
164
- const blocks = rawContext.split("\n---\n").filter((b) => b.trim());
165
- if (blocks.length <= 3) {
166
- return extractCompress(rawContext, originalTokens, maxTokens);
168
+ classifyError(status, message) {
169
+ if (status === 401 || /api key|unauthorized|forbidden/i.test(message)) {
170
+ return { code: "INVALID_API_KEY", retryable: false };
171
+ }
172
+ if (status === 404 || /project not found/i.test(message)) {
173
+ return { code: "PROJECT_NOT_FOUND", retryable: false };
174
+ }
175
+ if (status === 408) {
176
+ return { code: "TIMEOUT", retryable: true };
177
+ }
178
+ if (status === 429) {
179
+ return { code: "RATE_LIMITED", retryable: true };
180
+ }
181
+ if (status !== void 0 && status >= 500) {
182
+ return { code: "TEMPORARY_UNAVAILABLE", retryable: true };
183
+ }
184
+ return { code: "REQUEST_FAILED", retryable: false };
167
185
  }
168
- const budgetPerBlock = Math.floor(maxTokens / blocks.length);
169
- try {
170
- const summaries = await Promise.all(
171
- blocks.map(async (block) => {
172
- if (estimateTokens(block) <= budgetPerBlock) return block;
173
- const res = await openai.chat.completions.create({
174
- model: "gpt-4o-mini",
175
- // Fixed: was "gpt-4.1-nano" which doesn't exist
176
- messages: [
177
- {
178
- role: "system",
179
- content: `Summarize this context block in ${budgetPerBlock} tokens or less. Preserve code signatures, key facts, and important details. Output only the summary.`
180
- },
181
- { role: "user", content: block }
182
- ],
183
- max_tokens: budgetPerBlock,
184
- temperature: 0
186
+ async request(endpoint, options = {}) {
187
+ const maxAttempts = Math.max(1, this.retryConfig.maxAttempts);
188
+ let lastError;
189
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
190
+ const controller = new AbortController();
191
+ const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
192
+ try {
193
+ const response = await fetch(`${this.baseUrl}${endpoint}`, {
194
+ ...options,
195
+ signal: controller.signal,
196
+ headers: {
197
+ Authorization: `Bearer ${this.apiKey}`,
198
+ "Content-Type": "application/json",
199
+ ...this.orgId ? { "X-Whisper-Org-Id": this.orgId } : {},
200
+ ...options.headers
201
+ }
185
202
  });
186
- return res.choices[0]?.message?.content?.trim() || block.slice(0, budgetPerBlock * 4);
187
- })
188
- );
189
- const compressed = summaries.join("\n\n---\n\n");
190
- let compressedTokens = estimateTokens(compressed);
191
- let finalContext = compressed;
192
- if (compressedTokens > maxTokens) {
193
- finalContext = truncateToTokens(compressed, maxTokens);
194
- compressedTokens = estimateTokens(finalContext);
203
+ clearTimeout(timeout);
204
+ if (!response.ok) {
205
+ let payload = null;
206
+ try {
207
+ payload = await response.json();
208
+ } catch {
209
+ payload = await response.text().catch(() => "");
210
+ }
211
+ const message = typeof payload === "string" ? payload : payload?.error || payload?.message || `HTTP ${response.status}: ${response.statusText}`;
212
+ const { code, retryable } = this.classifyError(response.status, message);
213
+ const err = new WhisperError({
214
+ code,
215
+ message,
216
+ status: response.status,
217
+ retryable,
218
+ details: payload
219
+ });
220
+ if (!retryable || attempt === maxAttempts - 1) {
221
+ throw err;
222
+ }
223
+ await sleep(getBackoffDelay(attempt, this.retryConfig.baseDelayMs, this.retryConfig.maxDelayMs));
224
+ continue;
225
+ }
226
+ return response.json();
227
+ } catch (error) {
228
+ clearTimeout(timeout);
229
+ const isAbort = error?.name === "AbortError";
230
+ const mapped = error instanceof WhisperError ? error : new WhisperError({
231
+ code: isAbort ? "TIMEOUT" : "NETWORK_ERROR",
232
+ message: isAbort ? "Request timed out" : error?.message || "Network request failed",
233
+ retryable: true,
234
+ details: error
235
+ });
236
+ lastError = mapped;
237
+ if (!mapped.retryable || attempt === maxAttempts - 1) {
238
+ throw mapped;
239
+ }
240
+ await sleep(getBackoffDelay(attempt, this.retryConfig.baseDelayMs, this.retryConfig.maxDelayMs));
241
+ }
195
242
  }
196
- return {
197
- context: finalContext,
198
- originalTokens,
199
- compressedTokens,
200
- reductionPercent: Math.round((1 - compressedTokens / originalTokens) * 100),
201
- strategy: "summarize"
202
- };
203
- } catch {
204
- const truncated = truncateToTokens(rawContext, maxTokens);
205
- return {
206
- context: truncated,
207
- originalTokens,
208
- compressedTokens: estimateTokens(truncated),
209
- reductionPercent: Math.round((1 - estimateTokens(truncated) / originalTokens) * 100),
210
- strategy: "truncate-fallback"
211
- };
243
+ throw lastError instanceof Error ? lastError : new WhisperError({ code: "REQUEST_FAILED", message: "Request failed" });
212
244
  }
213
- }
214
- function estimateTokens(text) {
215
- return Math.ceil(text.length / 4);
216
- }
217
- function truncateToTokens(text, maxTokens) {
218
- const maxChars = maxTokens * 4;
219
- if (text.length <= maxChars) return text;
220
- return text.slice(0, maxChars) + "\n\n[...truncated]";
221
- }
222
- var cacheCleanupInterval = null;
223
- function startCacheCleanup() {
224
- if (cacheCleanupInterval) return;
225
- cacheCleanupInterval = setInterval(() => {
226
- const now = Date.now();
227
- for (const [key, val] of deltaCache) {
228
- if (now - val.timestamp > DELTA_CACHE_TTL) deltaCache.delete(key);
245
+ async query(params) {
246
+ const projectRef = this.getRequiredProject(params.project);
247
+ return this.withProjectRefFallback(projectRef, (project) => this.request("/v1/context/query", {
248
+ method: "POST",
249
+ body: JSON.stringify({ ...params, project })
250
+ }));
251
+ }
252
+ async createProject(params) {
253
+ const project = await this.request("/v1/projects", {
254
+ method: "POST",
255
+ body: JSON.stringify(params)
256
+ });
257
+ this.projectRefToId.set(project.id, project.id);
258
+ this.projectRefToId.set(project.slug, project.id);
259
+ this.projectRefToId.set(project.name, project.id);
260
+ this.projectCache = [
261
+ ...this.projectCache.filter((p) => p.id !== project.id),
262
+ project
263
+ ];
264
+ this.projectCacheExpiresAt = Date.now() + PROJECT_CACHE_TTL_MS;
265
+ return project;
266
+ }
267
+ async listProjects() {
268
+ const projects = await this.request("/v1/projects", { method: "GET" });
269
+ this.projectCache = projects.projects || [];
270
+ for (const p of projects.projects || []) {
271
+ this.projectRefToId.set(p.id, p.id);
272
+ this.projectRefToId.set(p.slug, p.id);
273
+ this.projectRefToId.set(p.name, p.id);
229
274
  }
230
- }, 6e4);
231
- }
232
- function stopCacheCleanup() {
233
- if (cacheCleanupInterval) {
234
- clearInterval(cacheCleanupInterval);
235
- cacheCleanupInterval = null;
275
+ this.projectCacheExpiresAt = Date.now() + PROJECT_CACHE_TTL_MS;
276
+ return projects;
236
277
  }
237
- }
238
- startCacheCleanup();
239
- process.on("SIGTERM", stopCacheCleanup);
240
- process.on("SIGINT", stopCacheCleanup);
241
- process.on("beforeExit", stopCacheCleanup);
242
-
243
- // ../src/engine/retriever.ts
244
- import { createHash as createHash2 } from "crypto";
245
- import OpenAI2 from "openai";
246
- var openai2 = new OpenAI2({ apiKey: process.env.OPENAI_API_KEY });
247
- async function retrieve(opts) {
248
- const {
249
- projectId,
250
- query,
251
- topK = 10,
252
- threshold = 0.3,
253
- chunkTypes,
254
- hybridSearch = true,
255
- vectorWeight = 0.7,
256
- bm25Weight = 0.3,
257
- rerank = true,
258
- rerankTopK,
259
- includeMemories = false,
260
- userId,
261
- sessionId,
262
- agentId,
263
- includeGraph = false,
264
- graphDepth = 1,
265
- maxTokens,
266
- compress = false,
267
- compressionStrategy = "adaptive",
268
- previousContextHash,
269
- useCache = true,
270
- cacheTtlSeconds = 300
271
- } = opts;
272
- const startTime = Date.now();
273
- if (useCache) {
274
- const cached = await checkCache(projectId, query);
275
- if (cached) {
276
- const cachedResults = cached;
277
- return {
278
- results: cachedResults,
279
- context: packContext(cachedResults, maxTokens),
280
- meta: {
281
- totalResults: cachedResults.length,
282
- latencyMs: Date.now() - startTime,
283
- cacheHit: true,
284
- tokensUsed: 0
278
+ async getProject(id) {
279
+ const projectId = await this.resolveProjectId(id);
280
+ return this.request(`/v1/projects/${projectId}`);
281
+ }
282
+ async deleteProject(id) {
283
+ const projectId = await this.resolveProjectId(id);
284
+ return this.request(`/v1/projects/${projectId}`, { method: "DELETE" });
285
+ }
286
+ async addSource(projectId, params) {
287
+ const resolvedProjectId = await this.resolveProjectId(projectId);
288
+ return this.request(`/v1/projects/${resolvedProjectId}/sources`, {
289
+ method: "POST",
290
+ body: JSON.stringify(params)
291
+ });
292
+ }
293
+ async syncSource(sourceId) {
294
+ return this.request(`/v1/sources/${sourceId}/sync`, { method: "POST" });
295
+ }
296
+ async ingest(projectId, documents) {
297
+ const resolvedProjectId = await this.resolveProjectId(projectId);
298
+ return this.request(`/v1/projects/${resolvedProjectId}/ingest`, {
299
+ method: "POST",
300
+ body: JSON.stringify({ documents })
301
+ });
302
+ }
303
+ async addContext(params) {
304
+ const projectId = await this.resolveProjectId(this.getRequiredProject(params.project));
305
+ return this.ingest(projectId, [
306
+ {
307
+ title: params.title || "Context",
308
+ content: params.content,
309
+ metadata: params.metadata || { source: "addContext" }
310
+ }
311
+ ]);
312
+ }
313
+ async addMemory(params) {
314
+ const projectRef = this.getRequiredProject(params.project);
315
+ return this.withProjectRefFallback(projectRef, async (project) => {
316
+ const toSotaType = (memoryType) => {
317
+ switch (memoryType) {
318
+ case "episodic":
319
+ return "event";
320
+ case "semantic":
321
+ return "factual";
322
+ case "procedural":
323
+ return "instruction";
324
+ default:
325
+ return memoryType;
285
326
  }
286
327
  };
287
- }
328
+ const toLegacyType = (memoryType) => {
329
+ switch (memoryType) {
330
+ case "event":
331
+ return "episodic";
332
+ case "instruction":
333
+ return "procedural";
334
+ case "preference":
335
+ case "relationship":
336
+ case "opinion":
337
+ case "goal":
338
+ return "semantic";
339
+ default:
340
+ return memoryType;
341
+ }
342
+ };
343
+ try {
344
+ const direct = await this.request("/v1/memory", {
345
+ method: "POST",
346
+ body: JSON.stringify({
347
+ project,
348
+ content: params.content,
349
+ memory_type: toSotaType(params.memory_type),
350
+ user_id: params.user_id,
351
+ session_id: params.session_id,
352
+ agent_id: params.agent_id,
353
+ importance: params.importance,
354
+ metadata: params.metadata
355
+ })
356
+ });
357
+ const id2 = direct?.memory?.id || direct?.id || direct?.memory_id;
358
+ if (id2) {
359
+ return { id: id2, success: true, path: "sota", fallback_used: false };
360
+ }
361
+ } catch (error) {
362
+ if (params.allow_legacy_fallback === false) {
363
+ throw error;
364
+ }
365
+ }
366
+ const legacy = await this.request("/v1/memories", {
367
+ method: "POST",
368
+ body: JSON.stringify({
369
+ project,
370
+ content: params.content,
371
+ memory_type: toLegacyType(params.memory_type),
372
+ user_id: params.user_id,
373
+ session_id: params.session_id,
374
+ agent_id: params.agent_id,
375
+ importance: params.importance,
376
+ metadata: params.metadata,
377
+ expires_in_seconds: params.expires_in_seconds
378
+ })
379
+ });
380
+ const id = legacy?.memory?.id || legacy?.id || legacy?.memory_id;
381
+ if (!id) {
382
+ throw new WhisperError({
383
+ code: "REQUEST_FAILED",
384
+ message: "Memory create succeeded but no memory id was returned by the API"
385
+ });
386
+ }
387
+ return { id, success: true, path: "legacy", fallback_used: true };
388
+ });
389
+ }
390
+ async searchMemories(params) {
391
+ const projectRef = this.getRequiredProject(params.project);
392
+ return this.withProjectRefFallback(projectRef, (project) => this.request("/v1/memory/search", {
393
+ method: "POST",
394
+ body: JSON.stringify({
395
+ query: params.query,
396
+ project,
397
+ user_id: params.user_id,
398
+ session_id: params.session_id,
399
+ memory_types: params.memory_type ? [params.memory_type] : void 0,
400
+ top_k: params.top_k || 10
401
+ })
402
+ }));
403
+ }
404
+ async createApiKey(params) {
405
+ return this.request("/v1/keys", {
406
+ method: "POST",
407
+ body: JSON.stringify(params)
408
+ });
409
+ }
410
+ async listApiKeys() {
411
+ return this.request("/v1/keys");
412
+ }
413
+ async getUsage(days = 30) {
414
+ return this.request(`/v1/usage?days=${days}`);
288
415
  }
289
- const queryEmbedding = await embedSingle(query);
290
- let allResults = [];
291
- const maxResultsPerSearch = Math.min(topK * 2, 50);
292
- const vectorResults = await vectorSearch(projectId, queryEmbedding, maxResultsPerSearch, chunkTypes);
293
- allResults.push(...vectorResults);
294
- if (hybridSearch) {
295
- const bm25Results = await fullTextSearch(projectId, query, maxResultsPerSearch, chunkTypes);
296
- allResults.push(...bm25Results);
416
+ async searchMemoriesSOTA(params) {
417
+ const projectRef = this.getRequiredProject(params.project);
418
+ return this.withProjectRefFallback(projectRef, (project) => this.request("/v1/memory/search", {
419
+ method: "POST",
420
+ body: JSON.stringify({ ...params, project })
421
+ }));
297
422
  }
298
- if (includeMemories) {
299
- const memoryResults = await memorySearch(projectId, queryEmbedding, {
300
- userId,
301
- sessionId,
302
- agentId,
303
- topK: Math.min(Math.ceil(topK / 3), 10)
423
+ async ingestSession(params) {
424
+ const projectRef = this.getRequiredProject(params.project);
425
+ return this.withProjectRefFallback(projectRef, (project) => this.request("/v1/memory/ingest/session", {
426
+ method: "POST",
427
+ body: JSON.stringify({ ...params, project })
428
+ }));
429
+ }
430
+ async getSessionMemories(params) {
431
+ const project = await this.resolveProjectId(this.getRequiredProject(params.project));
432
+ const query = new URLSearchParams({
433
+ project,
434
+ ...params.limit && { limit: params.limit.toString() },
435
+ ...params.since_date && { since_date: params.since_date }
304
436
  });
305
- allResults.push(...memoryResults);
437
+ return this.request(`/v1/memory/session/${params.session_id}?${query}`);
306
438
  }
307
- if (includeGraph) {
308
- const graphResults = await graphSearch(projectId, queryEmbedding, {
309
- depth: Math.min(graphDepth, 2),
310
- topK: Math.min(Math.ceil(topK / 3), 10)
439
+ async getUserProfile(params) {
440
+ const project = await this.resolveProjectId(this.getRequiredProject(params.project));
441
+ const query = new URLSearchParams({
442
+ project,
443
+ ...params.memory_types && { memory_types: params.memory_types }
311
444
  });
312
- allResults.push(...graphResults);
445
+ return this.request(`/v1/memory/profile/${params.user_id}?${query}`);
446
+ }
447
+ async getMemoryVersions(memoryId) {
448
+ return this.request(`/v1/memory/${memoryId}/versions`);
313
449
  }
314
- if (allResults.length > topK * 4) {
315
- allResults = allResults.sort((a, b) => b.score - a.score).slice(0, topK * 4);
450
+ async updateMemory(memoryId, params) {
451
+ return this.request(`/v1/memory/${memoryId}`, {
452
+ method: "PUT",
453
+ body: JSON.stringify(params)
454
+ });
316
455
  }
317
- allResults = deduplicateResults(allResults);
318
- if (hybridSearch) {
319
- allResults = reciprocalRankFusion(allResults, vectorWeight, bm25Weight);
456
+ async deleteMemory(memoryId) {
457
+ return this.request(`/v1/memory/${memoryId}`, { method: "DELETE" });
320
458
  }
321
- allResults = allResults.filter((r) => r.score >= threshold);
322
- if (rerank && allResults.length > 0) {
323
- const reranked = await rerankResults(query, allResults, rerankTopK || topK);
324
- allResults = reranked;
459
+ async getMemoryRelations(memoryId) {
460
+ return this.request(`/v1/memory/${memoryId}/relations`);
325
461
  }
326
- allResults = allResults.slice(0, topK);
327
- allResults = await enrichResults(allResults);
328
- let context = packContext(allResults, maxTokens);
329
- const contextHash = createHash2("sha256").update(context).digest("hex").slice(0, 16);
330
- let compressionMeta;
331
- if (compress && context.length > 0) {
332
- const compressed = await compressContext(context, {
333
- maxTokens: maxTokens || 4e3,
334
- strategy: compressionStrategy,
335
- previousContextHash
462
+ async oracleSearch(params) {
463
+ const project = await this.resolveProjectId(this.getRequiredProject(params.project));
464
+ return this.request("/v1/oracle/search", {
465
+ method: "POST",
466
+ body: JSON.stringify({ ...params, project })
336
467
  });
337
- context = compressed.context;
338
- compressionMeta = {
339
- originalTokens: compressed.originalTokens,
340
- compressedTokens: compressed.compressedTokens,
341
- reductionPercent: compressed.reductionPercent,
342
- strategy: compressed.strategy
343
- };
344
468
  }
345
- if (useCache && allResults.length > 0) {
346
- await setCache(projectId, query, allResults, cacheTtlSeconds);
469
+ async autosubscribe(params) {
470
+ const project = await this.resolveProjectId(this.getRequiredProject(params.project));
471
+ return this.request("/v1/autosubscribe", {
472
+ method: "POST",
473
+ body: JSON.stringify({ ...params, project })
474
+ });
347
475
  }
348
- const latencyMs = Date.now() - startTime;
349
- return {
350
- results: allResults,
351
- context,
352
- meta: {
353
- totalResults: allResults.length,
354
- latencyMs,
355
- cacheHit: false,
356
- tokensUsed: estimateTokens2(context),
357
- contextHash,
358
- compression: compressionMeta
359
- }
360
- };
361
- }
362
- async function vectorSearch(projectId, queryEmbedding, limit, chunkTypes) {
363
- const embeddingStr = `[${queryEmbedding.join(",")}]`;
364
- const results = chunkTypes && chunkTypes.length > 0 ? await prisma.$queryRaw`
365
- SELECT
366
- id, content, "chunkType", metadata,
367
- 1 - (embedding <=> ${embeddingStr}::vector) as similarity
368
- FROM chunks
369
- WHERE "projectId" = ${projectId}
370
- AND "chunkType" = ANY(${chunkTypes})
371
- ORDER BY embedding <=> ${embeddingStr}::vector
372
- LIMIT ${limit}
373
- ` : await prisma.$queryRaw`
374
- SELECT
375
- id, content, "chunkType", metadata,
376
- 1 - (embedding <=> ${embeddingStr}::vector) as similarity
377
- FROM chunks
378
- WHERE "projectId" = ${projectId}
379
- ORDER BY embedding <=> ${embeddingStr}::vector
380
- LIMIT ${limit}
381
- `;
382
- return results.map((r) => ({
383
- id: r.id,
384
- content: r.content,
385
- score: r.similarity,
386
- metadata: r.metadata || {},
387
- chunkType: r.chunkType,
388
- source: "vector"
389
- }));
390
- }
391
- async function fullTextSearch(projectId, query, limit, chunkTypes) {
392
- const tsQuery = query.replace(/[^\w\s]/g, " ").trim().split(/\s+/).filter((w) => w.length > 1).join(" & ");
393
- if (!tsQuery) return [];
394
- const results = chunkTypes && chunkTypes.length > 0 ? await prisma.$queryRaw`
395
- SELECT
396
- id, content, "chunkType", metadata,
397
- ts_rank(to_tsvector('english', coalesce("searchContent", content)), to_tsquery('english', ${tsQuery})) as rank
398
- FROM chunks
399
- WHERE "projectId" = ${projectId}
400
- AND "chunkType" = ANY(${chunkTypes})
401
- AND to_tsvector('english', coalesce("searchContent", content)) @@ to_tsquery('english', ${tsQuery})
402
- ORDER BY rank DESC
403
- LIMIT ${limit}
404
- ` : await prisma.$queryRaw`
405
- SELECT
406
- id, content, "chunkType", metadata,
407
- ts_rank(to_tsvector('english', coalesce("searchContent", content)), to_tsquery('english', ${tsQuery})) as rank
408
- FROM chunks
409
- WHERE "projectId" = ${projectId}
410
- AND to_tsvector('english', coalesce("searchContent", content)) @@ to_tsquery('english', ${tsQuery})
411
- ORDER BY rank DESC
412
- LIMIT ${limit}
413
- `;
414
- const maxRank = results.length > 0 ? Math.max(...results.map((r) => r.rank)) : 1;
415
- return results.map((r) => ({
416
- id: r.id,
417
- content: r.content,
418
- score: maxRank > 0 ? r.rank / maxRank : 0,
419
- metadata: r.metadata || {},
420
- chunkType: r.chunkType,
421
- source: "bm25"
422
- }));
423
- }
424
- async function memorySearch(projectId, queryEmbedding, opts) {
425
- const embeddingStr = `[${queryEmbedding.join(",")}]`;
426
- let query;
427
- if (opts.userId && opts.sessionId && opts.agentId) {
428
- query = prisma.$queryRaw`
429
- SELECT
430
- id, content, memory_type as "memoryType", metadata, importance,
431
- 1 - (embedding <=> ${embeddingStr}::vector) as similarity
432
- FROM memories
433
- WHERE project_id = ${projectId}
434
- AND is_active = true
435
- AND (expires_at IS NULL OR expires_at > NOW())
436
- AND user_id = ${opts.userId}
437
- AND session_id = ${opts.sessionId}
438
- AND agent_id = ${opts.agentId}
439
- ORDER BY embedding <=> ${embeddingStr}::vector
440
- LIMIT ${opts.topK}
441
- `;
442
- } else if (opts.userId && opts.sessionId) {
443
- query = prisma.$queryRaw`
444
- SELECT
445
- id, content, memory_type as "memoryType", metadata, importance,
446
- 1 - (embedding <=> ${embeddingStr}::vector) as similarity
447
- FROM memories
448
- WHERE project_id = ${projectId}
449
- AND is_active = true
450
- AND (expires_at IS NULL OR expires_at > NOW())
451
- AND user_id = ${opts.userId}
452
- AND session_id = ${opts.sessionId}
453
- ORDER BY embedding <=> ${embeddingStr}::vector
454
- LIMIT ${opts.topK}
455
- `;
456
- } else if (opts.userId) {
457
- query = prisma.$queryRaw`
458
- SELECT
459
- id, content, memory_type as "memoryType", metadata, importance,
460
- 1 - (embedding <=> ${embeddingStr}::vector) as similarity
461
- FROM memories
462
- WHERE project_id = ${projectId}
463
- AND is_active = true
464
- AND (expires_at IS NULL OR expires_at > NOW())
465
- AND user_id = ${opts.userId}
466
- ORDER BY embedding <=> ${embeddingStr}::vector
467
- LIMIT ${opts.topK}
468
- `;
469
- } else {
470
- query = prisma.$queryRaw`
471
- SELECT
472
- id, content, memory_type as "memoryType", metadata, importance,
473
- 1 - (embedding <=> ${embeddingStr}::vector) as similarity
474
- FROM memories
475
- WHERE project_id = ${projectId}
476
- AND is_active = true
477
- AND (expires_at IS NULL OR expires_at > NOW())
478
- ORDER BY embedding <=> ${embeddingStr}::vector
479
- LIMIT ${opts.topK}
480
- `;
476
+ async createSharedContext(params) {
477
+ const project = await this.resolveProjectId(this.getRequiredProject(params.project));
478
+ return this.request("/v1/context/share", {
479
+ method: "POST",
480
+ body: JSON.stringify({ ...params, project })
481
+ });
481
482
  }
482
- const results = await query;
483
- const ids = results.map((r) => r.id);
484
- if (ids.length > 0) {
485
- await prisma.memory.updateMany({
486
- where: { id: { in: ids } },
487
- data: {
488
- accessCount: { increment: 1 },
489
- lastAccessedAt: /* @__PURE__ */ new Date()
490
- }
483
+ async loadSharedContext(shareId) {
484
+ return this.request(`/v1/context/shared/${shareId}`);
485
+ }
486
+ async resumeFromSharedContext(params) {
487
+ const project = await this.resolveProjectId(this.getRequiredProject(params.project));
488
+ return this.request("/v1/context/resume", {
489
+ method: "POST",
490
+ body: JSON.stringify({ ...params, project })
491
491
  });
492
492
  }
493
- return results.map((r) => ({
494
- id: r.id,
495
- content: r.content,
496
- score: r.similarity * (r.importance || 0.5),
497
- metadata: { ...r.metadata || {}, memoryType: r.memoryType },
498
- chunkType: "memory",
499
- source: "memory"
500
- }));
501
- }
502
- async function graphSearch(projectId, queryEmbedding, opts) {
503
- const embeddingStr = `[${queryEmbedding.join(",")}]`;
504
- const relevantEntities = await prisma.$queryRaw`
505
- SELECT
506
- id, name, entity_type as "entityType", description, metadata, source_chunk_id as "sourceChunkId",
507
- 1 - (embedding <=> ${embeddingStr}::vector) as similarity
508
- FROM entities
509
- WHERE project_id = ${projectId}
510
- ORDER BY embedding <=> ${embeddingStr}::vector
511
- LIMIT 5
512
- `;
513
- if (relevantEntities.length === 0) return [];
514
- const entityIds = relevantEntities.map((e) => e.id);
515
- const relatedEntities = await prisma.$queryRaw`
516
- SELECT
517
- e.id, e.name, e.entity_type as "entityType", e.description, e.metadata, e.source_chunk_id as "sourceChunkId",
518
- er.relation_type as "relationType", er.weight
519
- FROM entity_relations er
520
- INNER JOIN entities e ON er.to_entity_id = e.id
521
- WHERE er.project_id = ${projectId}
522
- AND er.from_entity_id = ANY(${entityIds})
523
- LIMIT ${opts.topK}
524
- `;
525
- const chunkIds = [
526
- ...relevantEntities.map((e) => e.sourceChunkId).filter(Boolean),
527
- ...relatedEntities.map((e) => e.sourceChunkId).filter(Boolean)
528
- ];
529
- if (chunkIds.length === 0) return [];
530
- const relatedChunks = await prisma.chunk.findMany({
531
- where: { id: { in: chunkIds } },
532
- take: opts.topK
533
- });
534
- return relatedChunks.map((c) => {
535
- const entity = relevantEntities.find((e) => e.sourceChunkId === c.id);
536
- return {
537
- id: c.id,
538
- content: c.content,
539
- score: entity ? entity.similarity * 0.8 : 0.5,
540
- metadata: {
541
- ...c.metadata,
542
- entityName: entity?.name,
543
- entityType: entity?.entityType
544
- },
545
- chunkType: c.chunkType || "text",
546
- source: "graph"
547
- };
548
- });
549
- }
550
- function reciprocalRankFusion(results, vectorWeight, bm25Weight, k = 60) {
551
- const scoreMap = /* @__PURE__ */ new Map();
552
- const vectorResults = results.filter((r) => r.source === "vector");
553
- const bm25Results = results.filter((r) => r.source === "bm25");
554
- const otherResults = results.filter((r) => r.source !== "vector" && r.source !== "bm25");
555
- vectorResults.forEach((r, rank) => {
556
- const existing = scoreMap.get(r.id);
557
- const rrfScore = vectorWeight / (k + rank + 1);
558
- if (existing) {
559
- existing.score += rrfScore;
560
- } else {
561
- scoreMap.set(r.id, { result: r, score: rrfScore });
562
- }
563
- });
564
- bm25Results.forEach((r, rank) => {
565
- const existing = scoreMap.get(r.id);
566
- const rrfScore = bm25Weight / (k + rank + 1);
567
- if (existing) {
568
- existing.score += rrfScore;
569
- existing.result.source = "hybrid";
570
- } else {
571
- scoreMap.set(r.id, { result: { ...r, source: "hybrid" }, score: rrfScore });
572
- }
573
- });
574
- otherResults.forEach((r) => {
575
- if (!scoreMap.has(r.id)) {
576
- scoreMap.set(r.id, { result: r, score: r.score * 0.5 });
577
- }
578
- });
579
- return Array.from(scoreMap.values()).sort((a, b) => b.score - a.score).map((entry) => ({ ...entry.result, score: entry.score }));
580
- }
581
- async function rerankResults(query, results, topK) {
582
- if (results.length <= 3) return results;
583
- const candidates = results.slice(0, Math.min(results.length, topK * 3));
584
- const prompt = `Given the query: "${query}"
585
-
586
- Rank these ${candidates.length} text passages by relevance (most relevant first). Return ONLY a JSON array of indices (0-based), e.g. [2, 0, 4, 1, 3].
587
-
588
- ${candidates.map((r, i) => `[${i}] ${r.content.slice(0, 300)}`).join("\n\n")}`;
589
- try {
590
- const res = await openai2.chat.completions.create({
591
- model: "gpt-4o-mini",
592
- // Fixed: was "gpt-4.1-nano" which doesn't exist
593
- messages: [{ role: "user", content: prompt }],
594
- temperature: 0,
595
- max_tokens: 200
493
+ async consolidateMemories(params) {
494
+ const project = await this.resolveProjectId(this.getRequiredProject(params.project));
495
+ return this.request("/v1/memory/consolidate", {
496
+ method: "POST",
497
+ body: JSON.stringify({ ...params, project })
596
498
  });
597
- const text = res.choices[0]?.message?.content?.trim() || "";
598
- const match = text.match(/\[[\d,\s]+\]/);
599
- if (!match) return results;
600
- const indices = JSON.parse(match[0]);
601
- const reranked = [];
602
- for (const idx of indices) {
603
- if (idx >= 0 && idx < candidates.length) {
604
- reranked.push({
605
- ...candidates[idx],
606
- score: 1 - reranked.length * (1 / indices.length)
607
- // normalize
608
- });
609
- }
610
- }
611
- for (const r of results) {
612
- if (!reranked.find((rr) => rr.id === r.id)) {
613
- reranked.push(r);
614
- }
615
- }
616
- return reranked.slice(0, topK);
617
- } catch {
618
- return results.slice(0, topK);
619
499
  }
620
- }
621
- function deduplicateResults(results) {
622
- const seen = /* @__PURE__ */ new Map();
623
- for (const r of results) {
624
- const existing = seen.get(r.id);
625
- if (!existing || r.score > existing.score) {
626
- seen.set(r.id, r);
627
- }
500
+ async updateImportanceDecay(params) {
501
+ const project = await this.resolveProjectId(this.getRequiredProject(params.project));
502
+ return this.request("/v1/memory/decay/update", {
503
+ method: "POST",
504
+ body: JSON.stringify({ ...params, project })
505
+ });
628
506
  }
629
- return Array.from(seen.values());
630
- }
631
- function packContext(results, maxTokens) {
632
- if (results.length === 0) return "";
633
- const limit = maxTokens || 8e3;
634
- let totalTokens = 0;
635
- const packed = [];
636
- for (const r of results) {
637
- const header = buildChunkHeader(r);
638
- const block = `${header}
639
- ${r.content}
640
- `;
641
- const tokens = estimateTokens2(block);
642
- if (totalTokens + tokens > limit) break;
643
- packed.push(block);
644
- totalTokens += tokens;
507
+ async getImportanceStats(project) {
508
+ const resolvedProject = await this.resolveProjectId(this.getRequiredProject(project));
509
+ return this.request(`/v1/memory/decay/stats?project=${resolvedProject}`);
645
510
  }
646
- return packed.join("\n---\n\n");
647
- }
648
- function buildChunkHeader(r) {
649
- const parts = [];
650
- if (r.sourceName) parts.push(`Source: ${r.sourceName}`);
651
- if (r.documentTitle) parts.push(`Document: ${r.documentTitle}`);
652
- if (r.metadata?.filePath) parts.push(`File: ${r.metadata.filePath}`);
653
- if (r.metadata?.startLine) parts.push(`Lines: ${r.metadata.startLine}-${r.metadata.endLine || "?"}`);
654
- if (r.chunkType && r.chunkType !== "text") parts.push(`Type: ${r.chunkType}`);
655
- return parts.length > 0 ? `[${parts.join(" | ")}]` : "";
656
- }
657
- function estimateTokens2(text) {
658
- return Math.ceil(text.length / 4);
659
- }
660
- async function enrichResults(results) {
661
- const chunkResults = results.filter((r) => r.source !== "memory");
662
- if (chunkResults.length === 0) return results;
663
- const chunkIds = chunkResults.map((r) => r.id);
664
- if (chunkIds.length === 0) return results;
665
- const chunkDocs = await prisma.$queryRaw`
666
- SELECT
667
- c.id as "chunkId", d.title as "docTitle", s.name as "sourceName"
668
- FROM chunks c
669
- INNER JOIN documents d ON c."documentId" = d.id
670
- INNER JOIN sources s ON d."sourceId" = s.id
671
- WHERE c.id = ANY(${chunkIds})
672
- `;
673
- const enrichMap = new Map(chunkDocs.map((d) => [d.chunkId, d]));
674
- return results.map((r) => {
675
- const enrichment = enrichMap.get(r.id);
676
- if (enrichment) {
677
- return {
678
- ...r,
679
- documentTitle: enrichment.docTitle || void 0,
680
- sourceName: enrichment.sourceName || void 0
681
- };
682
- }
683
- return r;
684
- });
685
- }
686
- var MAX_CACHE_PER_PROJECT = 500;
687
- var lastCacheCleanup = 0;
688
- var CACHE_CLEANUP_INTERVAL = 6e4;
689
- function hashQuery(query) {
690
- return createHash2("sha256").update(query.toLowerCase().trim()).digest("hex");
691
- }
692
- async function cleanupQueryCache(projectId) {
693
- const now = Date.now();
694
- if (now - lastCacheCleanup < CACHE_CLEANUP_INTERVAL) return;
695
- lastCacheCleanup = now;
696
- try {
697
- const count = await prisma.queryCache.count({ where: { projectId } });
698
- if (count > MAX_CACHE_PER_PROJECT * 1.5) {
699
- await prisma.queryCache.deleteMany({
700
- where: {
701
- projectId,
702
- expiresAt: { lt: /* @__PURE__ */ new Date() }
703
- }
704
- });
705
- const remaining = await prisma.queryCache.count({ where: { projectId } });
706
- if (remaining > MAX_CACHE_PER_PROJECT) {
707
- const oldest = await prisma.queryCache.findFirst({
708
- where: { projectId },
709
- orderBy: { createdAt: "asc" },
710
- skip: MAX_CACHE_PER_PROJECT - 1
711
- });
712
- if (oldest) {
713
- await prisma.queryCache.deleteMany({
714
- where: {
715
- projectId,
716
- createdAt: { lte: oldest.createdAt },
717
- id: { notIn: (await prisma.queryCache.findMany({
718
- where: { projectId },
719
- take: MAX_CACHE_PER_PROJECT,
720
- orderBy: { createdAt: "desc" },
721
- select: { id: true }
722
- })).map((x) => x.id) }
723
- }
724
- });
725
- }
726
- }
727
- }
728
- } catch (error) {
729
- console.error("Cache cleanup error:", error);
511
+ async getCacheStats() {
512
+ return this.request("/v1/cache/stats");
730
513
  }
731
- }
732
- async function checkCache(projectId, query) {
733
- const hash = hashQuery(query);
734
- const cached = await prisma.queryCache.findFirst({
735
- where: {
736
- projectId,
737
- queryHash: hash,
738
- expiresAt: { gt: /* @__PURE__ */ new Date() }
739
- }
740
- });
741
- if (cached) {
742
- await prisma.queryCache.update({
743
- where: { id: cached.id },
744
- data: { hitCount: { increment: 1 } }
514
+ async warmCache(params) {
515
+ const project = await this.resolveProjectId(this.getRequiredProject(params.project));
516
+ return this.request("/v1/cache/warm", {
517
+ method: "POST",
518
+ body: JSON.stringify({ ...params, project })
745
519
  });
746
- return cached.results;
747
520
  }
748
- return null;
749
- }
750
- async function setCache(projectId, query, results, ttlSeconds) {
751
- const hash = hashQuery(query);
752
- const expiresAt = new Date(Date.now() + ttlSeconds * 1e3);
753
- await prisma.queryCache.upsert({
754
- where: {
755
- projectId_queryHash: {
756
- projectId,
757
- queryHash: hash
758
- }
759
- },
760
- update: {
761
- results,
762
- expiresAt,
763
- hitCount: 0
764
- },
765
- create: {
766
- projectId,
767
- queryHash: hash,
768
- query,
769
- results,
770
- expiresAt
771
- }
772
- });
773
- await cleanupQueryCache(projectId);
774
- }
521
+ async clearCache(params) {
522
+ return this.request("/v1/cache/clear", {
523
+ method: "DELETE",
524
+ body: JSON.stringify(params)
525
+ });
526
+ }
527
+ async getCostSummary(params = {}) {
528
+ const resolvedProject = params.project ? await this.resolveProjectId(params.project) : void 0;
529
+ const query = new URLSearchParams({
530
+ ...resolvedProject && { project: resolvedProject },
531
+ ...params.start_date && { start_date: params.start_date },
532
+ ...params.end_date && { end_date: params.end_date }
533
+ });
534
+ return this.request(`/v1/cost/summary?${query}`);
535
+ }
536
+ async getCostBreakdown(params = {}) {
537
+ const resolvedProject = params.project ? await this.resolveProjectId(params.project) : void 0;
538
+ const query = new URLSearchParams({
539
+ ...resolvedProject && { project: resolvedProject },
540
+ ...params.group_by && { group_by: params.group_by },
541
+ ...params.start_date && { start_date: params.start_date },
542
+ ...params.end_date && { end_date: params.end_date }
543
+ });
544
+ return this.request(`/v1/cost/breakdown?${query}`);
545
+ }
546
+ async getCostSavings(params = {}) {
547
+ const resolvedProject = params.project ? await this.resolveProjectId(params.project) : void 0;
548
+ const query = new URLSearchParams({
549
+ ...resolvedProject && { project: resolvedProject },
550
+ ...params.start_date && { start_date: params.start_date },
551
+ ...params.end_date && { end_date: params.end_date }
552
+ });
553
+ return this.request(`/v1/cost/savings?${query}`);
554
+ }
555
+ // Backward-compatible grouped namespaces.
556
+ projects = {
557
+ create: (params) => this.createProject(params),
558
+ list: () => this.listProjects(),
559
+ get: (id) => this.getProject(id),
560
+ delete: (id) => this.deleteProject(id)
561
+ };
562
+ sources = {
563
+ add: (projectId, params) => this.addSource(projectId, params),
564
+ sync: (sourceId) => this.syncSource(sourceId),
565
+ syncSource: (sourceId) => this.syncSource(sourceId)
566
+ };
567
+ memory = {
568
+ add: (params) => this.addMemory(params),
569
+ search: (params) => this.searchMemories(params),
570
+ searchSOTA: (params) => this.searchMemoriesSOTA(params),
571
+ ingestSession: (params) => this.ingestSession(params),
572
+ getSessionMemories: (params) => this.getSessionMemories(params),
573
+ getUserProfile: (params) => this.getUserProfile(params),
574
+ getVersions: (memoryId) => this.getMemoryVersions(memoryId),
575
+ update: (memoryId, params) => this.updateMemory(memoryId, params),
576
+ delete: (memoryId) => this.deleteMemory(memoryId),
577
+ getRelations: (memoryId) => this.getMemoryRelations(memoryId),
578
+ consolidate: (params) => this.consolidateMemories(params),
579
+ updateDecay: (params) => this.updateImportanceDecay(params),
580
+ getImportanceStats: (project) => this.getImportanceStats(project)
581
+ };
582
+ keys = {
583
+ create: (params) => this.createApiKey(params),
584
+ list: () => this.listApiKeys(),
585
+ getUsage: (days) => this.getUsage(days)
586
+ };
587
+ oracle = {
588
+ search: (params) => this.oracleSearch(params)
589
+ };
590
+ context = {
591
+ createShare: (params) => this.createSharedContext(params),
592
+ loadShare: (shareId) => this.loadSharedContext(shareId),
593
+ resumeShare: (params) => this.resumeFromSharedContext(params)
594
+ };
595
+ optimization = {
596
+ getCacheStats: () => this.getCacheStats(),
597
+ warmCache: (params) => this.warmCache(params),
598
+ clearCache: (params) => this.clearCache(params),
599
+ getCostSummary: (params) => this.getCostSummary(params),
600
+ getCostBreakdown: (params) => this.getCostBreakdown(params),
601
+ getCostSavings: (params) => this.getCostSavings(params)
602
+ };
603
+ };
775
604
 
776
605
  // ../src/mcp/server.ts
777
- var ORG_ID = process.env.WHISPER_ORG_ID || "";
606
+ var API_KEY = process.env.WHISPER_API_KEY || "";
607
+ var DEFAULT_PROJECT = process.env.WHISPER_PROJECT || "";
608
+ var BASE_URL = process.env.WHISPER_BASE_URL;
609
+ if (!API_KEY) {
610
+ console.error("Error: WHISPER_API_KEY environment variable is required");
611
+ process.exit(1);
612
+ }
613
+ var whisper = new WhisperContext({
614
+ apiKey: API_KEY,
615
+ project: DEFAULT_PROJECT,
616
+ ...BASE_URL && { baseUrl: BASE_URL }
617
+ });
778
618
  var server = new McpServer({
779
619
  name: "whisper-context",
780
- version: "0.1.0"
620
+ version: "0.2.8"
781
621
  });
782
- async function resolveProject(name) {
783
- const proj = await prisma.project.findFirst({
784
- where: {
785
- orgId: ORG_ID,
786
- OR: [
787
- { name },
788
- { slug: name }
789
- ]
790
- }
791
- });
792
- return proj;
793
- }
794
622
  server.tool(
795
623
  "query_context",
796
624
  "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.",
797
625
  {
798
- project: z.string().describe("Project name or slug"),
626
+ project: z.string().optional().describe("Project name or slug (optional if WHISPER_PROJECT is set)"),
799
627
  query: z.string().describe("What are you looking for?"),
800
628
  top_k: z.number().optional().default(10).describe("Number of results"),
801
629
  chunk_types: z.array(z.string()).optional().describe("Filter: code, function, class, documentation, api_spec, schema, config, text"),
@@ -806,93 +634,89 @@ server.tool(
806
634
  max_tokens: z.number().optional().describe("Max tokens for packed context")
807
635
  },
808
636
  async ({ project, query, top_k, chunk_types, include_memories, include_graph, user_id, session_id, max_tokens }) => {
809
- const proj = await resolveProject(project);
810
- if (!proj) return { content: [{ type: "text", text: `Project '${project}' not found.` }] };
811
- const response = await retrieve({
812
- projectId: proj.id,
813
- query,
814
- topK: top_k,
815
- chunkTypes: chunk_types,
816
- includeMemories: include_memories,
817
- includeGraph: include_graph,
818
- userId: user_id,
819
- sessionId: session_id,
820
- maxTokens: max_tokens
821
- });
822
- if (response.results.length === 0) {
823
- return { content: [{ type: "text", text: "No relevant context found." }] };
824
- }
825
- const header = `Found ${response.meta.totalResults} results (${response.meta.latencyMs}ms${response.meta.cacheHit ? ", cached" : ""}):
637
+ try {
638
+ const response = await whisper.query({
639
+ project,
640
+ query,
641
+ top_k,
642
+ chunk_types,
643
+ include_memories,
644
+ include_graph,
645
+ user_id,
646
+ session_id,
647
+ max_tokens
648
+ });
649
+ if (response.results.length === 0) {
650
+ return { content: [{ type: "text", text: "No relevant context found." }] };
651
+ }
652
+ const header = `Found ${response.meta.total} results (${response.meta.latency_ms}ms${response.meta.cache_hit ? ", cached" : ""}):
826
653
 
827
654
  `;
828
- return { content: [{ type: "text", text: header + response.context }] };
655
+ return { content: [{ type: "text", text: header + response.context }] };
656
+ } catch (error) {
657
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
658
+ }
829
659
  }
830
660
  );
831
661
  server.tool(
832
662
  "add_memory",
833
663
  "Store a memory (fact, preference, decision) that persists across conversations. Memories can be scoped to a user, session, or agent.",
834
664
  {
835
- project: z.string().describe("Project name or slug"),
665
+ project: z.string().optional().describe("Project name or slug"),
836
666
  content: z.string().describe("The memory content to store"),
837
- memory_type: z.enum(["factual", "episodic", "semantic", "procedural"]).optional().default("factual"),
667
+ memory_type: z.enum(["factual", "preference", "event", "relationship", "opinion", "goal", "instruction"]).optional().default("factual"),
838
668
  user_id: z.string().optional().describe("User this memory belongs to"),
839
669
  session_id: z.string().optional().describe("Session scope"),
840
670
  agent_id: z.string().optional().describe("Agent scope"),
841
671
  importance: z.number().optional().default(0.5).describe("Importance 0-1")
842
672
  },
843
673
  async ({ project, content, memory_type, user_id, session_id, agent_id, importance }) => {
844
- const proj = await resolveProject(project);
845
- if (!proj) return { content: [{ type: "text", text: `Project '${project}' not found.` }] };
846
- const embedding = await embedSingle(content);
847
- const memory = await prisma.memory.create({
848
- data: {
849
- projectId: proj.id,
674
+ try {
675
+ const result = await whisper.addMemory({
676
+ project,
850
677
  content,
851
- memoryType: memory_type,
852
- userId: user_id,
853
- sessionId: session_id,
854
- agentId: agent_id,
855
- importance,
856
- embedding
857
- }
858
- });
859
- return { content: [{ type: "text", text: `Memory stored (id: ${memory.id}, type: ${memory_type}).` }] };
678
+ memory_type,
679
+ user_id,
680
+ session_id,
681
+ agent_id,
682
+ importance
683
+ });
684
+ return { content: [{ type: "text", text: `Memory stored (id: ${result.id}, type: ${memory_type}).` }] };
685
+ } catch (error) {
686
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
687
+ }
860
688
  }
861
689
  );
862
690
  server.tool(
863
691
  "search_memories",
864
692
  "Search stored memories by semantic similarity. Recall facts, preferences, past decisions from previous interactions.",
865
693
  {
866
- project: z.string().describe("Project name or slug"),
694
+ project: z.string().optional().describe("Project name or slug"),
867
695
  query: z.string().describe("What to search for"),
868
696
  user_id: z.string().optional().describe("Filter by user"),
869
697
  session_id: z.string().optional().describe("Filter by session"),
870
- top_k: z.number().optional().default(10).describe("Number of results")
698
+ top_k: z.number().optional().default(10).describe("Number of results"),
699
+ memory_types: z.array(z.enum(["factual", "preference", "event", "relationship", "opinion", "goal", "instruction"])).optional()
871
700
  },
872
- async ({ project, query, user_id, session_id, top_k }) => {
873
- const proj = await resolveProject(project);
874
- if (!proj) return { content: [{ type: "text", text: `Project '${project}' not found.` }] };
875
- const queryEmbedding = await embedSingle(query);
876
- let whereClause = `
877
- project_id = '${proj.id}'
878
- AND is_active = true
879
- AND (expires_at IS NULL OR expires_at > NOW())
880
- `;
881
- if (user_id) whereClause += ` AND user_id = '${user_id}'`;
882
- if (session_id) whereClause += ` AND session_id = '${session_id}'`;
883
- const results = await prisma.$queryRawUnsafe(`
884
- SELECT
885
- id, content, memory_type as "memoryType", importance,
886
- 1 - (embedding <=> '${JSON.stringify(queryEmbedding)}'::vector) as similarity
887
- FROM memories
888
- WHERE ${whereClause}
889
- ORDER BY embedding <=> '${JSON.stringify(queryEmbedding)}'::vector
890
- LIMIT ${top_k}
891
- `);
892
- if (results.length === 0) return { content: [{ type: "text", text: "No memories found." }] };
893
- const text = results.map((r, i) => `${i + 1}. [${r.memoryType}, importance: ${r.importance}, score: ${r.similarity.toFixed(3)}]
701
+ async ({ project, query, user_id, session_id, top_k, memory_types }) => {
702
+ try {
703
+ const results = await whisper.searchMemoriesSOTA({
704
+ project,
705
+ query,
706
+ user_id,
707
+ session_id,
708
+ top_k,
709
+ memory_types
710
+ });
711
+ if (!results.memories || results.memories.length === 0) {
712
+ return { content: [{ type: "text", text: "No memories found." }] };
713
+ }
714
+ const text = results.memories.map((r, i) => `${i + 1}. [${r.memory_type}, score: ${r.similarity?.toFixed(3) || "N/A"}]
894
715
  ${r.content}`).join("\n\n");
895
- return { content: [{ type: "text", text }] };
716
+ return { content: [{ type: "text", text }] };
717
+ } catch (error) {
718
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
719
+ }
896
720
  }
897
721
  );
898
722
  server.tool(
@@ -900,139 +724,56 @@ server.tool(
900
724
  "List all available context projects.",
901
725
  {},
902
726
  async () => {
903
- const projs = await prisma.project.findMany({
904
- where: { orgId: ORG_ID }
905
- });
906
- const text = projs.length === 0 ? "No projects found." : projs.map((p) => `- ${p.name} (${p.slug})${p.description ? `: ${p.description}` : ""}`).join("\n");
907
- return { content: [{ type: "text", text }] };
727
+ try {
728
+ const { projects } = await whisper.listProjects();
729
+ const text = projects.length === 0 ? "No projects found." : projects.map((p) => `- ${p.name} (${p.slug})${p.description ? `: ${p.description}` : ""}`).join("\n");
730
+ return { content: [{ type: "text", text }] };
731
+ } catch (error) {
732
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
733
+ }
908
734
  }
909
735
  );
910
736
  server.tool(
911
737
  "list_sources",
912
738
  "List all data sources connected to a project.",
913
- { project: z.string().describe("Project name or slug") },
739
+ { project: z.string().optional().describe("Project name or slug") },
914
740
  async ({ project }) => {
915
- const proj = await resolveProject(project);
916
- if (!proj) return { content: [{ type: "text", text: `Project '${project}' not found.` }] };
917
- const srcs = await prisma.source.findMany({
918
- where: { projectId: proj.id }
919
- });
920
- const text = srcs.length === 0 ? "No sources connected." : srcs.map((s) => `- ${s.name} (${s.connectorType}) \u2014 ${s.status} | ${s.documentCount} docs, ${s.chunkCount} chunks`).join("\n");
921
- return { content: [{ type: "text", text }] };
741
+ try {
742
+ const projectData = await whisper.getProject(project || DEFAULT_PROJECT);
743
+ const srcs = projectData.sources || [];
744
+ const text = srcs.length === 0 ? "No sources connected." : srcs.map((s) => `- ${s.name} (${s.connectorType}) \u2014 ${s.status}`).join("\n");
745
+ return { content: [{ type: "text", text }] };
746
+ } catch (error) {
747
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
748
+ }
922
749
  }
923
750
  );
924
751
  server.tool(
925
752
  "add_context",
926
753
  "Add text content to a project's knowledge base.",
927
754
  {
928
- project: z.string().describe("Project name or slug"),
755
+ project: z.string().optional().describe("Project name or slug"),
929
756
  title: z.string().describe("Title for this content"),
930
757
  content: z.string().describe("The text content to index")
931
758
  },
932
759
  async ({ project, title, content }) => {
933
- const proj = await resolveProject(project);
934
- if (!proj) return { content: [{ type: "text", text: `Project '${project}' not found.` }] };
935
- let directSource = await prisma.source.findFirst({
936
- where: {
937
- projectId: proj.id,
938
- connectorType: "custom",
939
- name: "mcp-ingest"
940
- }
941
- });
942
- if (!directSource) {
943
- directSource = await prisma.source.create({
944
- data: {
945
- orgId: ORG_ID,
946
- projectId: proj.id,
947
- name: "mcp-ingest",
948
- type: "custom",
949
- connectorType: "custom",
950
- config: {},
951
- status: "READY"
952
- }
953
- });
954
- }
955
- await ingestDocument({ sourceId: directSource.id, projectId: proj.id, externalId: `mcp-${title}`, title, content });
956
- return { content: [{ type: "text", text: `Indexed "${title}" (${content.length} chars) into '${project}'.` }] };
957
- }
958
- );
959
- server.tool(
960
- "track_conversation",
961
- "Add a message to a conversation. Creates the conversation if it doesn't exist.",
962
- {
963
- project: z.string().describe("Project name or slug"),
964
- session_id: z.string().describe("Unique session identifier"),
965
- role: z.enum(["user", "assistant", "system", "tool"]),
966
- content: z.string().describe("Message content"),
967
- user_id: z.string().optional().describe("User identifier")
968
- },
969
- async ({ project, session_id, role, content, user_id }) => {
970
- const proj = await resolveProject(project);
971
- if (!proj) return { content: [{ type: "text", text: `Project '${project}' not found.` }] };
972
- let conv = await prisma.session.findFirst({
973
- where: {
974
- projectId: proj.id,
975
- sessionId: session_id
976
- }
977
- });
978
- if (!conv) {
979
- conv = await prisma.session.create({
980
- data: {
981
- projectId: proj.id,
982
- sessionId: session_id,
983
- userId: user_id
984
- }
760
+ try {
761
+ await whisper.addContext({
762
+ project,
763
+ title,
764
+ content
985
765
  });
766
+ return { content: [{ type: "text", text: `Indexed "${title}" (${content.length} chars).` }] };
767
+ } catch (error) {
768
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
986
769
  }
987
- await prisma.message.create({
988
- data: {
989
- conversationId: conv.id,
990
- role,
991
- content
992
- }
993
- });
994
- await prisma.session.update({
995
- where: { id: conv.id },
996
- data: {
997
- messageCount: { increment: 1 },
998
- updatedAt: /* @__PURE__ */ new Date()
999
- }
1000
- });
1001
- return { content: [{ type: "text", text: `Message added (session: ${session_id}).` }] };
1002
- }
1003
- );
1004
- server.tool(
1005
- "get_conversation",
1006
- "Retrieve conversation history for a session.",
1007
- {
1008
- project: z.string().describe("Project name or slug"),
1009
- session_id: z.string().describe("Session identifier"),
1010
- limit: z.number().optional().default(50)
1011
- },
1012
- async ({ project, session_id, limit }) => {
1013
- const proj = await resolveProject(project);
1014
- if (!proj) return { content: [{ type: "text", text: `Project '${project}' not found.` }] };
1015
- const conv = await prisma.session.findFirst({
1016
- where: {
1017
- projectId: proj.id,
1018
- sessionId: session_id
1019
- }
1020
- });
1021
- if (!conv) return { content: [{ type: "text", text: "No conversation found for this session." }] };
1022
- const msgs = await prisma.message.findMany({
1023
- where: { conversationId: conv.id },
1024
- orderBy: { createdAt: "asc" },
1025
- take: limit
1026
- });
1027
- const text = msgs.map((m) => `[${m.role}]: ${m.content}`).join("\n\n");
1028
- return { content: [{ type: "text", text: text || "No messages yet." }] };
1029
770
  }
1030
771
  );
1031
772
  server.tool(
1032
773
  "memory_search_sota",
1033
774
  "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.",
1034
775
  {
1035
- project: z.string().describe("Project name or slug"),
776
+ project: z.string().optional().describe("Project name or slug"),
1036
777
  query: z.string().describe("Search query (supports temporal: 'yesterday', 'last week')"),
1037
778
  user_id: z.string().optional().describe("Filter by user"),
1038
779
  session_id: z.string().optional().describe("Filter by session"),
@@ -1042,43 +783,42 @@ server.tool(
1042
783
  include_relations: z.boolean().optional().default(true).describe("Include related memories via knowledge graph")
1043
784
  },
1044
785
  async ({ project, query, user_id, session_id, question_date, memory_types, top_k, include_relations }) => {
1045
- const proj = await resolveProject(project);
1046
- if (!proj) return { content: [{ type: "text", text: `Project '${project}' not found.` }] };
1047
- const { searchMemories } = await import("./search-BLVHWLWC.js");
1048
- const results = await searchMemories({
1049
- query,
1050
- questionDate: question_date ? new Date(question_date) : /* @__PURE__ */ new Date(),
1051
- projectId: proj.id,
1052
- orgId: ORG_ID,
1053
- userId: user_id,
1054
- sessionId: session_id,
1055
- topK: top_k,
1056
- memoryTypes: memory_types
1057
- });
1058
- if (results.length === 0) return { content: [{ type: "text", text: "No memories found." }] };
1059
- const text = results.map((r, i) => {
1060
- let line = `${i + 1}. [${r.memory.memoryType}, confidence: ${r.memory.confidence.toFixed(2)}, score: ${r.similarity.toFixed(3)}]
1061
- `;
1062
- line += ` ${r.memory.content}
786
+ try {
787
+ const results = await whisper.searchMemoriesSOTA({
788
+ project,
789
+ query,
790
+ user_id,
791
+ session_id,
792
+ question_date,
793
+ memory_types,
794
+ top_k,
795
+ include_relations
796
+ });
797
+ if (!results.memories || results.memories.length === 0) {
798
+ return { content: [{ type: "text", text: "No memories found." }] };
799
+ }
800
+ const text = results.memories.map((r, i) => {
801
+ let line = `${i + 1}. [${r.memory_type}, score: ${r.similarity?.toFixed(3) || "N/A"}]
1063
802
  `;
1064
- if (r.memory.temporal.eventDate) {
1065
- line += ` Event: ${r.memory.temporal.eventDate.toISOString().split("T")[0]}
803
+ line += ` ${r.content}
1066
804
  `;
1067
- }
1068
- if (include_relations && r.relations && r.relations.length > 0) {
1069
- line += ` Relations: ${r.relations.map((rel) => rel.relationType).join(", ")}
805
+ if (r.event_date) {
806
+ line += ` Event: ${new Date(r.event_date).toISOString().split("T")[0]}
1070
807
  `;
1071
- }
1072
- return line;
1073
- }).join("\n");
1074
- return { content: [{ type: "text", text }] };
808
+ }
809
+ return line;
810
+ }).join("\n");
811
+ return { content: [{ type: "text", text }] };
812
+ } catch (error) {
813
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
814
+ }
1075
815
  }
1076
816
  );
1077
817
  server.tool(
1078
818
  "ingest_conversation",
1079
819
  "Extract memories from a conversation session. Automatically handles disambiguation, temporal grounding, and relation detection.",
1080
820
  {
1081
- project: z.string().describe("Project name or slug"),
821
+ project: z.string().optional().describe("Project name or slug"),
1082
822
  session_id: z.string().describe("Session identifier"),
1083
823
  user_id: z.string().optional().describe("User identifier"),
1084
824
  messages: z.array(z.object({
@@ -1088,75 +828,73 @@ server.tool(
1088
828
  })).describe("Array of conversation messages with timestamps")
1089
829
  },
1090
830
  async ({ project, session_id, user_id, messages }) => {
1091
- const proj = await resolveProject(project);
1092
- if (!proj) return { content: [{ type: "text", text: `Project '${project}' not found.` }] };
1093
- const { ingestSession } = await import("./ingest-2LPTWUUM.js");
1094
- const result = await ingestSession({
1095
- sessionId: session_id,
1096
- projectId: proj.id,
1097
- orgId: ORG_ID,
1098
- userId: user_id,
1099
- messages: messages.map((m) => ({
1100
- role: m.role,
1101
- content: m.content,
1102
- timestamp: new Date(m.timestamp)
1103
- }))
1104
- });
1105
- return {
1106
- content: [{
1107
- type: "text",
1108
- text: `Processed ${messages.length} messages:
1109
- - Created ${result.memoriesCreated} memories
1110
- - Detected ${result.relationsCreated} relations
1111
- - Updated ${result.memoriesInvalidated} outdated memories` + (result.errors && result.errors.length > 0 ? `
831
+ try {
832
+ const result = await whisper.ingestSession({
833
+ project,
834
+ session_id,
835
+ user_id,
836
+ messages
837
+ });
838
+ return {
839
+ content: [{
840
+ type: "text",
841
+ text: `Processed ${messages.length} messages:
842
+ - Created ${result.memories_created} memories
843
+ - Detected ${result.relations_created} relations
844
+ - Updated ${result.memories_invalidated} outdated memories` + (result.errors && result.errors.length > 0 ? `
1112
845
  - Errors: ${result.errors.join(", ")}` : "")
1113
- }]
1114
- };
846
+ }]
847
+ };
848
+ } catch (error) {
849
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
850
+ }
1115
851
  }
1116
852
  );
1117
853
  server.tool(
1118
854
  "oracle_search",
1119
855
  "Oracle Research Mode - Tree-guided document navigation with multi-step reasoning. More precise than standard search, especially for bleeding-edge features.",
1120
856
  {
1121
- project: z.string().describe("Project name or slug"),
857
+ project: z.string().optional().describe("Project name or slug"),
1122
858
  query: z.string().describe("Research question"),
1123
859
  mode: z.enum(["search", "research"]).optional().default("search").describe("'search' for tree-guided, 'research' for multi-step reasoning"),
1124
860
  max_results: z.number().optional().default(5),
1125
861
  max_steps: z.number().optional().default(5).describe("For research mode: max reasoning steps")
1126
862
  },
1127
863
  async ({ project, query, mode, max_results, max_steps }) => {
1128
- const proj = await resolveProject(project);
1129
- if (!proj) return { content: [{ type: "text", text: `Project '${project}' not found.` }] };
1130
- const { oracleSearch, oracleResearch } = await import("./oracle-J47QCSEW.js");
1131
- if (mode === "research") {
1132
- const result = await oracleResearch({
1133
- question: query,
1134
- projectId: proj.id,
1135
- maxSteps: max_steps
864
+ try {
865
+ const results = await whisper.oracleSearch({
866
+ project,
867
+ query,
868
+ mode,
869
+ max_results,
870
+ max_steps
1136
871
  });
1137
- let text = `Answer: ${result.answer}
872
+ if (mode === "research" && results.answer) {
873
+ let text = `Answer: ${results.answer}
1138
874
 
1139
875
  Reasoning Steps:
1140
876
  `;
1141
- result.steps.forEach((step, i) => {
1142
- text += `${i + 1}. Query: ${step.query}
877
+ if (results.steps) {
878
+ results.steps.forEach((step, i) => {
879
+ text += `${i + 1}. Query: ${step.query}
1143
880
  Reasoning: ${step.reasoning}
1144
- Results: ${step.results.length} items
881
+ Results: ${step.results?.length || 0} items
1145
882
  `;
1146
- });
1147
- return { content: [{ type: "text", text }] };
1148
- } else {
1149
- const results = await oracleSearch({
1150
- query,
1151
- projectId: proj.id,
1152
- topK: max_results
1153
- });
1154
- if (results.length === 0) return { content: [{ type: "text", text: "No results found." }] };
1155
- const text = results.map(
1156
- (r, i) => `${i + 1}. [${r.path}] (relevance: ${r.relevance.toFixed(3)})
883
+ });
884
+ }
885
+ return { content: [{ type: "text", text }] };
886
+ } else {
887
+ if (!results.results || results.results.length === 0) {
888
+ return { content: [{ type: "text", text: "No results found." }] };
889
+ }
890
+ const text = results.results.map(
891
+ (r, i) => `${i + 1}. [${r.path || r.source}] (relevance: ${r.relevance?.toFixed(3) || r.score?.toFixed(3) || "N/A"})
1157
892
  ${r.content.slice(0, 200)}...`
1158
- ).join("\n\n");
1159
- return { content: [{ type: "text", text }] };
893
+ ).join("\n\n");
894
+ return { content: [{ type: "text", text }] };
895
+ }
896
+ } catch (error) {
897
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
1160
898
  }
1161
899
  }
1162
900
  );
@@ -1164,7 +902,7 @@ server.tool(
1164
902
  "autosubscribe_dependencies",
1165
903
  "Automatically index a project's dependencies (package.json, requirements.txt, etc.). Resolves docs URLs and indexes documentation.",
1166
904
  {
1167
- project: z.string().describe("Project name or slug"),
905
+ project: z.string().optional().describe("Project name or slug"),
1168
906
  source_type: z.enum(["github", "local"]).describe("Source location"),
1169
907
  github_owner: z.string().optional().describe("For GitHub: owner/org name"),
1170
908
  github_repo: z.string().optional().describe("For GitHub: repository name"),
@@ -1173,104 +911,100 @@ server.tool(
1173
911
  index_limit: z.number().optional().default(20).describe("Max dependencies to index")
1174
912
  },
1175
913
  async ({ project, source_type, github_owner, github_repo, local_path, dependency_file, index_limit }) => {
1176
- const proj = await resolveProject(project);
1177
- if (!proj) return { content: [{ type: "text", text: `Project '${project}' not found.` }] };
1178
- const { autosubscribe } = await import("./autosubscribe-6EDKPBE2.js");
1179
- const result = await autosubscribe({
1180
- projectId: proj.id,
1181
- orgId: ORG_ID,
1182
- source: source_type === "github" ? { type: "github", owner: github_owner, repo: github_repo } : { type: "local", filePath: local_path },
1183
- indexLimit: index_limit
1184
- });
1185
- return {
1186
- content: [{
1187
- type: "text",
1188
- text: `Autosubscribe completed:
914
+ try {
915
+ const result = await whisper.autosubscribe({
916
+ project,
917
+ source: source_type === "github" ? { type: "github", owner: github_owner, repo: github_repo } : { type: "local", path: local_path },
918
+ index_limit
919
+ });
920
+ return {
921
+ content: [{
922
+ type: "text",
923
+ text: `Autosubscribe completed:
1189
924
  - Discovered: ${result.discovered} dependencies
1190
925
  - Indexed: ${result.indexed} successfully
1191
- - Skipped: ${result.skipped} (already indexed)
1192
- - Errors: ${result.errors.length}`
1193
- }]
1194
- };
926
+ - Errors: ${result.errors?.length || 0}`
927
+ }]
928
+ };
929
+ } catch (error) {
930
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
931
+ }
1195
932
  }
1196
933
  );
1197
934
  server.tool(
1198
935
  "share_context",
1199
936
  "Create a shareable snapshot of a conversation with memories. Returns a URL that can be shared or resumed later.",
1200
937
  {
1201
- project: z.string().describe("Project name or slug"),
938
+ project: z.string().optional().describe("Project name or slug"),
1202
939
  session_id: z.string().describe("Session to share"),
1203
940
  title: z.string().optional().describe("Title for the shared context"),
1204
941
  expiry_days: z.number().optional().default(30).describe("Days until expiry")
1205
942
  },
1206
943
  async ({ project, session_id, title, expiry_days }) => {
1207
- const proj = await resolveProject(project);
1208
- if (!proj) return { content: [{ type: "text", text: `Project '${project}' not found.` }] };
1209
- const { createSharedContext } = await import("./context-sharing-PH64JTXS.js");
1210
- const result = await createSharedContext({
1211
- sessionId: session_id,
1212
- projectId: proj.id,
1213
- orgId: ORG_ID,
1214
- includeMemories: true,
1215
- expiryDays: expiry_days
1216
- });
1217
- return {
1218
- content: [{
1219
- type: "text",
1220
- text: `Shared context created:
1221
- - Share ID: ${result.id}
1222
- - Memories: ${result.memories?.length || 0}
1223
- - Messages: ${result.messages?.length || 0}
1224
- - Expires: ${result.expiresAt?.toISOString() || "Never"}
944
+ try {
945
+ const result = await whisper.createSharedContext({
946
+ project,
947
+ session_id,
948
+ title,
949
+ expiry_days
950
+ });
951
+ return {
952
+ content: [{
953
+ type: "text",
954
+ text: `Shared context created:
955
+ - Share ID: ${result.share_id}
956
+ - Memories: ${result.memories_count || 0}
957
+ - Messages: ${result.messages_count || 0}
958
+ - Expires: ${result.expires_at || "Never"}
1225
959
 
1226
- Share URL: ${result.shareUrl}`
1227
- }]
1228
- };
960
+ Share URL: ${result.share_url}`
961
+ }]
962
+ };
963
+ } catch (error) {
964
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
965
+ }
1229
966
  }
1230
967
  );
1231
968
  server.tool(
1232
969
  "consolidate_memories",
1233
970
  "Find and merge duplicate memories to reduce bloat. Uses vector similarity + LLM merging.",
1234
971
  {
1235
- project: z.string().describe("Project name or slug"),
972
+ project: z.string().optional().describe("Project name or slug"),
1236
973
  similarity_threshold: z.number().optional().default(0.95).describe("Similarity threshold (0-1)"),
1237
974
  dry_run: z.boolean().optional().default(false).describe("Preview without merging")
1238
975
  },
1239
976
  async ({ project, similarity_threshold, dry_run }) => {
1240
- const proj = await resolveProject(project);
1241
- if (!proj) return { content: [{ type: "text", text: `Project '${project}' not found.` }] };
1242
- const { findDuplicateMemories, consolidateMemories } = await import("./consolidation-FOVQTWNQ.js");
1243
- if (dry_run) {
1244
- const clusters = await findDuplicateMemories({
1245
- projectId: proj.id,
1246
- similarityThreshold: similarity_threshold
977
+ try {
978
+ const result = await whisper.consolidateMemories({
979
+ project,
980
+ similarity_threshold,
981
+ dry_run
1247
982
  });
1248
- const totalDuplicates = clusters.reduce((sum, c) => sum + c.duplicates.length, 0);
1249
- return {
1250
- content: [{
1251
- type: "text",
1252
- text: `Found ${clusters.length} duplicate clusters:
983
+ if (dry_run && result.clusters) {
984
+ const totalDuplicates = result.clusters.reduce((sum, c) => sum + (c.duplicates?.length || 0), 0);
985
+ return {
986
+ content: [{
987
+ type: "text",
988
+ text: `Found ${result.clusters.length} duplicate clusters:
1253
989
  - Total duplicates: ${totalDuplicates}
1254
990
  - Estimated savings: ${totalDuplicates} memories
1255
991
 
1256
992
  Run without dry_run to merge.`
1257
- }]
1258
- };
1259
- } else {
1260
- const result = await consolidateMemories({
1261
- projectId: proj.id,
1262
- similarityThreshold: similarity_threshold,
1263
- dryRun: false
1264
- });
1265
- return {
1266
- content: [{
1267
- type: "text",
1268
- text: `Consolidation complete:
1269
- - Clusters found: ${result.clustersFound}
1270
- - Memories merged: ${result.memoriesMerged}
1271
- - Memories deactivated: ${result.memoriesDeactivated}`
1272
- }]
1273
- };
993
+ }]
994
+ };
995
+ } else {
996
+ return {
997
+ content: [{
998
+ type: "text",
999
+ text: `Consolidation complete:
1000
+ - Clusters found: ${result.clusters_found || 0}
1001
+ - Memories merged: ${result.memories_merged || 0}
1002
+ - Memories deactivated: ${result.memories_deactivated || 0}`
1003
+ }]
1004
+ };
1005
+ }
1006
+ } catch (error) {
1007
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
1274
1008
  }
1275
1009
  }
1276
1010
  );
@@ -1282,48 +1016,39 @@ server.tool(
1282
1016
  days: z.number().optional().default(30).describe("Time period in days")
1283
1017
  },
1284
1018
  async ({ project, days }) => {
1285
- let projectId;
1286
- if (project) {
1287
- const proj = await resolveProject(project);
1288
- if (!proj) return { content: [{ type: "text", text: `Project '${project}' not found.` }] };
1289
- projectId = proj.id;
1290
- }
1291
- const { getCostSummary, calculateSavings } = await import("./cost-optimization-F3L5BS5F.js");
1292
- const endDate = /* @__PURE__ */ new Date();
1293
- const startDate = new Date(endDate.getTime() - days * 24 * 60 * 60 * 1e3);
1294
- const summary = getCostSummary({
1295
- since: startDate,
1296
- groupBy: "model"
1297
- });
1298
- const savings = calculateSavings({
1299
- since: startDate
1300
- });
1301
- let text = `Cost Summary (last ${days} days):
1019
+ try {
1020
+ const endDate = /* @__PURE__ */ new Date();
1021
+ const startDate = new Date(endDate.getTime() - days * 24 * 60 * 60 * 1e3);
1022
+ const summary = await whisper.getCostSummary({
1023
+ project,
1024
+ start_date: startDate.toISOString(),
1025
+ end_date: endDate.toISOString()
1026
+ });
1027
+ const savings = await whisper.getCostSavings({
1028
+ project,
1029
+ start_date: startDate.toISOString(),
1030
+ end_date: endDate.toISOString()
1031
+ });
1032
+ let text = `Cost Summary (last ${days} days):
1302
1033
 
1303
1034
  `;
1304
- text += `Total Cost: $${savings.actualCost.toFixed(2)}
1035
+ text += `Total Cost: $${savings.actual_cost?.toFixed(2) || "0.00"}
1305
1036
  `;
1306
- text += `Total Requests: ${Object.values(summary).reduce((sum, s) => sum + s.calls, 0)}
1307
- `;
1308
- text += `Avg Cost/Request: $${(savings.actualCost / (Object.values(summary).reduce((sum, s) => sum + s.calls, 0) || 1)).toFixed(4)}
1037
+ text += `Total Requests: ${summary.total_requests || 0}
1309
1038
 
1310
1039
  `;
1311
- text += `By Model:
1312
- `;
1313
- Object.entries(summary).forEach(([model, stats]) => {
1314
- text += `- ${model}: $${stats.totalCost.toFixed(2)} (${stats.calls} calls)
1040
+ text += `Savings vs Always-Opus:
1315
1041
  `;
1316
- });
1317
- text += `
1318
- Savings vs Always-Opus:
1319
- `;
1320
- text += `- Actual: $${savings.actualCost.toFixed(2)}
1042
+ text += `- Actual: $${savings.actual_cost?.toFixed(2) || "0.00"}
1321
1043
  `;
1322
- text += `- Opus-only: $${savings.opusCost.toFixed(2)}
1044
+ text += `- Opus-only: $${savings.opus_cost?.toFixed(2) || "0.00"}
1323
1045
  `;
1324
- text += `- Saved: $${savings.savings.toFixed(2)} (${savings.savingsPercent.toFixed(1)}%)
1046
+ text += `- Saved: $${savings.savings?.toFixed(2) || "0.00"} (${savings.savings_percent?.toFixed(1) || "0"}%)
1325
1047
  `;
1326
- return { content: [{ type: "text", text }] };
1048
+ return { content: [{ type: "text", text }] };
1049
+ } catch (error) {
1050
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
1051
+ }
1327
1052
  }
1328
1053
  );
1329
1054
  async function main() {