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