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