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