@usewhisper/mcp-server 2.9.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/server.js +1250 -296
  2. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -6,7 +6,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
6
6
  import { z } from "zod";
7
7
  import { execSync, spawnSync } from "child_process";
8
8
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, appendFileSync } from "fs";
9
- import { join, relative, extname } from "path";
9
+ import { join, relative, extname, normalize as normalizePath, resolve as resolvePath } from "path";
10
10
  import { homedir } from "os";
11
11
  import { createHash, randomUUID } from "crypto";
12
12
 
@@ -3879,17 +3879,195 @@ function ensureStateDir() {
3879
3879
  mkdirSync(STATE_DIR, { recursive: true });
3880
3880
  }
3881
3881
  }
3882
+ function canonicalizeWorkspacePath(path, cwd = process.cwd()) {
3883
+ const basePath = path?.trim() ? path.trim() : cwd;
3884
+ const absolute = resolvePath(cwd, basePath);
3885
+ const normalized = normalizePath(absolute).replace(/\\/g, "/");
3886
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
3887
+ }
3888
+ function chooseWorkspaceProjectSource(args) {
3889
+ const explicitProject = args.explicitProject?.trim();
3890
+ if (explicitProject) {
3891
+ return { project_ref: explicitProject, resolved_by: "explicit_project" };
3892
+ }
3893
+ const workspaceProjectRef = args.workspaceProjectRef?.trim();
3894
+ if (workspaceProjectRef) {
3895
+ return { project_ref: workspaceProjectRef, resolved_by: "workspace_binding" };
3896
+ }
3897
+ const defaultProject = args.defaultProject?.trim();
3898
+ if (defaultProject) {
3899
+ return { project_ref: defaultProject, resolved_by: "env_default" };
3900
+ }
3901
+ return { project_ref: null, resolved_by: "unresolved" };
3902
+ }
3903
+ function classifyWorkspaceHealth(args) {
3904
+ if (!args.project_ref) return "unbound";
3905
+ if (!args.last_indexed_at || (args.coverage ?? 0) <= 0) return "unindexed";
3906
+ const nowMs = args.now_ms ?? Date.now();
3907
+ const lastIndexedMs = new Date(args.last_indexed_at).getTime();
3908
+ const ageHours = Number.isFinite(lastIndexedMs) ? (nowMs - lastIndexedMs) / (60 * 60 * 1e3) : Number.POSITIVE_INFINITY;
3909
+ const pendingChanges = args.pending_changes ?? 0;
3910
+ const drifted = pendingChanges > 0 || Boolean(
3911
+ args.last_indexed_commit && args.current_commit && args.last_indexed_commit !== args.current_commit
3912
+ );
3913
+ if (drifted) return "drifted";
3914
+ if (ageHours > (args.max_staleness_hours ?? 168)) return "stale";
3915
+ return "healthy";
3916
+ }
3917
+ function resolveWorkspaceIdentity(args) {
3918
+ const rootPath = canonicalizeWorkspacePath(args?.path, args?.cwd);
3919
+ const derivedWorkspaceId = getWorkspaceIdForPath(rootPath);
3920
+ const requestedWorkspaceId = args?.workspace_id?.trim();
3921
+ if (requestedWorkspaceId && requestedWorkspaceId !== derivedWorkspaceId) {
3922
+ throw new Error(
3923
+ `workspace_id '${requestedWorkspaceId}' does not match canonical workspace '${derivedWorkspaceId}' for ${rootPath}.`
3924
+ );
3925
+ }
3926
+ return {
3927
+ workspace_id: derivedWorkspaceId,
3928
+ root_path: rootPath,
3929
+ identity_source: args?.path?.trim() ? "path_canonical" : "cwd_canonical"
3930
+ };
3931
+ }
3882
3932
  function getWorkspaceId(workspaceId) {
3883
3933
  if (workspaceId?.trim()) return workspaceId.trim();
3884
- const seed = `${process.cwd()}|${DEFAULT_PROJECT || "default"}|${API_KEY.slice(0, 12)}`;
3934
+ const seed = `${canonicalizeWorkspacePath(process.cwd())}|${API_KEY.slice(0, 12) || "anon"}`;
3885
3935
  return createHash("sha256").update(seed).digest("hex").slice(0, 20);
3886
3936
  }
3887
3937
  function getWorkspaceIdForPath(path, workspaceId) {
3888
3938
  if (workspaceId?.trim()) return workspaceId.trim();
3889
3939
  if (!path) return getWorkspaceId(void 0);
3890
- const seed = `${path}|${DEFAULT_PROJECT || "default"}|${API_KEY.slice(0, 12)}`;
3940
+ const seed = `${canonicalizeWorkspacePath(path)}|${API_KEY.slice(0, 12) || "anon"}`;
3891
3941
  return createHash("sha256").update(seed).digest("hex").slice(0, 20);
3892
3942
  }
3943
+ function normalizeLoosePath(value) {
3944
+ if (!value || !String(value).trim()) return null;
3945
+ return canonicalizeWorkspacePath(String(value));
3946
+ }
3947
+ function normalizeRepoName(value) {
3948
+ if (!value || !String(value).trim()) return null;
3949
+ return String(value).trim().replace(/\.git$/i, "").toLowerCase();
3950
+ }
3951
+ function parseGitHubRemote(remote) {
3952
+ if (!remote) return null;
3953
+ const normalized = remote.trim().replace(/\.git$/i, "");
3954
+ const httpsMatch = normalized.match(/github\.com[/:]([^/:\s]+)\/([^/\s]+)$/i);
3955
+ if (httpsMatch) {
3956
+ return { owner: httpsMatch[1].toLowerCase(), repo: httpsMatch[2].toLowerCase() };
3957
+ }
3958
+ const sshMatch = normalized.match(/git@github\.com:([^/:\s]+)\/([^/\s]+)$/i);
3959
+ if (sshMatch) {
3960
+ return { owner: sshMatch[1].toLowerCase(), repo: sshMatch[2].toLowerCase() };
3961
+ }
3962
+ return null;
3963
+ }
3964
+ function getGitRemoteUrl(searchPath) {
3965
+ const root = searchPath || process.cwd();
3966
+ const result = spawnSync("git", ["-C", root, "config", "--get", "remote.origin.url"], { encoding: "utf-8" });
3967
+ if (result.status !== 0) return void 0;
3968
+ const out = String(result.stdout || "").trim();
3969
+ return out || void 0;
3970
+ }
3971
+ function getGitBranch(searchPath) {
3972
+ const root = searchPath || process.cwd();
3973
+ const result = spawnSync("git", ["-C", root, "rev-parse", "--abbrev-ref", "HEAD"], { encoding: "utf-8" });
3974
+ if (result.status !== 0) return void 0;
3975
+ const out = String(result.stdout || "").trim();
3976
+ return out || void 0;
3977
+ }
3978
+ function sourceStatusLooksReady(status) {
3979
+ const normalized = String(status || "").trim().toLowerCase();
3980
+ if (!normalized) return true;
3981
+ if (["ready", "indexed", "completed", "complete", "active", "synced", "success"].includes(normalized)) return true;
3982
+ if (["processing", "queued", "syncing", "pending", "creating", "indexing", "error", "failed"].includes(normalized)) {
3983
+ return false;
3984
+ }
3985
+ return !normalized.includes("error") && !normalized.includes("fail") && !normalized.includes("queue");
3986
+ }
3987
+ function sourceMatchesWorkspacePath(source, rootPath) {
3988
+ const config = source.config || {};
3989
+ const candidates = [
3990
+ config.path,
3991
+ config.root_path,
3992
+ config.workspace_path,
3993
+ config.workspacePath,
3994
+ config.local_path,
3995
+ config.file_path,
3996
+ config.directory
3997
+ ].map((value) => normalizeLoosePath(typeof value === "string" ? value : null)).filter(Boolean);
3998
+ return candidates.some((candidate) => candidate === rootPath || rootPath.startsWith(`${candidate}/`) || candidate.startsWith(`${rootPath}/`));
3999
+ }
4000
+ function sourceMatchesWorkspaceRepo(source, repoIdentity, branch) {
4001
+ if (!repoIdentity) return false;
4002
+ const config = source.config || {};
4003
+ const owner = normalizeRepoName(config.owner || config.org || config.organization || null);
4004
+ const repo = normalizeRepoName(config.repo || config.repository || config.name || null);
4005
+ const sourceUrlRepo = parseGitHubRemote(typeof config.url === "string" ? config.url : null);
4006
+ const branchValue = normalizeRepoName(config.branch || config.ref || config.default_branch || null);
4007
+ const ownerMatches = owner === repoIdentity.owner || sourceUrlRepo?.owner === repoIdentity.owner;
4008
+ const repoMatches = repo === repoIdentity.repo || sourceUrlRepo?.repo === repoIdentity.repo;
4009
+ if (!ownerMatches || !repoMatches) return false;
4010
+ if (!branchValue || !branch) return true;
4011
+ return branchValue === normalizeRepoName(branch);
4012
+ }
4013
+ function classifyProjectRepoReadiness(args) {
4014
+ const remoteIdentity = parseGitHubRemote(args.remote_url || null);
4015
+ const matchedSources = args.sources.filter(
4016
+ (source) => sourceMatchesWorkspacePath(source, args.root_path) || sourceMatchesWorkspaceRepo(source, remoteIdentity, args.branch || void 0)
4017
+ );
4018
+ if (matchedSources.length === 0) {
4019
+ return {
4020
+ retrieval_readiness: "project_bound_no_repo_source",
4021
+ matched_sources: [],
4022
+ matched_source_ids: [],
4023
+ warnings: [`No verified local or GitHub source matches ${args.root_path}.`]
4024
+ };
4025
+ }
4026
+ const readySources = matchedSources.filter((source) => sourceStatusLooksReady(source.status));
4027
+ if (readySources.length === 0) {
4028
+ return {
4029
+ retrieval_readiness: "project_repo_source_stale",
4030
+ matched_sources: matchedSources,
4031
+ matched_source_ids: matchedSources.map((source) => source.id),
4032
+ warnings: [
4033
+ `Matching repo sources are present but not ready (${matchedSources.map((source) => `${source.name}:${source.status}`).join(", ")}).`
4034
+ ]
4035
+ };
4036
+ }
4037
+ return {
4038
+ retrieval_readiness: "project_repo_source_ready",
4039
+ matched_sources: readySources,
4040
+ matched_source_ids: readySources.map((source) => source.id),
4041
+ warnings: []
4042
+ };
4043
+ }
4044
+ function classifyRepoGroundedQuery(args) {
4045
+ const query = args.query.toLowerCase();
4046
+ let score = 0;
4047
+ if (/(^|[\s"'])where is\b|which file|what file|show me|wiring|handler|middleware|implementation|route|endpoint|function|class|module|repo|workspace|code/.test(query)) {
4048
+ score += 1;
4049
+ }
4050
+ if (/\bauth\b|\burl\b|\bcookie\b|\bsession\b|\bapi\b/.test(query)) {
4051
+ score += 1;
4052
+ }
4053
+ if (/[A-Za-z0-9/_-]+\.[A-Za-z0-9]+/.test(args.query) || /\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]{2,}/.test(args.query)) {
4054
+ score += 2;
4055
+ }
4056
+ if (/[A-Za-z_$][A-Za-z0-9_$-]*\(/.test(args.query) || /[A-Z][A-Za-z0-9]+/.test(args.query)) {
4057
+ score += 1;
4058
+ }
4059
+ if ((args.chunk_types || []).some((chunkType) => ["code", "function", "class", "config", "schema", "api_spec"].includes(chunkType))) {
4060
+ score += 2;
4061
+ }
4062
+ const changedTokens = new Set((args.changed_path_tokens || []).map((token) => token.toLowerCase()));
4063
+ if (changedTokens.size > 0) {
4064
+ const queryTokens = tokenizeQueryForLexicalRescue(args.query);
4065
+ if (queryTokens.some((token) => changedTokens.has(token.toLowerCase()))) {
4066
+ score += 1;
4067
+ }
4068
+ }
4069
+ return score >= 2;
4070
+ }
3893
4071
  function clamp012(value) {
3894
4072
  if (Number.isNaN(value)) return 0;
3895
4073
  if (value < 0) return 0;
@@ -3978,7 +4156,10 @@ function getWorkspaceState(state, workspaceId) {
3978
4156
  annotations: [],
3979
4157
  session_summaries: [],
3980
4158
  events: [],
3981
- index_metadata: {}
4159
+ index_metadata: {},
4160
+ root_path: void 0,
4161
+ project_ref: void 0,
4162
+ project_id: void 0
3982
4163
  };
3983
4164
  }
3984
4165
  return state.workspaces[workspaceId];
@@ -3988,17 +4169,126 @@ function computeChecksum(value) {
3988
4169
  }
3989
4170
  var cachedProjectRef = DEFAULT_PROJECT || void 0;
3990
4171
  var cachedMcpSessionId = process.env.WHISPER_SESSION_ID || `mcp_${randomUUID().slice(0, 12)}`;
4172
+ async function resolveProjectDescriptor(projectRef) {
4173
+ try {
4174
+ const resolved = await whisper.resolveProject(projectRef);
4175
+ const resolvedRef = resolved.slug || resolved.name || resolved.id || projectRef;
4176
+ cachedProjectRef = resolvedRef;
4177
+ return { project_ref: resolvedRef, project_id: resolved.id || null };
4178
+ } catch {
4179
+ cachedProjectRef = projectRef;
4180
+ return { project_ref: projectRef, project_id: null };
4181
+ }
4182
+ }
4183
+ function getWorkspaceRecommendedNextCalls(health) {
4184
+ if (health === "unbound") return ["index.workspace_resolve", "context.list_projects", "index.workspace_status"];
4185
+ if (health === "unindexed") return ["index.workspace_status", "index.workspace_run", "search_code", "grep"];
4186
+ if (health === "stale" || health === "drifted") return ["index.workspace_status", "index.workspace_run", "grep", "search_code"];
4187
+ return [];
4188
+ }
4189
+ function getWorkspaceWarnings(args) {
4190
+ if (args.health === "unbound") {
4191
+ return [`Workspace ${args.rootPath} is not bound to a Whisper project.`];
4192
+ }
4193
+ if (args.health === "unindexed") {
4194
+ return [`Workspace ${args.rootPath} is bound to ${args.projectRef}, but no usable local index metadata exists yet.`];
4195
+ }
4196
+ if (args.health === "stale") {
4197
+ const age = args.freshness.age_hours == null ? "unknown" : `${Math.round(args.freshness.age_hours)}h`;
4198
+ return [`Workspace index for ${args.projectRef} is stale (${age} old).`];
4199
+ }
4200
+ if (args.health === "drifted") {
4201
+ const reasons = [];
4202
+ if (args.pendingChanges && args.pendingChanges > 0) reasons.push(`${args.pendingChanges} pending local changes`);
4203
+ if (args.lastIndexedCommit && args.currentCommit && args.lastIndexedCommit !== args.currentCommit) {
4204
+ reasons.push(`indexed commit ${args.lastIndexedCommit.slice(0, 12)} differs from current ${args.currentCommit.slice(0, 12)}`);
4205
+ }
4206
+ const suffix = reasons.length ? `: ${reasons.join("; ")}` : "";
4207
+ return [`Workspace state drifted since the last index${suffix}.`];
4208
+ }
4209
+ return [];
4210
+ }
4211
+ async function resolveWorkspaceTrust(args) {
4212
+ const identity = resolveWorkspaceIdentity({ path: args.path, workspace_id: args.workspace_id });
4213
+ const rootPath = identity.root_path;
4214
+ const workspaceId = identity.workspace_id;
4215
+ const state = loadState();
4216
+ const workspace = getWorkspaceState(state, workspaceId);
4217
+ let mutated = false;
4218
+ if (workspace.root_path !== rootPath) {
4219
+ workspace.root_path = rootPath;
4220
+ mutated = true;
4221
+ }
4222
+ const selected = chooseWorkspaceProjectSource({
4223
+ explicitProject: args.project,
4224
+ workspaceProjectRef: workspace.project_ref,
4225
+ defaultProject: DEFAULT_PROJECT || null
4226
+ });
4227
+ let projectRef = null;
4228
+ let projectId = workspace.project_id || null;
4229
+ if (selected.project_ref) {
4230
+ const resolved = await resolveProjectDescriptor(selected.project_ref);
4231
+ projectRef = resolved.project_ref;
4232
+ projectId = resolved.project_id;
4233
+ if (workspace.project_ref !== projectRef || (workspace.project_id || null) !== projectId) {
4234
+ workspace.project_ref = projectRef;
4235
+ workspace.project_id = projectId || void 0;
4236
+ mutated = true;
4237
+ }
4238
+ }
4239
+ if (mutated) saveState(state);
4240
+ const lastIndexedAt = workspace.index_metadata?.last_indexed_at || null;
4241
+ const ageHours = lastIndexedAt ? (Date.now() - new Date(lastIndexedAt).getTime()) / (60 * 60 * 1e3) : null;
4242
+ const currentCommit = getGitHead(rootPath) || null;
4243
+ const pendingChanges = getGitPendingCount(rootPath);
4244
+ const lastIndexedCommit = workspace.index_metadata?.last_indexed_commit || null;
4245
+ const coverage = workspace.index_metadata?.coverage ?? 0;
4246
+ const health = classifyWorkspaceHealth({
4247
+ project_ref: projectRef,
4248
+ coverage,
4249
+ last_indexed_at: lastIndexedAt,
4250
+ last_indexed_commit: lastIndexedCommit,
4251
+ current_commit: currentCommit,
4252
+ pending_changes: pendingChanges ?? null,
4253
+ max_staleness_hours: args.max_staleness_hours ?? 168
4254
+ });
4255
+ const freshness = {
4256
+ stale: health === "stale",
4257
+ age_hours: ageHours,
4258
+ last_indexed_at: lastIndexedAt
4259
+ };
4260
+ const warnings = getWorkspaceWarnings({
4261
+ health,
4262
+ rootPath,
4263
+ projectRef,
4264
+ currentCommit,
4265
+ lastIndexedCommit,
4266
+ pendingChanges: pendingChanges ?? null,
4267
+ freshness
4268
+ });
4269
+ return {
4270
+ workspace_id: workspaceId,
4271
+ root_path: rootPath,
4272
+ identity_source: identity.identity_source,
4273
+ project_ref: projectRef,
4274
+ project_id: projectId,
4275
+ resolved_by: selected.resolved_by,
4276
+ health,
4277
+ warnings,
4278
+ recommended_next_calls: getWorkspaceRecommendedNextCalls(health),
4279
+ freshness,
4280
+ coverage,
4281
+ last_indexed_commit: lastIndexedCommit,
4282
+ current_commit: currentCommit,
4283
+ pending_changes: pendingChanges ?? null,
4284
+ grounded_to_workspace: health === "healthy"
4285
+ };
4286
+ }
3991
4287
  async function resolveProjectRef(explicit) {
3992
4288
  if (explicit?.trim()) {
3993
4289
  const requestedRef = explicit.trim();
3994
- try {
3995
- const resolved = await whisper.resolveProject(requestedRef);
3996
- cachedProjectRef = resolved.slug || resolved.name || resolved.id;
3997
- return cachedProjectRef;
3998
- } catch {
3999
- cachedProjectRef = requestedRef;
4000
- return requestedRef;
4001
- }
4290
+ const resolved = await resolveProjectDescriptor(requestedRef);
4291
+ return resolved.project_ref;
4002
4292
  }
4003
4293
  if (cachedProjectRef) return cachedProjectRef;
4004
4294
  try {
@@ -4011,6 +4301,98 @@ async function resolveProjectRef(explicit) {
4011
4301
  return void 0;
4012
4302
  }
4013
4303
  }
4304
+ function collectGitChangedPathTokens(searchPath) {
4305
+ const root = searchPath || process.cwd();
4306
+ const result = spawnSync("git", ["-C", root, "status", "--porcelain"], { encoding: "utf-8" });
4307
+ if (result.status !== 0) return [];
4308
+ const lines = String(result.stdout || "").split("\n").map((line) => line.trim()).filter(Boolean);
4309
+ const tokens = /* @__PURE__ */ new Set();
4310
+ for (const line of lines) {
4311
+ const filePath = line.slice(3).split(" -> ").at(-1)?.trim();
4312
+ if (!filePath) continue;
4313
+ for (const token of filePath.split(/[\\/._-]+/).filter((part) => part.length >= 3)) {
4314
+ tokens.add(token.toLowerCase());
4315
+ }
4316
+ }
4317
+ return Array.from(tokens);
4318
+ }
4319
+ async function inspectProjectRepoSources(args) {
4320
+ if (!args.project_ref) {
4321
+ return {
4322
+ retrieval_readiness: "no_project",
4323
+ matched_sources: [],
4324
+ matched_source_ids: [],
4325
+ warnings: ["No Whisper project is bound to this workspace."]
4326
+ };
4327
+ }
4328
+ try {
4329
+ const sourceData = await whisper.listSources(args.project_ref);
4330
+ const classified = classifyProjectRepoReadiness({
4331
+ sources: sourceData.sources || [],
4332
+ root_path: args.root_path,
4333
+ remote_url: getGitRemoteUrl(args.root_path) || null,
4334
+ branch: getGitBranch(args.root_path) || null
4335
+ });
4336
+ if (classified.retrieval_readiness === "project_bound_no_repo_source") {
4337
+ return {
4338
+ ...classified,
4339
+ warnings: [`Project ${args.project_ref} has no verified local or GitHub source for ${args.root_path}.`]
4340
+ };
4341
+ }
4342
+ if (classified.retrieval_readiness === "project_repo_source_stale") {
4343
+ return {
4344
+ ...classified,
4345
+ warnings: [
4346
+ `Project ${args.project_ref} has matching repo sources, but none are ready (${classified.matched_sources.map((source) => `${source.name}:${source.status}`).join(", ")}).`
4347
+ ]
4348
+ };
4349
+ }
4350
+ return classified;
4351
+ } catch (error) {
4352
+ return {
4353
+ retrieval_readiness: "project_unverified",
4354
+ matched_sources: [],
4355
+ matched_source_ids: [],
4356
+ warnings: [`Could not verify project sources for ${args.project_ref}: ${error.message}`]
4357
+ };
4358
+ }
4359
+ }
4360
+ async function resolveRepoGroundingPreflight(args) {
4361
+ const trust = await resolveWorkspaceTrust({ path: args.path, workspace_id: args.workspace_id, project: args.project });
4362
+ const repoGrounded = classifyRepoGroundedQuery({
4363
+ query: args.query,
4364
+ chunk_types: args.chunk_types,
4365
+ changed_path_tokens: collectGitChangedPathTokens(trust.root_path)
4366
+ });
4367
+ const sourceVerification = await inspectProjectRepoSources({
4368
+ project_ref: trust.project_ref,
4369
+ root_path: trust.root_path
4370
+ });
4371
+ const warnings = [...trust.warnings, ...sourceVerification.warnings];
4372
+ let retrievalReadiness = sourceVerification.retrieval_readiness;
4373
+ let retrievalRoute = "none";
4374
+ if (repoGrounded) {
4375
+ retrievalRoute = sourceVerification.retrieval_readiness === "project_repo_source_ready" ? "project_repo" : "local_workspace_fallback";
4376
+ if (retrievalRoute === "local_workspace_fallback") {
4377
+ retrievalReadiness = "local_fallback";
4378
+ warnings.push("Using live local workspace retrieval because project-backed repo retrieval is unavailable or unverified.");
4379
+ }
4380
+ }
4381
+ const recommendedNextCalls = [...trust.recommended_next_calls];
4382
+ if (sourceVerification.retrieval_readiness === "project_bound_no_repo_source" || sourceVerification.retrieval_readiness === "project_unverified") {
4383
+ recommendedNextCalls.push("context.list_sources", "index.local_scan_ingest");
4384
+ }
4385
+ return {
4386
+ repo_grounded: repoGrounded,
4387
+ trust_state: trust,
4388
+ retrieval_readiness: retrievalReadiness,
4389
+ retrieval_route: retrievalRoute,
4390
+ matched_sources: sourceVerification.matched_sources,
4391
+ matched_source_ids: sourceVerification.matched_source_ids,
4392
+ warnings: Array.from(new Set(warnings)),
4393
+ recommended_next_calls: Array.from(new Set(recommendedNextCalls))
4394
+ };
4395
+ }
4014
4396
  async function ingestSessionWithSyncFallback(params) {
4015
4397
  try {
4016
4398
  return await whisper.ingestSession({
@@ -4037,7 +4419,7 @@ function resolveMcpScope(params) {
4037
4419
  project: params?.project,
4038
4420
  userId: params?.user_id?.trim() || defaultMcpUserId(),
4039
4421
  sessionId: params?.session_id?.trim() || cachedMcpSessionId,
4040
- workspacePath: process.env.WHISPER_WORKSPACE_PATH || process.cwd()
4422
+ workspacePath: canonicalizeWorkspacePath(params?.path || process.env.WHISPER_WORKSPACE_PATH || process.cwd())
4041
4423
  };
4042
4424
  }
4043
4425
  async function prepareAutomaticQuery(params) {
@@ -4101,7 +4483,9 @@ function buildAbstain(args) {
4101
4483
  reason: args.reason,
4102
4484
  message: args.message,
4103
4485
  closest_evidence: args.closest_evidence,
4104
- recommended_next_calls: ["index.workspace_status", "index.workspace_run", "symbol_search", "context.get_relevant"],
4486
+ warnings: args.warnings || [],
4487
+ trust_state: args.trust_state,
4488
+ recommended_next_calls: args.recommended_next_calls || ["index.workspace_status", "index.workspace_run", "grep", "context.get_relevant"],
4105
4489
  diagnostics: {
4106
4490
  claims_evaluated: args.claims_evaluated,
4107
4491
  evidence_items_found: args.evidence_items_found,
@@ -4177,6 +4561,86 @@ function formatCanonicalMemoryResults(rawResults) {
4177
4561
  };
4178
4562
  });
4179
4563
  }
4564
+ function normalizeLooseText(value) {
4565
+ return String(value || "").trim().toLowerCase().replace(/\s+/g, " ");
4566
+ }
4567
+ function resolveForgetQueryCandidates(rawResults, query) {
4568
+ const normalizedQuery = normalizeLooseText(query);
4569
+ const candidates = normalizeCanonicalResults(rawResults).map((result) => {
4570
+ const memory = result?.memory || result;
4571
+ const metadata = memory?.metadata || {};
4572
+ return {
4573
+ id: String(memory?.id || "").trim(),
4574
+ content: normalizeLooseText(memory?.content),
4575
+ normalized_content: normalizeLooseText(metadata?.normalized_content),
4576
+ canonical_content: normalizeLooseText(metadata?.canonical_content),
4577
+ similarity: typeof result?.similarity === "number" ? result.similarity : typeof result?.score === "number" ? result.score : 0
4578
+ };
4579
+ }).filter((candidate) => candidate.id);
4580
+ const exactMatches = candidates.filter(
4581
+ (candidate) => candidate.id.toLowerCase() === normalizedQuery || candidate.content === normalizedQuery || candidate.normalized_content === normalizedQuery || candidate.canonical_content === normalizedQuery
4582
+ );
4583
+ if (exactMatches.length > 0) {
4584
+ return { memory_ids: [...new Set(exactMatches.map((candidate) => candidate.id))], resolved_by: "exact" };
4585
+ }
4586
+ const substringMatches = candidates.filter((candidate) => {
4587
+ const haystacks = [candidate.content, candidate.normalized_content, candidate.canonical_content].filter(Boolean);
4588
+ return haystacks.some(
4589
+ (value) => value.includes(normalizedQuery) || normalizedQuery.length >= 12 && normalizedQuery.includes(value)
4590
+ );
4591
+ });
4592
+ if (substringMatches.length === 1) {
4593
+ return { memory_ids: [substringMatches[0].id], resolved_by: "substring" };
4594
+ }
4595
+ if (substringMatches.length > 1) {
4596
+ return {
4597
+ memory_ids: [],
4598
+ resolved_by: "ambiguous",
4599
+ warning: "Query matched multiple memories by recognizable text. Use memory_id or a more specific query."
4600
+ };
4601
+ }
4602
+ const ranked = [...candidates].sort((a, b) => b.similarity - a.similarity);
4603
+ if (ranked[0] && ranked[0].similarity >= 0.9 && (!ranked[1] || ranked[0].similarity - ranked[1].similarity >= 0.08)) {
4604
+ return { memory_ids: [ranked[0].id], resolved_by: "high_confidence" };
4605
+ }
4606
+ return { memory_ids: [], resolved_by: "none", warning: "Query did not resolve to a reliable memory match. No memories were changed." };
4607
+ }
4608
+ async function searchMemoriesForContextQuery(args) {
4609
+ return whisper.searchMemoriesSOTA({
4610
+ project: args.project,
4611
+ query: args.query,
4612
+ user_id: args.user_id,
4613
+ session_id: args.session_id,
4614
+ top_k: args.top_k ?? 5,
4615
+ include_relations: false,
4616
+ include_pending: true
4617
+ });
4618
+ }
4619
+ async function runContextQueryMemoryRescue(args) {
4620
+ const scoped = await searchMemoriesForContextQuery(args);
4621
+ const scopedResults = formatCanonicalMemoryResults(scoped);
4622
+ if (scopedResults.length > 0) {
4623
+ return { results: scopedResults, rescue_mode: "scoped" };
4624
+ }
4625
+ if (args.user_id || args.session_id) {
4626
+ return { results: [], rescue_mode: null };
4627
+ }
4628
+ const broad = await searchMemoriesForContextQuery({
4629
+ project: args.project,
4630
+ query: args.query,
4631
+ top_k: args.top_k
4632
+ });
4633
+ return { results: formatCanonicalMemoryResults(broad), rescue_mode: formatCanonicalMemoryResults(broad).length > 0 ? "project_broad" : null };
4634
+ }
4635
+ function renderContextQueryMemoryRescue(args) {
4636
+ const lines = args.results.map(
4637
+ (result, index) => `${index + 1}. [${result.memory_type || "memory"}, score: ${result.similarity ?? "n/a"}] ${result.content}`
4638
+ );
4639
+ const scopeLabel = args.rescue_mode === "project_broad" ? `project=${args.project}, project_broad_memory_rescue=true` : `project=${args.project}, user=${args.scope.userId}, session=${args.scope.sessionId}, scoped_memory_rescue=true`;
4640
+ return `Found ${args.results.length} memory result(s) (${scopeLabel}):
4641
+
4642
+ ${lines.join("\n\n")}`;
4643
+ }
4180
4644
  function likelyEmbeddingFailure(error) {
4181
4645
  const message = String(error?.message || error || "").toLowerCase();
4182
4646
  return message.includes("embedding") || message.includes("vector") || message.includes("timeout") || message.includes("timed out") || message.includes("temporarily unavailable");
@@ -4191,6 +4655,7 @@ async function queryWithDegradedFallback(params) {
4191
4655
  include_graph: params.include_graph,
4192
4656
  user_id: params.user_id,
4193
4657
  session_id: params.session_id,
4658
+ source_ids: params.source_ids,
4194
4659
  hybrid: true,
4195
4660
  rerank: true
4196
4661
  });
@@ -4205,6 +4670,7 @@ async function queryWithDegradedFallback(params) {
4205
4670
  include_graph: false,
4206
4671
  user_id: params.user_id,
4207
4672
  session_id: params.session_id,
4673
+ source_ids: params.source_ids,
4208
4674
  hybrid: false,
4209
4675
  rerank: false,
4210
4676
  vector_weight: 0,
@@ -4218,6 +4684,308 @@ async function queryWithDegradedFallback(params) {
4218
4684
  };
4219
4685
  }
4220
4686
  }
4687
+ function collectCodeFiles(rootPath, allowedExts, maxFiles) {
4688
+ const files = [];
4689
+ function collect(dir) {
4690
+ if (files.length >= maxFiles) return;
4691
+ let entries;
4692
+ try {
4693
+ entries = readdirSync(dir, { withFileTypes: true });
4694
+ } catch {
4695
+ return;
4696
+ }
4697
+ for (const entry of entries) {
4698
+ if (files.length >= maxFiles) break;
4699
+ if (SKIP_DIRS.has(entry.name)) continue;
4700
+ const full = join(dir, entry.name);
4701
+ if (entry.isDirectory()) collect(full);
4702
+ else if (entry.isFile()) {
4703
+ const ext = extname(entry.name).replace(".", "");
4704
+ if (allowedExts.has(ext)) files.push(full);
4705
+ }
4706
+ }
4707
+ }
4708
+ collect(rootPath);
4709
+ return files;
4710
+ }
4711
+ function tokenizeQueryForLexicalRescue(query) {
4712
+ const stopWords = /* @__PURE__ */ new Set(["where", "what", "show", "find", "logic", "code", "file", "files", "handled", "handling"]);
4713
+ const rawTokens = query.toLowerCase().split(/[^a-z0-9/._:-]+/).map((token) => token.trim()).filter(Boolean);
4714
+ const expanded = /* @__PURE__ */ new Set();
4715
+ for (const token of rawTokens) {
4716
+ if (token.length >= 3 && !stopWords.has(token)) expanded.add(token);
4717
+ for (const part of token.split(/[/:._-]+/).filter((value) => value.length >= 3 && !stopWords.has(value))) {
4718
+ expanded.add(part);
4719
+ }
4720
+ }
4721
+ return Array.from(expanded);
4722
+ }
4723
+ function buildLexicalRescueResults(args) {
4724
+ const tokens = tokenizeQueryForLexicalRescue(args.query);
4725
+ if (tokens.length === 0) return [];
4726
+ const scored = args.documents.map((doc) => {
4727
+ const haystack = `${doc.id}
4728
+ ${doc.content}
4729
+ ${doc.raw_content}`.toLowerCase();
4730
+ const tokenHits = tokens.filter((token) => haystack.includes(token));
4731
+ const uniqueHits = tokenHits.length;
4732
+ const exactPhraseBonus = haystack.includes(args.query.toLowerCase()) ? 0.25 : 0;
4733
+ const score = uniqueHits === 0 ? 0 : Math.min(0.95, uniqueHits / tokens.length + exactPhraseBonus);
4734
+ return {
4735
+ id: doc.id,
4736
+ score,
4737
+ content: doc.content,
4738
+ snippet: doc.content.split("\n").find((line) => line.trim().length > 10)?.slice(0, 200) || doc.id,
4739
+ search_mode: "lexical_rescue"
4740
+ };
4741
+ });
4742
+ return scored.filter((result) => result.score > 0).sort((a, b) => b.score - a.score).slice(0, args.top_k);
4743
+ }
4744
+ async function runSearchCodeSearch(args) {
4745
+ const topK = args.top_k ?? 10;
4746
+ const requestedThreshold = args.threshold ?? 0.2;
4747
+ const semanticTopK = Math.min(args.documents.length || topK, Math.max(topK * 3, topK + 5));
4748
+ const semanticDocuments = args.documents.map((doc) => ({ id: doc.id, content: doc.content }));
4749
+ const defaultResponse = await args.semantic_search({
4750
+ query: args.query,
4751
+ documents: semanticDocuments,
4752
+ top_k: semanticTopK,
4753
+ threshold: requestedThreshold
4754
+ });
4755
+ if (defaultResponse.results?.length) {
4756
+ return {
4757
+ results: defaultResponse.results.slice(0, topK).map((result) => ({ ...result, search_mode: "semantic" })),
4758
+ mode: "semantic",
4759
+ threshold_used: requestedThreshold,
4760
+ fallback_used: false,
4761
+ fallback_reason: null
4762
+ };
4763
+ }
4764
+ const adaptiveThreshold = Math.max(0.05, Math.min(requestedThreshold, requestedThreshold / 2));
4765
+ if (adaptiveThreshold < requestedThreshold) {
4766
+ const adaptiveResponse = await args.semantic_search({
4767
+ query: args.query,
4768
+ documents: semanticDocuments,
4769
+ top_k: semanticTopK,
4770
+ threshold: adaptiveThreshold
4771
+ });
4772
+ if (adaptiveResponse.results?.length) {
4773
+ return {
4774
+ results: adaptiveResponse.results.slice(0, topK).map((result) => ({ ...result, search_mode: "adaptive_semantic" })),
4775
+ mode: "adaptive_semantic",
4776
+ threshold_used: adaptiveThreshold,
4777
+ fallback_used: true,
4778
+ fallback_reason: `No results at threshold ${requestedThreshold}; lowered to ${adaptiveThreshold}.`
4779
+ };
4780
+ }
4781
+ }
4782
+ const lexicalResults = buildLexicalRescueResults({
4783
+ query: args.query,
4784
+ documents: args.documents,
4785
+ top_k: topK
4786
+ });
4787
+ return {
4788
+ results: lexicalResults,
4789
+ mode: "lexical_rescue",
4790
+ threshold_used: null,
4791
+ fallback_used: true,
4792
+ fallback_reason: lexicalResults.length ? "Semantic ranking returned no matches; lexical rescue over local file paths and content was used." : "Semantic ranking returned no matches and lexical rescue found no strong candidates."
4793
+ };
4794
+ }
4795
+ function buildLocalWorkspaceDocuments(rootPath, allowedExts, maxFiles) {
4796
+ const files = collectCodeFiles(rootPath, allowedExts, maxFiles);
4797
+ const documents = [];
4798
+ for (const filePath of files) {
4799
+ try {
4800
+ const stat = statSync(filePath);
4801
+ if (stat.size > 500 * 1024) continue;
4802
+ const content = readFileSync(filePath, "utf-8");
4803
+ const relPath = relative(rootPath, filePath).replace(/\\/g, "/");
4804
+ documents.push({ id: relPath, content: extractSignature(relPath, content), raw_content: content });
4805
+ } catch {
4806
+ }
4807
+ }
4808
+ return documents;
4809
+ }
4810
+ function extractRelevantSnippet(rawContent, query) {
4811
+ const lines = rawContent.split("\n");
4812
+ const tokens = tokenizeQueryForLexicalRescue(query);
4813
+ let matchIndex = -1;
4814
+ for (let index = 0; index < lines.length; index += 1) {
4815
+ const lower = lines[index].toLowerCase();
4816
+ if (tokens.some((token) => lower.includes(token))) {
4817
+ matchIndex = index;
4818
+ break;
4819
+ }
4820
+ }
4821
+ if (matchIndex < 0) {
4822
+ matchIndex = lines.findIndex((line) => line.trim().length > 0);
4823
+ }
4824
+ const start = Math.max(0, matchIndex < 0 ? 0 : matchIndex - 1);
4825
+ const end = Math.min(lines.length, start + 4);
4826
+ const snippet = lines.slice(start, end).join("\n").trim().slice(0, 500);
4827
+ return {
4828
+ snippet,
4829
+ line_start: start + 1,
4830
+ ...end > start + 1 ? { line_end: end } : {}
4831
+ };
4832
+ }
4833
+ async function runLocalWorkspaceRetrieval(args) {
4834
+ const identity = resolveWorkspaceIdentity({ path: args.path });
4835
+ const workspace = await resolveWorkspaceTrust({ path: identity.root_path, project: args.project });
4836
+ const allowedExts = args.file_types ? new Set(args.file_types) : CODE_EXTENSIONS;
4837
+ const candidateFiles = collectCodeFiles(identity.root_path, allowedExts, args.max_files ?? 150);
4838
+ const documents = buildLocalWorkspaceDocuments(identity.root_path, allowedExts, args.max_files ?? 150);
4839
+ const sharedWarnings = [...workspace.warnings];
4840
+ const searchResult = await runSearchCodeSearch({
4841
+ query: args.query,
4842
+ documents,
4843
+ top_k: args.top_k,
4844
+ threshold: args.threshold,
4845
+ semantic_search: (params) => whisper.semanticSearch(params)
4846
+ });
4847
+ if (searchResult.mode === "lexical_rescue") {
4848
+ sharedWarnings.push("Local lexical rescue was used because semantic search returned no matches.");
4849
+ } else if (searchResult.mode === "adaptive_semantic") {
4850
+ sharedWarnings.push(searchResult.fallback_reason || "Adaptive semantic thresholding was used.");
4851
+ }
4852
+ if (!workspace.grounded_to_workspace && workspace.health !== "unbound" && workspace.health !== "unindexed") {
4853
+ sharedWarnings.push("Local code search is live, but project-backed retrieval may disagree until the workspace is re-indexed.");
4854
+ }
4855
+ const documentMap = new Map(documents.map((document) => [document.id, document]));
4856
+ const localResults = searchResult.results.map((result) => {
4857
+ const document = documentMap.get(result.id);
4858
+ const snippet = extractRelevantSnippet(document?.raw_content || result.snippet || "", args.query);
4859
+ return {
4860
+ ...result,
4861
+ snippet: snippet.snippet || result.snippet,
4862
+ raw_snippet: snippet.snippet || result.snippet,
4863
+ line_start: snippet.line_start,
4864
+ ...snippet.line_end ? { line_end: snippet.line_end } : {},
4865
+ retrieval_method: result.search_mode === "lexical_rescue" ? "lexical" : "semantic"
4866
+ };
4867
+ });
4868
+ return {
4869
+ workspace,
4870
+ documents,
4871
+ results: localResults,
4872
+ warnings: sharedWarnings,
4873
+ diagnostics: {
4874
+ workspace_id: workspace.workspace_id,
4875
+ root_path: workspace.root_path,
4876
+ project_ref: workspace.project_ref,
4877
+ project_id: workspace.project_id,
4878
+ identity_source: workspace.identity_source,
4879
+ index_health: workspace.health,
4880
+ grounded_to_workspace: true,
4881
+ threshold_requested: args.threshold ?? 0.2,
4882
+ threshold_used: searchResult.threshold_used,
4883
+ fallback_used: searchResult.fallback_used,
4884
+ fallback_reason: searchResult.fallback_reason,
4885
+ search_mode: searchResult.mode,
4886
+ candidate_files_scanned: candidateFiles.length,
4887
+ semantic_candidate_count: documents.length,
4888
+ retrieval_route: "local_workspace",
4889
+ warnings: sharedWarnings
4890
+ }
4891
+ };
4892
+ }
4893
+ function isLikelyRepoBackedResult(result) {
4894
+ const metadata = result.metadata || {};
4895
+ const retrievalSource = String(result.retrieval_source || "").toLowerCase();
4896
+ if (retrievalSource.includes("memory")) return false;
4897
+ const pathCandidate = String(metadata.file_path || metadata.path || result.source || result.document || "");
4898
+ if (/[\\/]/.test(pathCandidate) || /\.[a-z0-9]+$/i.test(pathCandidate)) return true;
4899
+ const sourceType = String(metadata.source_type || metadata.connector_type || result.type || "").toLowerCase();
4900
+ return ["local", "github", "code", "file", "repo"].some((token) => sourceType.includes(token));
4901
+ }
4902
+ function filterProjectRepoResults(results, matchedSourceIds) {
4903
+ const scoped = results.filter((result) => {
4904
+ const sourceId = String(result.metadata?.source_id || result.metadata?.sourceId || result.metadata?.source || "");
4905
+ return matchedSourceIds.length === 0 || matchedSourceIds.includes(sourceId);
4906
+ });
4907
+ const repoResults = scoped.filter((result) => isLikelyRepoBackedResult(result));
4908
+ return repoResults.length > 0 ? repoResults : scoped;
4909
+ }
4910
+ function localHitsToEvidence(workspaceId, hits) {
4911
+ return hits.map(
4912
+ (hit) => toEvidenceRef(
4913
+ {
4914
+ id: hit.id,
4915
+ content: hit.raw_snippet,
4916
+ score: hit.score,
4917
+ retrieval_source: hit.retrieval_method,
4918
+ metadata: {
4919
+ file_path: hit.id,
4920
+ snippet: hit.raw_snippet,
4921
+ line_start: hit.line_start,
4922
+ ...hit.line_end ? { line_end: hit.line_end } : {}
4923
+ }
4924
+ },
4925
+ workspaceId,
4926
+ hit.retrieval_method
4927
+ )
4928
+ );
4929
+ }
4930
+ function renderLocalWorkspaceContext(args) {
4931
+ const lines = args.hits.map((hit, index) => {
4932
+ const location = hit.line_end && hit.line_end !== hit.line_start ? `${hit.id}:${hit.line_start}-${hit.line_end}` : `${hit.id}:${hit.line_start}`;
4933
+ return `${index + 1}. [${location}, score: ${hit.score.toFixed(2)}, mode: ${hit.search_mode}] ${hit.raw_snippet || hit.snippet || hit.id}`;
4934
+ });
4935
+ const header = `Found ${args.hits.length} local workspace result(s) (route=${args.route}, workspace=${args.diagnostics.workspace_id}, project=${args.project_ref || "none"}, user=${args.scope.userId}, session=${args.scope.sessionId}):`;
4936
+ const warnings = args.warnings.length ? `
4937
+
4938
+ [warnings]
4939
+ ${args.warnings.join("\n")}` : "";
4940
+ return `${header}
4941
+
4942
+ ${lines.join("\n\n")}${warnings}`;
4943
+ }
4944
+ async function runSearchCodeTool(args) {
4945
+ const rootPath = canonicalizeWorkspacePath(args.path);
4946
+ const allowedExts = args.file_types ? new Set(args.file_types) : CODE_EXTENSIONS;
4947
+ const files = collectCodeFiles(rootPath, allowedExts, args.max_files ?? 150);
4948
+ if (files.length === 0) {
4949
+ const workspace = await resolveWorkspaceTrust({ path: rootPath });
4950
+ const sharedWarnings = [...workspace.warnings];
4951
+ return {
4952
+ tool: "search_code",
4953
+ query: args.query,
4954
+ path: rootPath,
4955
+ results: [],
4956
+ count: 0,
4957
+ warnings: sharedWarnings,
4958
+ diagnostics: {
4959
+ workspace_id: workspace.workspace_id,
4960
+ root_path: workspace.root_path,
4961
+ project_ref: workspace.project_ref,
4962
+ project_id: workspace.project_id,
4963
+ identity_source: workspace.identity_source,
4964
+ index_health: workspace.health,
4965
+ grounded_to_workspace: workspace.grounded_to_workspace,
4966
+ threshold_requested: args.threshold ?? 0.2,
4967
+ threshold_used: null,
4968
+ fallback_used: false,
4969
+ fallback_reason: null,
4970
+ search_mode: "semantic",
4971
+ candidate_files_scanned: 0,
4972
+ semantic_candidate_count: 0,
4973
+ retrieval_route: "local_workspace",
4974
+ warnings: sharedWarnings
4975
+ }
4976
+ };
4977
+ }
4978
+ const localRetrieval = await runLocalWorkspaceRetrieval(args);
4979
+ return {
4980
+ tool: "search_code",
4981
+ query: args.query,
4982
+ path: rootPath,
4983
+ results: localRetrieval.results,
4984
+ count: localRetrieval.results.length,
4985
+ warnings: localRetrieval.warnings,
4986
+ diagnostics: localRetrieval.diagnostics
4987
+ };
4988
+ }
4221
4989
  function getLocalAllowlistRoots() {
4222
4990
  const fromEnv = (process.env.WHISPER_LOCAL_ALLOWLIST || "").split(",").map((v) => v.trim()).filter(Boolean);
4223
4991
  if (fromEnv.length > 0) return fromEnv;
@@ -4297,7 +5065,8 @@ async function ingestLocalPath(params) {
4297
5065
  }
4298
5066
  collect(rootPath);
4299
5067
  const manifest = loadIngestManifest();
4300
- const workspaceId = getWorkspaceIdForPath(rootPath);
5068
+ const canonicalRootPath = canonicalizeWorkspacePath(rootPath);
5069
+ const workspaceId = getWorkspaceIdForPath(canonicalRootPath);
4301
5070
  if (!manifest[workspaceId]) manifest[workspaceId] = { last_run_at: (/* @__PURE__ */ new Date(0)).toISOString(), files: {} };
4302
5071
  const docs = [];
4303
5072
  const skipped = [];
@@ -4333,6 +5102,10 @@ async function ingestLocalPath(params) {
4333
5102
  }
4334
5103
  manifest[workspaceId].last_run_at = (/* @__PURE__ */ new Date()).toISOString();
4335
5104
  saveIngestManifest(manifest);
5105
+ const state = loadState();
5106
+ const workspace = getWorkspaceState(state, workspaceId);
5107
+ workspace.root_path = canonicalRootPath;
5108
+ saveState(state);
4336
5109
  appendFileSync(
4337
5110
  AUDIT_LOG_PATH,
4338
5111
  `${(/* @__PURE__ */ new Date()).toISOString()} local_ingest workspace=${workspaceId} root_hash=${createHash("sha256").update(rootPath).digest("hex").slice(0, 16)} files=${docs.length}
@@ -4488,22 +5261,27 @@ server.tool(
4488
5261
  },
4489
5262
  async ({ path, workspace_id, project }) => {
4490
5263
  try {
4491
- const workspaceId = getWorkspaceIdForPath(path, workspace_id);
5264
+ const identity = resolveWorkspaceIdentity({ path, workspace_id });
4492
5265
  const state = loadState();
4493
- const existed = Boolean(state.workspaces[workspaceId]);
4494
- const workspace = getWorkspaceState(state, workspaceId);
4495
- const resolvedProject = await resolveProjectRef(project);
4496
- const resolvedBy = project?.trim() ? "explicit_project" : DEFAULT_PROJECT ? "env_default" : resolvedProject ? "auto_first_project" : "unresolved";
4497
- saveState(state);
5266
+ const existed = Boolean(state.workspaces[identity.workspace_id]);
5267
+ const trust = await resolveWorkspaceTrust({ path: identity.root_path, workspace_id, project });
5268
+ const preflight = await resolveRepoGroundingPreflight({ query: "workspace status", path, workspace_id, project });
4498
5269
  const payload = {
4499
- workspace_id: workspaceId,
4500
- project_id: resolvedProject || null,
5270
+ workspace_id: identity.workspace_id,
5271
+ root_path: trust.root_path,
5272
+ identity_source: trust.identity_source,
5273
+ project_ref: trust.project_ref,
5274
+ project_id: trust.project_id,
4501
5275
  created: !existed,
4502
- resolved_by: resolvedBy,
5276
+ resolved_by: trust.resolved_by,
5277
+ health: trust.health,
5278
+ retrieval_readiness: preflight.retrieval_readiness,
5279
+ warnings: preflight.warnings,
5280
+ recommended_next_calls: preflight.recommended_next_calls,
4503
5281
  index_state: {
4504
- last_indexed_at: workspace.index_metadata?.last_indexed_at || null,
4505
- last_indexed_commit: workspace.index_metadata?.last_indexed_commit || null,
4506
- coverage: workspace.index_metadata?.coverage ?? 0
5282
+ last_indexed_at: trust.freshness.last_indexed_at,
5283
+ last_indexed_commit: trust.last_indexed_commit,
5284
+ coverage: trust.coverage
4507
5285
  }
4508
5286
  };
4509
5287
  return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
@@ -4521,24 +5299,30 @@ server.tool(
4521
5299
  },
4522
5300
  async ({ workspace_id, path }) => {
4523
5301
  try {
4524
- const rootPath = path || process.cwd();
4525
- const workspaceId = getWorkspaceIdForPath(rootPath, workspace_id);
4526
- const state = loadState();
4527
- const workspace = getWorkspaceState(state, workspaceId);
4528
- const lastIndexedAt = workspace.index_metadata?.last_indexed_at;
4529
- const ageHours = lastIndexedAt ? (Date.now() - new Date(lastIndexedAt).getTime()) / (60 * 60 * 1e3) : null;
4530
- const stale = ageHours === null ? true : ageHours > 168;
5302
+ const trust = await resolveWorkspaceTrust({ path, workspace_id });
5303
+ const repoVerification = await inspectProjectRepoSources({ project_ref: trust.project_ref, root_path: trust.root_path });
4531
5304
  const payload = {
4532
- workspace_id: workspaceId,
4533
- freshness: {
4534
- stale,
4535
- age_hours: ageHours,
4536
- last_indexed_at: lastIndexedAt || null
4537
- },
4538
- coverage: workspace.index_metadata?.coverage ?? 0,
4539
- last_indexed_commit: workspace.index_metadata?.last_indexed_commit || null,
4540
- current_commit: getGitHead(rootPath) || null,
4541
- pending_changes: getGitPendingCount(rootPath)
5305
+ workspace_id: trust.workspace_id,
5306
+ root_path: trust.root_path,
5307
+ identity_source: trust.identity_source,
5308
+ project_ref: trust.project_ref,
5309
+ project_id: trust.project_id,
5310
+ resolved_by: trust.resolved_by,
5311
+ health: trust.health,
5312
+ retrieval_readiness: repoVerification.retrieval_readiness,
5313
+ warnings: Array.from(/* @__PURE__ */ new Set([...trust.warnings, ...repoVerification.warnings])),
5314
+ recommended_next_calls: Array.from(
5315
+ /* @__PURE__ */ new Set([
5316
+ ...trust.recommended_next_calls,
5317
+ ...repoVerification.retrieval_readiness === "project_bound_no_repo_source" ? ["context.list_sources", "index.local_scan_ingest"] : []
5318
+ ])
5319
+ ),
5320
+ freshness: trust.freshness,
5321
+ coverage: trust.coverage,
5322
+ last_indexed_commit: trust.last_indexed_commit,
5323
+ current_commit: trust.current_commit,
5324
+ pending_changes: trust.pending_changes,
5325
+ grounded_to_workspace: trust.grounded_to_workspace
4542
5326
  };
4543
5327
  return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
4544
5328
  } catch (error) {
@@ -4557,13 +5341,15 @@ server.tool(
4557
5341
  },
4558
5342
  async ({ workspace_id, path, mode, max_files }) => {
4559
5343
  try {
4560
- const rootPath = path || process.cwd();
4561
- const workspaceId = getWorkspaceIdForPath(rootPath, workspace_id);
5344
+ const identity = resolveWorkspaceIdentity({ path, workspace_id });
5345
+ const rootPath = identity.root_path;
5346
+ const workspaceId = identity.workspace_id;
4562
5347
  const state = loadState();
4563
5348
  const workspace = getWorkspaceState(state, workspaceId);
4564
5349
  const fileStats = countCodeFiles(rootPath, max_files);
4565
5350
  const coverage = Math.max(0, Math.min(1, fileStats.total / Math.max(1, max_files)));
4566
5351
  const now = (/* @__PURE__ */ new Date()).toISOString();
5352
+ workspace.root_path = rootPath;
4567
5353
  workspace.index_metadata = {
4568
5354
  last_indexed_at: now,
4569
5355
  last_indexed_commit: getGitHead(rootPath),
@@ -4572,6 +5358,7 @@ server.tool(
4572
5358
  saveState(state);
4573
5359
  const payload = {
4574
5360
  workspace_id: workspaceId,
5361
+ root_path: rootPath,
4575
5362
  mode,
4576
5363
  indexed_files: fileStats.total,
4577
5364
  skipped_files: fileStats.skipped,
@@ -4623,9 +5410,10 @@ server.tool(
4623
5410
  );
4624
5411
  server.tool(
4625
5412
  "context.get_relevant",
4626
- "Core retrieval. Task goes in, ranked context chunks come out with structured evidence (file:line ready).",
5413
+ "Default grounded retrieval step for workspace/project questions. Call this before answering when you need ranked evidence with file:line citations instead of relying on model memory.",
4627
5414
  {
4628
5415
  question: z.string().describe("Task/question to retrieve context for"),
5416
+ path: z.string().optional().describe("Workspace path. Defaults to current working directory."),
4629
5417
  workspace_id: z.string().optional(),
4630
5418
  project: z.string().optional(),
4631
5419
  top_k: z.number().optional().default(12),
@@ -4634,45 +5422,115 @@ server.tool(
4634
5422
  session_id: z.string().optional(),
4635
5423
  user_id: z.string().optional()
4636
5424
  },
4637
- async ({ question, workspace_id, project, top_k, include_memories, include_graph, session_id, user_id }) => {
5425
+ async ({ question, path, workspace_id, project, top_k, include_memories, include_graph, session_id, user_id }) => {
4638
5426
  try {
4639
- const workspaceId = getWorkspaceId(workspace_id);
4640
- const resolvedProject = await resolveProjectRef(project);
4641
- if (!resolvedProject) {
5427
+ const preflight = await resolveRepoGroundingPreflight({ query: question, path, workspace_id, project });
5428
+ if (preflight.retrieval_route === "local_workspace_fallback") {
5429
+ const localRetrieval = await runLocalWorkspaceRetrieval({
5430
+ query: question,
5431
+ path: preflight.trust_state.root_path,
5432
+ project: preflight.trust_state.project_ref || project,
5433
+ top_k
5434
+ });
5435
+ const evidence2 = localHitsToEvidence(preflight.trust_state.workspace_id, localRetrieval.results);
5436
+ const payload2 = {
5437
+ question,
5438
+ workspace_id: preflight.trust_state.workspace_id,
5439
+ root_path: preflight.trust_state.root_path,
5440
+ identity_source: preflight.trust_state.identity_source,
5441
+ trust_state: preflight.trust_state,
5442
+ retrieval_readiness: preflight.retrieval_readiness,
5443
+ retrieval_route: preflight.retrieval_route,
5444
+ grounded_to_workspace: true,
5445
+ total_results: evidence2.length,
5446
+ context: evidence2.map((item) => `[${renderCitation(item)}] ${item.snippet || "Relevant local workspace context found."}`).join("\n"),
5447
+ evidence: evidence2,
5448
+ used_context_ids: evidence2.map((item) => item.source_id),
5449
+ latency_ms: 0,
5450
+ warnings: Array.from(/* @__PURE__ */ new Set([...preflight.warnings, ...localRetrieval.warnings])),
5451
+ recommended_next_calls: preflight.recommended_next_calls
5452
+ };
5453
+ return { content: [{ type: "text", text: JSON.stringify(payload2, null, 2) }] };
5454
+ }
5455
+ if (!preflight.trust_state.project_ref) {
4642
5456
  const payload2 = {
4643
5457
  question,
4644
- workspace_id: workspaceId,
5458
+ workspace_id: preflight.trust_state.workspace_id,
5459
+ root_path: preflight.trust_state.root_path,
5460
+ identity_source: preflight.trust_state.identity_source,
5461
+ trust_state: preflight.trust_state,
5462
+ retrieval_readiness: preflight.retrieval_readiness,
5463
+ retrieval_route: "none",
5464
+ grounded_to_workspace: false,
4645
5465
  total_results: 0,
4646
5466
  context: "",
4647
5467
  evidence: [],
4648
5468
  used_context_ids: [],
4649
5469
  latency_ms: 0,
4650
- warning: "No project resolved. Set WHISPER_PROJECT or create one in your account."
5470
+ warnings: preflight.warnings,
5471
+ recommended_next_calls: preflight.recommended_next_calls
4651
5472
  };
4652
5473
  return { content: [{ type: "text", text: JSON.stringify(payload2, null, 2) }] };
4653
5474
  }
4654
5475
  const queryResult = await queryWithDegradedFallback({
4655
- project: resolvedProject,
5476
+ project: preflight.trust_state.project_ref,
4656
5477
  query: question,
4657
5478
  top_k,
4658
- include_memories,
5479
+ include_memories: preflight.repo_grounded ? false : include_memories,
4659
5480
  include_graph,
4660
5481
  session_id,
4661
- user_id
5482
+ user_id,
5483
+ source_ids: preflight.repo_grounded ? preflight.matched_source_ids : void 0
4662
5484
  });
4663
5485
  const response = queryResult.response;
4664
- const evidence = (response.results || []).map((r) => toEvidenceRef(r, workspaceId, "semantic"));
5486
+ const rawResults = preflight.repo_grounded ? filterProjectRepoResults(response.results || [], preflight.matched_source_ids) : response.results || [];
5487
+ if (preflight.repo_grounded && rawResults.length === 0) {
5488
+ const localRetrieval = await runLocalWorkspaceRetrieval({
5489
+ query: question,
5490
+ path: preflight.trust_state.root_path,
5491
+ project: preflight.trust_state.project_ref,
5492
+ top_k
5493
+ });
5494
+ const evidence2 = localHitsToEvidence(preflight.trust_state.workspace_id, localRetrieval.results);
5495
+ const payload2 = {
5496
+ question,
5497
+ workspace_id: preflight.trust_state.workspace_id,
5498
+ root_path: preflight.trust_state.root_path,
5499
+ identity_source: preflight.trust_state.identity_source,
5500
+ trust_state: preflight.trust_state,
5501
+ retrieval_readiness: "local_fallback",
5502
+ retrieval_route: "local_workspace_fallback",
5503
+ grounded_to_workspace: true,
5504
+ total_results: evidence2.length,
5505
+ context: evidence2.map((item) => `[${renderCitation(item)}] ${item.snippet || "Relevant local workspace context found."}`).join("\n"),
5506
+ evidence: evidence2,
5507
+ used_context_ids: evidence2.map((item) => item.source_id),
5508
+ latency_ms: 0,
5509
+ warnings: Array.from(/* @__PURE__ */ new Set([...preflight.warnings, ...localRetrieval.warnings])),
5510
+ recommended_next_calls: preflight.recommended_next_calls
5511
+ };
5512
+ return { content: [{ type: "text", text: JSON.stringify(payload2, null, 2) }] };
5513
+ }
5514
+ const evidence = rawResults.map((r) => toEvidenceRef(r, preflight.trust_state.workspace_id, "semantic"));
4665
5515
  const payload = {
4666
5516
  question,
4667
- workspace_id: workspaceId,
4668
- total_results: response.meta?.total || evidence.length,
5517
+ workspace_id: preflight.trust_state.workspace_id,
5518
+ root_path: preflight.trust_state.root_path,
5519
+ identity_source: preflight.trust_state.identity_source,
5520
+ trust_state: preflight.trust_state,
5521
+ retrieval_readiness: preflight.retrieval_readiness,
5522
+ retrieval_route: preflight.repo_grounded ? "project_repo" : "none",
5523
+ grounded_to_workspace: preflight.repo_grounded ? true : preflight.trust_state.grounded_to_workspace,
5524
+ total_results: rawResults.length || response.meta?.total || evidence.length,
4669
5525
  context: response.context || "",
4670
5526
  evidence,
4671
- used_context_ids: (response.results || []).map((r) => String(r.id)),
5527
+ used_context_ids: rawResults.map((r) => String(r.id)),
4672
5528
  latency_ms: response.meta?.latency_ms || 0,
4673
5529
  degraded_mode: queryResult.degraded_mode,
4674
5530
  degraded_reason: queryResult.degraded_reason,
4675
- recommendation: queryResult.recommendation
5531
+ recommendation: queryResult.recommendation,
5532
+ warnings: preflight.warnings,
5533
+ recommended_next_calls: preflight.recommended_next_calls
4676
5534
  };
4677
5535
  return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
4678
5536
  } catch (error) {
@@ -4685,36 +5543,53 @@ server.tool(
4685
5543
  "Verify whether a claim is supported by retrieved context. Returns supported/partial/unsupported with evidence.",
4686
5544
  {
4687
5545
  claim: z.string().describe("Claim to verify"),
5546
+ path: z.string().optional().describe("Workspace path. Defaults to current working directory."),
4688
5547
  workspace_id: z.string().optional(),
4689
5548
  project: z.string().optional(),
4690
5549
  context_ids: z.array(z.string()).optional(),
4691
5550
  strict: z.boolean().optional().default(true)
4692
5551
  },
4693
- async ({ claim, workspace_id, project, context_ids, strict }) => {
5552
+ async ({ claim, path, workspace_id, project, context_ids, strict }) => {
4694
5553
  try {
4695
- const workspaceId = getWorkspaceId(workspace_id);
4696
- const resolvedProject = await resolveProjectRef(project);
4697
- if (!resolvedProject) {
5554
+ const preflight = await resolveRepoGroundingPreflight({ query: claim, path, workspace_id, project });
5555
+ let evidence = [];
5556
+ if (preflight.retrieval_route === "local_workspace_fallback") {
5557
+ const localRetrieval = await runLocalWorkspaceRetrieval({
5558
+ query: claim,
5559
+ path: preflight.trust_state.root_path,
5560
+ project: preflight.trust_state.project_ref || project,
5561
+ top_k: strict ? 8 : 12
5562
+ });
5563
+ evidence = localHitsToEvidence(preflight.trust_state.workspace_id, localRetrieval.results).filter((item) => !context_ids || context_ids.length === 0 || context_ids.includes(item.source_id));
5564
+ } else if (preflight.trust_state.project_ref) {
5565
+ const response = await whisper.query({
5566
+ project: preflight.trust_state.project_ref,
5567
+ query: claim,
5568
+ top_k: strict ? 8 : 12,
5569
+ include_memories: preflight.repo_grounded ? false : true,
5570
+ include_graph: true,
5571
+ ...preflight.repo_grounded ? { source_ids: preflight.matched_source_ids } : {}
5572
+ });
5573
+ const filtered = (preflight.repo_grounded ? filterProjectRepoResults(response.results || [], preflight.matched_source_ids) : response.results || []).filter(
5574
+ (r) => !context_ids || context_ids.length === 0 || context_ids.includes(String(r.id))
5575
+ );
5576
+ evidence = filtered.map((r) => toEvidenceRef(r, preflight.trust_state.workspace_id, "semantic"));
5577
+ }
5578
+ if (evidence.length === 0) {
4698
5579
  const payload2 = {
4699
5580
  verdict: "unsupported",
4700
5581
  confidence: 0,
4701
5582
  evidence: [],
4702
- missing_requirements: ["No project resolved. Set WHISPER_PROJECT or create one in your account."],
4703
- explanation: "Verifier could not run because no project is configured."
5583
+ trust_state: preflight.trust_state,
5584
+ retrieval_readiness: preflight.retrieval_readiness,
5585
+ retrieval_route: preflight.retrieval_route,
5586
+ warnings: preflight.warnings,
5587
+ recommended_next_calls: preflight.recommended_next_calls,
5588
+ missing_requirements: preflight.warnings.length ? preflight.warnings : ["No repo-grounded evidence was available for verification."],
5589
+ explanation: "Verifier did not find sufficient repo-grounded evidence."
4704
5590
  };
4705
5591
  return { content: [{ type: "text", text: JSON.stringify(payload2, null, 2) }] };
4706
5592
  }
4707
- const response = await whisper.query({
4708
- project: resolvedProject,
4709
- query: claim,
4710
- top_k: strict ? 8 : 12,
4711
- include_memories: true,
4712
- include_graph: true
4713
- });
4714
- const filtered = (response.results || []).filter(
4715
- (r) => !context_ids || context_ids.length === 0 || context_ids.includes(String(r.id))
4716
- );
4717
- const evidence = filtered.map((r) => toEvidenceRef(r, workspaceId, "semantic"));
4718
5593
  const directEvidence = evidence.filter((e) => e.score >= (strict ? 0.7 : 0.6));
4719
5594
  const weakEvidence = evidence.filter((e) => e.score >= (strict ? 0.45 : 0.35));
4720
5595
  let verdict = "unsupported";
@@ -4724,6 +5599,11 @@ server.tool(
4724
5599
  verdict,
4725
5600
  confidence: evidence.length ? Math.max(...evidence.map((e) => e.score)) : 0,
4726
5601
  evidence: verdict === "supported" ? directEvidence : weakEvidence,
5602
+ trust_state: preflight.trust_state,
5603
+ retrieval_readiness: preflight.retrieval_readiness,
5604
+ retrieval_route: preflight.retrieval_route,
5605
+ warnings: preflight.warnings,
5606
+ recommended_next_calls: preflight.recommended_next_calls,
4727
5607
  missing_requirements: verdict === "supported" ? [] : verdict === "partial" ? ["No direct evidence spans met strict threshold."] : ["No sufficient supporting evidence found for the claim."],
4728
5608
  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."
4729
5609
  };
@@ -4738,6 +5618,7 @@ server.tool(
4738
5618
  "Answer a question only when evidence requirements are met. Fails closed with an abstain payload when not verifiable.",
4739
5619
  {
4740
5620
  question: z.string(),
5621
+ path: z.string().optional().describe("Workspace path. Defaults to current working directory."),
4741
5622
  workspace_id: z.string().optional(),
4742
5623
  project: z.string().optional(),
4743
5624
  constraints: z.object({
@@ -4752,41 +5633,53 @@ server.tool(
4752
5633
  include_recent_decisions: z.boolean().optional().default(true)
4753
5634
  }).optional()
4754
5635
  },
4755
- async ({ question, workspace_id, project, constraints, retrieval }) => {
5636
+ async ({ question, path, workspace_id, project, constraints, retrieval }) => {
4756
5637
  try {
4757
- const workspaceId = getWorkspaceId(workspace_id);
4758
5638
  const requireCitations = constraints?.require_citations ?? true;
4759
5639
  const minEvidenceItems = constraints?.min_evidence_items ?? 2;
4760
5640
  const minConfidence = constraints?.min_confidence ?? 0.65;
4761
5641
  const maxStalenessHours = constraints?.max_staleness_hours ?? 168;
4762
5642
  const topK = retrieval?.top_k ?? 12;
4763
- const resolvedProject = await resolveProjectRef(project);
4764
- if (!resolvedProject) {
5643
+ const preflight = await resolveRepoGroundingPreflight({ query: question, path, workspace_id, project });
5644
+ let evidence = [];
5645
+ if (preflight.retrieval_route === "local_workspace_fallback") {
5646
+ const localRetrieval = await runLocalWorkspaceRetrieval({
5647
+ query: question,
5648
+ path: preflight.trust_state.root_path,
5649
+ project: preflight.trust_state.project_ref || project,
5650
+ top_k: topK
5651
+ });
5652
+ evidence = localHitsToEvidence(preflight.trust_state.workspace_id, localRetrieval.results);
5653
+ } else if (preflight.trust_state.project_ref) {
5654
+ const response = await whisper.query({
5655
+ project: preflight.trust_state.project_ref,
5656
+ query: question,
5657
+ top_k: topK,
5658
+ include_memories: preflight.repo_grounded ? false : true,
5659
+ include_graph: true,
5660
+ ...preflight.repo_grounded ? { source_ids: preflight.matched_source_ids } : {}
5661
+ });
5662
+ const filtered = preflight.repo_grounded ? filterProjectRepoResults(response.results || [], preflight.matched_source_ids) : response.results || [];
5663
+ evidence = filtered.map((r) => toEvidenceRef(r, preflight.trust_state.workspace_id, "semantic"));
5664
+ }
5665
+ if (evidence.length === 0) {
5666
+ const reason = preflight.trust_state.health === "stale" || preflight.trust_state.health === "drifted" ? "stale_index" : "no_retrieval_hits";
4765
5667
  const abstain = buildAbstain({
4766
- reason: "no_retrieval_hits",
4767
- message: "No project resolved. Set WHISPER_PROJECT or create one in your account.",
5668
+ reason,
5669
+ message: preflight.warnings[0] || "Workspace trust requirements were not met.",
4768
5670
  closest_evidence: [],
4769
5671
  claims_evaluated: 1,
4770
5672
  evidence_items_found: 0,
4771
5673
  min_required: minEvidenceItems,
4772
- index_fresh: true
5674
+ index_fresh: preflight.trust_state.health === "healthy",
5675
+ warnings: preflight.warnings,
5676
+ trust_state: preflight.trust_state,
5677
+ recommended_next_calls: preflight.recommended_next_calls
4773
5678
  });
4774
5679
  return { content: [{ type: "text", text: JSON.stringify(abstain, null, 2) }] };
4775
5680
  }
4776
- const response = await whisper.query({
4777
- project: resolvedProject,
4778
- query: question,
4779
- top_k: topK,
4780
- include_memories: true,
4781
- include_graph: true
4782
- });
4783
- const evidence = (response.results || []).map((r) => toEvidenceRef(r, workspaceId, "semantic"));
4784
5681
  const sorted = evidence.sort((a, b) => b.score - a.score);
4785
5682
  const confidence = sorted.length ? sorted[0].score : 0;
4786
- const state = loadState();
4787
- const workspace = getWorkspaceState(state, workspaceId);
4788
- const lastIndexedAt = workspace.index_metadata?.last_indexed_at;
4789
- const indexFresh = !lastIndexedAt || Date.now() - new Date(lastIndexedAt).getTime() <= maxStalenessHours * 60 * 60 * 1e3;
4790
5683
  if (sorted.length === 0) {
4791
5684
  const abstain = buildAbstain({
4792
5685
  reason: "no_retrieval_hits",
@@ -4795,24 +5688,15 @@ server.tool(
4795
5688
  claims_evaluated: 1,
4796
5689
  evidence_items_found: 0,
4797
5690
  min_required: minEvidenceItems,
4798
- index_fresh: indexFresh
5691
+ index_fresh: preflight.trust_state.health === "healthy",
5692
+ warnings: preflight.warnings,
5693
+ trust_state: preflight.trust_state,
5694
+ recommended_next_calls: preflight.recommended_next_calls
4799
5695
  });
4800
5696
  return { content: [{ type: "text", text: JSON.stringify(abstain, null, 2) }] };
4801
5697
  }
4802
5698
  const supportedEvidence = sorted.filter((e) => e.score >= minConfidence);
4803
5699
  const verdict = supportedEvidence.length >= 1 ? "supported" : "partial";
4804
- if (!indexFresh) {
4805
- const abstain = buildAbstain({
4806
- reason: "stale_index",
4807
- message: "Index freshness requirement not met. Re-index before answering.",
4808
- closest_evidence: sorted.slice(0, 3),
4809
- claims_evaluated: 1,
4810
- evidence_items_found: supportedEvidence.length,
4811
- min_required: minEvidenceItems,
4812
- index_fresh: false
4813
- });
4814
- return { content: [{ type: "text", text: JSON.stringify(abstain, null, 2) }] };
4815
- }
4816
5700
  if (requireCitations && (verdict !== "supported" || supportedEvidence.length < minEvidenceItems)) {
4817
5701
  const abstain = buildAbstain({
4818
5702
  reason: "insufficient_evidence",
@@ -4821,7 +5705,10 @@ server.tool(
4821
5705
  claims_evaluated: 1,
4822
5706
  evidence_items_found: supportedEvidence.length,
4823
5707
  min_required: minEvidenceItems,
4824
- index_fresh: true
5708
+ index_fresh: true,
5709
+ warnings: preflight.warnings,
5710
+ trust_state: preflight.trust_state,
5711
+ recommended_next_calls: preflight.recommended_next_calls
4825
5712
  });
4826
5713
  return { content: [{ type: "text", text: JSON.stringify(abstain, null, 2) }] };
4827
5714
  }
@@ -4834,6 +5721,11 @@ server.tool(
4834
5721
  answer: answerLines.join("\n"),
4835
5722
  citations,
4836
5723
  confidence,
5724
+ trust_state: preflight.trust_state,
5725
+ retrieval_readiness: preflight.retrieval_readiness,
5726
+ retrieval_route: preflight.retrieval_route,
5727
+ warnings: preflight.warnings,
5728
+ recommended_next_calls: preflight.recommended_next_calls,
4837
5729
  verification: {
4838
5730
  verdict,
4839
5731
  supported_claims: verdict === "supported" ? 1 : 0,
@@ -4849,10 +5741,11 @@ server.tool(
4849
5741
  );
4850
5742
  server.tool(
4851
5743
  "context.query",
4852
- "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.",
5744
+ "Use this when answering from project knowledge rather than general model memory. Retrieves packed context for a query using hybrid vector+keyword search with optional memory/graph expansion.",
4853
5745
  {
4854
5746
  project: z.string().optional().describe("Project name or slug (optional if WHISPER_PROJECT is set)"),
4855
5747
  query: z.string().describe("What are you looking for?"),
5748
+ path: z.string().optional().describe("Workspace path. Defaults to current working directory."),
4856
5749
  top_k: z.number().optional().default(10).describe("Number of results"),
4857
5750
  chunk_types: z.array(z.string()).optional().describe("Filter: code, function, class, documentation, api_spec, schema, config, text"),
4858
5751
  include_memories: z.boolean().optional().describe("Include relevant memories. Omit to use automatic runtime defaults."),
@@ -4861,13 +5754,105 @@ server.tool(
4861
5754
  session_id: z.string().optional().describe("Session ID for memory scoping"),
4862
5755
  max_tokens: z.number().optional().describe("Max tokens for packed context")
4863
5756
  },
4864
- async ({ project, query, top_k, chunk_types, include_memories, include_graph, user_id, session_id, max_tokens }) => {
5757
+ async ({ project, query, path, top_k, chunk_types, include_memories, include_graph, user_id, session_id, max_tokens }) => {
4865
5758
  try {
4866
- const resolvedProject = await resolveProjectRef(project);
4867
- if (!resolvedProject) {
5759
+ const preflight = await resolveRepoGroundingPreflight({ query, path, project, chunk_types });
5760
+ const resolvedProject = preflight.trust_state.project_ref || await resolveProjectRef(project);
5761
+ if (!resolvedProject && !preflight.repo_grounded) {
4868
5762
  return { content: [{ type: "text", text: "Error: No project resolved. Set WHISPER_PROJECT or pass project." }] };
4869
5763
  }
4870
- const automaticMode = include_memories !== false && include_graph !== true && !(chunk_types && chunk_types.length > 0) && max_tokens === void 0 && runtimeClient;
5764
+ const scope = resolveMcpScope({ project: resolvedProject, user_id, session_id, path: preflight.trust_state.root_path });
5765
+ if (preflight.repo_grounded && preflight.retrieval_route === "local_workspace_fallback") {
5766
+ const localRetrieval = await runLocalWorkspaceRetrieval({
5767
+ query,
5768
+ path: preflight.trust_state.root_path,
5769
+ project: resolvedProject || project,
5770
+ top_k
5771
+ });
5772
+ if (localRetrieval.results.length > 0) {
5773
+ return {
5774
+ content: [{
5775
+ type: "text",
5776
+ text: renderLocalWorkspaceContext({
5777
+ query,
5778
+ project_ref: resolvedProject || null,
5779
+ scope,
5780
+ route: "local_workspace_fallback",
5781
+ hits: localRetrieval.results,
5782
+ diagnostics: localRetrieval.diagnostics,
5783
+ warnings: Array.from(/* @__PURE__ */ new Set([...preflight.warnings, ...localRetrieval.warnings]))
5784
+ })
5785
+ }]
5786
+ };
5787
+ }
5788
+ }
5789
+ if (preflight.repo_grounded && resolvedProject) {
5790
+ const queryResult2 = await queryWithDegradedFallback({
5791
+ project: resolvedProject,
5792
+ query,
5793
+ top_k,
5794
+ include_memories: false,
5795
+ include_graph,
5796
+ user_id: user_id || scope.userId,
5797
+ session_id: session_id || scope.sessionId,
5798
+ source_ids: preflight.matched_source_ids
5799
+ });
5800
+ const repoResults = filterProjectRepoResults(queryResult2.response.results || [], preflight.matched_source_ids);
5801
+ if (repoResults.length > 0) {
5802
+ const scopedResponse = { ...queryResult2.response, results: repoResults };
5803
+ const header2 = `Found ${repoResults.length} repo-grounded result(s) (${scopedResponse.meta.latency_ms}ms${scopedResponse.meta.cache_hit ? ", cached" : ""}, route=project_repo, workspace=${preflight.trust_state.workspace_id}, project=${resolvedProject}, user=${scope.userId}, session=${scope.sessionId}):
5804
+
5805
+ `;
5806
+ const suffix2 = [
5807
+ `[diagnostics] identity_source=${preflight.trust_state.identity_source} retrieval_readiness=${preflight.retrieval_readiness} retrieval_route=project_repo`,
5808
+ queryResult2.degraded_mode ? `[degraded_mode=true] ${queryResult2.degraded_reason}
5809
+ Recommendation: ${queryResult2.recommendation}` : "",
5810
+ preflight.warnings.length ? `[warnings]
5811
+ ${preflight.warnings.join("\n")}` : ""
5812
+ ].filter(Boolean).join("\n\n");
5813
+ return { content: [{ type: "text", text: `${header2}${scopedResponse.context}${suffix2 ? `
5814
+
5815
+ ${suffix2}` : ""}` }] };
5816
+ }
5817
+ }
5818
+ if (preflight.repo_grounded) {
5819
+ if (resolvedProject && include_memories !== false) {
5820
+ const memoryRescue = await runContextQueryMemoryRescue({
5821
+ project: resolvedProject,
5822
+ query,
5823
+ user_id: user_id ? scope.userId : void 0,
5824
+ session_id: session_id ? scope.sessionId : void 0,
5825
+ top_k
5826
+ });
5827
+ if (memoryRescue.results.length && memoryRescue.rescue_mode) {
5828
+ return {
5829
+ content: [{
5830
+ type: "text",
5831
+ text: `${renderContextQueryMemoryRescue({
5832
+ project: resolvedProject,
5833
+ query,
5834
+ scope,
5835
+ results: memoryRescue.results,
5836
+ rescue_mode: memoryRescue.rescue_mode
5837
+ })}
5838
+
5839
+ [diagnostics]
5840
+ retrieval_route=memory_only retrieval_readiness=${preflight.retrieval_readiness} workspace=${preflight.trust_state.workspace_id}`
5841
+ }]
5842
+ };
5843
+ }
5844
+ }
5845
+ return {
5846
+ content: [{
5847
+ type: "text",
5848
+ text: `No relevant repo-grounded context found.
5849
+
5850
+ [diagnostics]
5851
+ workspace=${preflight.trust_state.workspace_id} identity_source=${preflight.trust_state.identity_source} retrieval_readiness=${preflight.retrieval_readiness} retrieval_route=${preflight.retrieval_route}`
5852
+ }]
5853
+ };
5854
+ }
5855
+ const automaticMode = !preflight.repo_grounded && include_memories !== false && include_graph !== true && !(chunk_types && chunk_types.length > 0) && max_tokens === void 0 && runtimeClient;
4871
5856
  if (automaticMode) {
4872
5857
  try {
4873
5858
  const prepared = await prepareAutomaticQuery({
@@ -4875,9 +5860,31 @@ server.tool(
4875
5860
  query,
4876
5861
  top_k,
4877
5862
  user_id,
4878
- session_id
5863
+ session_id,
5864
+ path: preflight.trust_state.root_path
4879
5865
  });
4880
5866
  if (!prepared.items.length) {
5867
+ const memoryRescue = include_memories !== false ? await runContextQueryMemoryRescue({
5868
+ project: resolvedProject,
5869
+ query,
5870
+ user_id: user_id ? scope2.userId : void 0,
5871
+ session_id: session_id ? scope2.sessionId : void 0,
5872
+ top_k
5873
+ }) : { results: [], rescue_mode: null };
5874
+ if (memoryRescue.results.length && memoryRescue.rescue_mode) {
5875
+ return {
5876
+ content: [{
5877
+ type: "text",
5878
+ text: renderContextQueryMemoryRescue({
5879
+ project: resolvedProject,
5880
+ query,
5881
+ scope: scope2,
5882
+ results: memoryRescue.results,
5883
+ rescue_mode: memoryRescue.rescue_mode
5884
+ })
5885
+ }]
5886
+ };
5887
+ }
4881
5888
  return { content: [{ type: "text", text: "No relevant context found." }] };
4882
5889
  }
4883
5890
  const warnings = prepared.retrieval.warnings.length ? `
@@ -4907,18 +5914,41 @@ ${prepared.context}${warnings}`
4907
5914
  top_k,
4908
5915
  include_memories: include_memories === true,
4909
5916
  include_graph,
4910
- user_id: user_id || resolveMcpScope({ user_id }).userId,
4911
- session_id: session_id || resolveMcpScope({ session_id }).sessionId
5917
+ user_id: user_id || scope.userId,
5918
+ session_id: session_id || scope.sessionId
4912
5919
  });
4913
5920
  const response2 = queryResult2.response;
4914
5921
  if (response2.results.length === 0) {
5922
+ const memoryRescue = include_memories !== false ? await runContextQueryMemoryRescue({
5923
+ project: resolvedProject,
5924
+ query,
5925
+ user_id: user_id ? scope.userId : void 0,
5926
+ session_id: session_id ? scope.sessionId : void 0,
5927
+ top_k
5928
+ }) : { results: [], rescue_mode: null };
5929
+ if (memoryRescue.results.length && memoryRescue.rescue_mode) {
5930
+ return {
5931
+ content: [{
5932
+ type: "text",
5933
+ text: `${renderContextQueryMemoryRescue({
5934
+ project: resolvedProject,
5935
+ query,
5936
+ scope,
5937
+ results: memoryRescue.results,
5938
+ rescue_mode: memoryRescue.rescue_mode
5939
+ })}
5940
+
5941
+ [automatic_runtime]
5942
+ ${automaticWarning}`
5943
+ }]
5944
+ };
5945
+ }
4915
5946
  return { content: [{ type: "text", text: `No relevant context found.
4916
5947
 
4917
5948
  [automatic_runtime]
4918
5949
  ${automaticWarning}` }] };
4919
5950
  }
4920
- const scope2 = resolveMcpScope({ user_id, session_id });
4921
- const header2 = `Found ${response2.meta.total} results (${response2.meta.latency_ms}ms${response2.meta.cache_hit ? ", cached" : ""}, project=${resolvedProject}, user=${scope2.userId}, session=${scope2.sessionId}):
5951
+ const header2 = `Found ${response2.meta.total} results (${response2.meta.latency_ms}ms${response2.meta.cache_hit ? ", cached" : ""}, project=${resolvedProject}, user=${scope.userId}, session=${scope.sessionId}):
4922
5952
 
4923
5953
  `;
4924
5954
  const suffix2 = queryResult2.degraded_mode ? `
@@ -4937,14 +5967,34 @@ ${automaticWarning}${suffix2}` }] };
4937
5967
  top_k,
4938
5968
  include_memories: include_memories === true,
4939
5969
  include_graph,
4940
- user_id: user_id || resolveMcpScope({ user_id }).userId,
4941
- session_id: session_id || resolveMcpScope({ session_id }).sessionId
5970
+ user_id: user_id || scope.userId,
5971
+ session_id: session_id || scope.sessionId
4942
5972
  });
4943
5973
  const response = queryResult.response;
4944
5974
  if (response.results.length === 0) {
5975
+ const memoryRescue = include_memories !== false ? await runContextQueryMemoryRescue({
5976
+ project: resolvedProject,
5977
+ query,
5978
+ user_id: user_id ? scope.userId : void 0,
5979
+ session_id: session_id ? scope.sessionId : void 0,
5980
+ top_k
5981
+ }) : { results: [], rescue_mode: null };
5982
+ if (memoryRescue.results.length && memoryRescue.rescue_mode) {
5983
+ return {
5984
+ content: [{
5985
+ type: "text",
5986
+ text: renderContextQueryMemoryRescue({
5987
+ project: resolvedProject,
5988
+ query,
5989
+ scope,
5990
+ results: memoryRescue.results,
5991
+ rescue_mode: memoryRescue.rescue_mode
5992
+ })
5993
+ }]
5994
+ };
5995
+ }
4945
5996
  return { content: [{ type: "text", text: "No relevant context found." }] };
4946
5997
  }
4947
- const scope = resolveMcpScope({ user_id, session_id });
4948
5998
  const header = `Found ${response.meta.total} results (${response.meta.latency_ms}ms${response.meta.cache_hit ? ", cached" : ""}, project=${resolvedProject}, user=${scope.userId}, session=${scope.sessionId}):
4949
5999
 
4950
6000
  `;
@@ -4996,7 +6046,7 @@ server.tool(
4996
6046
  );
4997
6047
  server.tool(
4998
6048
  "memory.search",
4999
- "Search stored memories by semantic similarity. Recall facts, preferences, past decisions from previous interactions.",
6049
+ "Call this before answering questions about user history (preferences, prior decisions, past tasks, or 'what did we discuss/search'). Returns memory context you would not otherwise know.",
5000
6050
  {
5001
6051
  project: z.string().optional().describe("Project name or slug"),
5002
6052
  query: z.string().describe("What to search for"),
@@ -5587,6 +6637,7 @@ server.tool(
5587
6637
  return { content: [{ type: "text", text: "Error: target.memory_id or target.query is required." }] };
5588
6638
  }
5589
6639
  const affectedIds = [];
6640
+ let queryResolution = null;
5590
6641
  const now = (/* @__PURE__ */ new Date()).toISOString();
5591
6642
  const actor = process.env.WHISPER_AGENT_ID || process.env.USERNAME || "api_key_principal";
5592
6643
  const resolvedProject = await resolveProjectRef(project);
@@ -5645,24 +6696,18 @@ server.tool(
5645
6696
  project: resolvedProject,
5646
6697
  query: target.query || "",
5647
6698
  top_k: 25,
5648
- include_relations: false
5649
- });
5650
- const normalizedQuery = String(target.query || "").trim().toLowerCase();
5651
- const exactMatches = (search.results || []).filter((r) => {
5652
- const memory = r?.memory || r;
5653
- const content = String(memory?.content || "").trim().toLowerCase();
5654
- const memoryId = String(memory?.id || "").trim().toLowerCase();
5655
- const metadata = memory?.metadata || {};
5656
- const normalizedContent = String(metadata?.normalized_content || "").trim().toLowerCase();
5657
- const canonicalContent = String(metadata?.canonical_content || "").trim().toLowerCase();
5658
- return memoryId === normalizedQuery || content === normalizedQuery || normalizedContent === normalizedQuery || canonicalContent === normalizedQuery;
6699
+ include_relations: false,
6700
+ include_pending: true
5659
6701
  });
5660
- const memoryIds = exactMatches.map((r) => String(r?.memory?.id || "")).filter(Boolean);
6702
+ const resolved = resolveForgetQueryCandidates(search, target.query || "");
6703
+ queryResolution = resolved.resolved_by;
6704
+ const memoryIds = resolved.memory_ids;
5661
6705
  if (memoryIds.length === 0) {
5662
6706
  const payload2 = {
5663
6707
  status: "completed",
5664
6708
  affected_ids: affectedIds,
5665
- warning: "Query did not resolve to an exact memory match. No memories were changed."
6709
+ warning: resolved.warning || "Query did not resolve to a reliable memory match. No memories were changed.",
6710
+ resolved_by: resolved.resolved_by
5666
6711
  };
5667
6712
  return { content: [{ type: "text", text: JSON.stringify(payload2, null, 2) }] };
5668
6713
  }
@@ -5684,6 +6729,7 @@ server.tool(
5684
6729
  const payload = {
5685
6730
  status: "completed",
5686
6731
  affected_ids: affectedIds,
6732
+ ...queryResolution ? { resolved_by: queryResolution } : {},
5687
6733
  audit: {
5688
6734
  audit_id: audit.audit_id,
5689
6735
  actor: audit.actor,
@@ -5978,30 +7024,56 @@ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".next"
5978
7024
  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"]);
5979
7025
  function extractSignature(filePath, content) {
5980
7026
  const lines = content.split("\n");
5981
- const signature = [`// File: ${filePath}`];
7027
+ const signature = /* @__PURE__ */ new Set([`// File: ${filePath}`]);
7028
+ signature.add(`relative_path:${filePath.replace(/\\/g, "/").toLowerCase()}`);
7029
+ for (const segment of filePath.split(/[\\/._-]+/).filter(Boolean)) {
7030
+ signature.add(`path:${segment}`);
7031
+ }
5982
7032
  const head = lines.slice(0, 60);
5983
7033
  for (const line of head) {
5984
7034
  const trimmed = line.trim();
5985
7035
  if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
7036
+ const urlMatches = trimmed.match(/https?:\/\/[^\s"'`]+/g);
7037
+ if (urlMatches) {
7038
+ for (const match of urlMatches.slice(0, 2)) signature.add(`url:${match.slice(0, 120)}`);
7039
+ }
7040
+ const routeMatches = trimmed.match(/\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]{2,}/g);
7041
+ if (routeMatches) {
7042
+ for (const match of routeMatches.slice(0, 3)) signature.add(`route:${match.slice(0, 120)}`);
7043
+ }
7044
+ const envKeyMatches = trimmed.match(/[A-Z][A-Z0-9_]{2,}/g);
7045
+ if (envKeyMatches) {
7046
+ for (const match of envKeyMatches.slice(0, 3)) signature.add(`env:${match}`);
7047
+ }
5986
7048
  if (/^(import|from|require|use |pub use )/.test(trimmed)) {
5987
- signature.push(trimmed.slice(0, 120));
7049
+ signature.add(trimmed.slice(0, 120));
5988
7050
  continue;
5989
7051
  }
5990
7052
  if (/^(export|async function|function|class|interface|type |const |let |def |pub fn |fn |struct |impl |enum )/.test(trimmed)) {
5991
- signature.push(trimmed.slice(0, 120));
7053
+ signature.add(trimmed.slice(0, 120));
7054
+ const exportMatch = trimmed.match(/export\s+(?:const|function|class|type|interface)\s+([A-Za-z0-9_$]+)/);
7055
+ if (exportMatch?.[1]) signature.add(`export:${exportMatch[1]}`);
5992
7056
  continue;
5993
7057
  }
5994
7058
  if (trimmed.startsWith("@") || trimmed.startsWith("#[")) {
5995
- signature.push(trimmed.slice(0, 80));
7059
+ signature.add(trimmed.slice(0, 80));
5996
7060
  }
5997
7061
  }
5998
- for (const line of lines.slice(60)) {
5999
- const trimmed = line.trim();
7062
+ for (let index = 60; index < lines.length; index += 1) {
7063
+ const trimmed = lines[index].trim();
6000
7064
  if (/^(export (default |async )?function|export (default )?class|export const|export type|export interface|async function|function |class |def |pub fn |fn )/.test(trimmed)) {
6001
- signature.push(trimmed.slice(0, 120));
7065
+ signature.add(trimmed.slice(0, 120));
7066
+ const surrounding = lines.slice(index, Math.min(lines.length, index + 3)).map((line) => line.trim()).filter(Boolean);
7067
+ for (const line of surrounding) signature.add(line.slice(0, 120));
7068
+ continue;
6002
7069
  }
7070
+ if (/^[A-Za-z0-9_$]+\s*[:=]\s*["'`][^"'`]{3,}["'`]/.test(trimmed)) {
7071
+ signature.add(trimmed.slice(0, 120));
7072
+ }
7073
+ const exportMatch = trimmed.match(/export\s+(?:const|function|class|type|interface)\s+([A-Za-z0-9_$]+)/);
7074
+ if (exportMatch?.[1]) signature.add(`export:${exportMatch[1]}`);
6003
7075
  }
6004
- return signature.join("\n").slice(0, 2e3);
7076
+ return Array.from(signature).join("\n").slice(0, 3200);
6005
7077
  }
6006
7078
  server.tool(
6007
7079
  "code.search_semantic",
@@ -6015,91 +7087,21 @@ server.tool(
6015
7087
  max_files: z.number().optional().default(150).describe("Max files to scan. For large codebases, narrow with file_types instead of raising this.")
6016
7088
  },
6017
7089
  async ({ query, path: searchPath, file_types, top_k, threshold, max_files }) => {
6018
- const rootPath = searchPath || process.cwd();
6019
- const allowedExts = file_types ? new Set(file_types) : CODE_EXTENSIONS;
6020
- const files = [];
6021
- function collect(dir) {
6022
- if (files.length >= (max_files ?? 300)) return;
6023
- let entries;
6024
- try {
6025
- entries = readdirSync(dir, { withFileTypes: true });
6026
- } catch {
6027
- return;
6028
- }
6029
- for (const entry of entries) {
6030
- if (files.length >= (max_files ?? 300)) break;
6031
- if (SKIP_DIRS.has(entry.name)) continue;
6032
- const full = join(dir, entry.name);
6033
- if (entry.isDirectory()) {
6034
- collect(full);
6035
- } else if (entry.isFile()) {
6036
- const ext = extname(entry.name).replace(".", "");
6037
- if (allowedExts.has(ext)) files.push(full);
6038
- }
6039
- }
6040
- }
6041
- collect(rootPath);
6042
- if (files.length === 0) {
6043
- return { content: [{ type: "text", text: `No code files found in ${rootPath}` }] };
6044
- }
6045
- const documents = [];
6046
- for (const filePath of files) {
6047
- try {
6048
- const stat = statSync(filePath);
6049
- if (stat.size > 500 * 1024) continue;
6050
- const content = readFileSync(filePath, "utf-8");
6051
- const relPath = relative(rootPath, filePath);
6052
- const signature = extractSignature(relPath, content);
6053
- documents.push({ id: relPath, content: signature });
6054
- } catch {
6055
- }
6056
- }
6057
- if (documents.length === 0) {
6058
- return { content: [{ type: "text", text: "Could not read any files." }] };
6059
- }
6060
- let response;
6061
7090
  try {
6062
- response = await whisper.semanticSearch({
6063
- query,
6064
- documents,
6065
- top_k: top_k ?? 10,
6066
- threshold: threshold ?? 0.2
7091
+ return primaryToolSuccess({
7092
+ ...await runSearchCodeTool({
7093
+ query,
7094
+ path: searchPath,
7095
+ file_types,
7096
+ top_k,
7097
+ threshold,
7098
+ max_files
7099
+ }),
7100
+ tool: "code.search_semantic"
6067
7101
  });
6068
7102
  } catch (error) {
6069
- return { content: [{ type: "text", text: `Semantic search failed: ${error.message}` }] };
6070
- }
6071
- if (!response.results || response.results.length === 0) {
6072
- return { content: [{ type: "text", text: `No semantically relevant files found for: "${query}"
6073
-
6074
- Searched ${documents.length} files in ${rootPath}.
6075
-
6076
- Try lowering the threshold or rephrasing your query.` }] };
6077
- }
6078
- const lines = [
6079
- `Semantic search: "${query}"`,
6080
- `Searched ${documents.length} files \u2192 ${response.results.length} relevant (${response.latency_ms}ms)
6081
- `
6082
- ];
6083
- for (const result of response.results) {
6084
- lines.push(`\u{1F4C4} ${result.id} (score: ${result.score})`);
6085
- if (result.snippet) {
6086
- lines.push(` ${result.snippet}`);
6087
- }
6088
- if (result.score > 0.5) {
6089
- try {
6090
- const fullPath = join(rootPath, result.id);
6091
- const content = readFileSync(fullPath, "utf-8");
6092
- const excerpt = content.split("\n").slice(0, 30).join("\n");
6093
- lines.push(`
6094
- \`\`\`
6095
- ${excerpt}
6096
- \`\`\``);
6097
- } catch {
6098
- }
6099
- }
6100
- lines.push("");
7103
+ return primaryToolError(`Semantic search failed: ${error.message}`);
6101
7104
  }
6102
- return { content: [{ type: "text", text: lines.join("\n") }] };
6103
7105
  }
6104
7106
  );
6105
7107
  function* walkDir(dir, fileTypes) {
@@ -6338,7 +7340,7 @@ server.tool(
6338
7340
  );
6339
7341
  server.tool(
6340
7342
  "search",
6341
- "Search retrievable context by query, exact id, or both. Use `id` for exact fetch and `query` for semantic retrieval.",
7343
+ "Primary retrieval alias. Call this whenever the user asks to find or recall context: use `query` for semantic retrieval, `id` for exact memory fetch, or both for hybrid recall.",
6342
7344
  {
6343
7345
  project: z.string().optional().describe("Project name or slug"),
6344
7346
  query: z.string().optional().describe("Semantic retrieval query"),
@@ -6416,72 +7418,15 @@ server.tool(
6416
7418
  max_files: z.number().optional().default(150)
6417
7419
  },
6418
7420
  async ({ query, path, file_types, top_k, threshold, max_files }) => {
6419
- const rootPath = path || process.cwd();
6420
- const allowedExts = file_types ? new Set(file_types) : CODE_EXTENSIONS;
6421
- const files = [];
6422
- function collect(dir) {
6423
- if (files.length >= (max_files ?? 150)) return;
6424
- let entries;
6425
- try {
6426
- entries = readdirSync(dir, { withFileTypes: true });
6427
- } catch {
6428
- return;
6429
- }
6430
- for (const entry of entries) {
6431
- if (files.length >= (max_files ?? 150)) break;
6432
- if (SKIP_DIRS.has(entry.name)) continue;
6433
- const full = join(dir, entry.name);
6434
- if (entry.isDirectory()) collect(full);
6435
- else if (entry.isFile()) {
6436
- const ext = extname(entry.name).replace(".", "");
6437
- if (allowedExts.has(ext)) files.push(full);
6438
- }
6439
- }
6440
- }
6441
- collect(rootPath);
6442
- if (files.length === 0) {
6443
- return primaryToolSuccess({
6444
- tool: "search_code",
6445
- query,
6446
- path: rootPath,
6447
- results: [],
6448
- count: 0
6449
- });
6450
- }
6451
- const documents = [];
6452
- for (const filePath of files) {
6453
- try {
6454
- const stat = statSync(filePath);
6455
- if (stat.size > 500 * 1024) continue;
6456
- const content = readFileSync(filePath, "utf-8");
6457
- const relPath = relative(rootPath, filePath);
6458
- documents.push({ id: relPath, content: extractSignature(relPath, content) });
6459
- } catch {
6460
- }
6461
- }
6462
7421
  try {
6463
- const response = await whisper.semanticSearch({
6464
- query,
6465
- documents,
6466
- top_k: top_k ?? 10,
6467
- threshold: threshold ?? 0.2
6468
- });
6469
- if (!response.results?.length) {
6470
- return primaryToolSuccess({
6471
- tool: "search_code",
6472
- query,
6473
- path: rootPath,
6474
- results: [],
6475
- count: 0
6476
- });
6477
- }
6478
- return primaryToolSuccess({
6479
- tool: "search_code",
7422
+ return primaryToolSuccess(await runSearchCodeTool({
6480
7423
  query,
6481
- path: rootPath,
6482
- results: response.results,
6483
- count: response.results.length
6484
- });
7424
+ path,
7425
+ file_types,
7426
+ top_k,
7427
+ threshold,
7428
+ max_files
7429
+ }));
6485
7430
  } catch (error) {
6486
7431
  return primaryToolError(`Semantic search failed: ${error.message}`);
6487
7432
  }
@@ -6625,7 +7570,7 @@ server.tool(
6625
7570
  );
6626
7571
  server.tool(
6627
7572
  "index",
6628
- "Index a new source or refresh a workspace. Use action='source' to add GitHub/web/pdf/local/slack/video. Use action='workspace' to refresh local workspace metadata.",
7573
+ "Administrative indexing tool. Call this when retrieval is stale/missing or the user asks to connect new data. Use action='source' to add GitHub/web/pdf/local/slack/video, action='workspace' to refresh local workspace metadata.",
6629
7574
  {
6630
7575
  action: z.enum(["source", "workspace"]).default("source"),
6631
7576
  project: z.string().optional(),
@@ -6706,7 +7651,7 @@ server.tool(
6706
7651
  );
6707
7652
  server.tool(
6708
7653
  "remember",
6709
- "Store something the agent should keep across sessions: a fact, decision, preference, or instruction.",
7654
+ "Call this whenever the user states a durable preference, decision, instruction, or personal/project fact that should persist across sessions. Save proactively without waiting for an explicit 'remember this'.",
6710
7655
  {
6711
7656
  project: z.string().optional(),
6712
7657
  content: z.string().describe("Memory content"),
@@ -6785,7 +7730,7 @@ server.tool(
6785
7730
  );
6786
7731
  server.tool(
6787
7732
  "learn",
6788
- "Unified learning tool for conversation memory, text ingestion, and source indexing. Prefer this over the older learning-adjacent compatibility tools.",
7733
+ "Unified ingestion entrypoint. Call this when the user asks to import knowledge: mode='conversation' for chat logs, mode='text' for raw text, mode='source' for external sources to index. Prefer this over legacy compatibility tools.",
6789
7734
  {
6790
7735
  mode: z.enum(["conversation", "text", "source"]).describe("What kind of learning to perform"),
6791
7736
  project: z.string().optional(),
@@ -6916,8 +7861,17 @@ if (process.argv[1] && /server\.(mjs|cjs|js|ts)$/.test(process.argv[1])) {
6916
7861
  main().catch(console.error);
6917
7862
  }
6918
7863
  export {
7864
+ canonicalizeWorkspacePath,
7865
+ chooseWorkspaceProjectSource,
7866
+ classifyProjectRepoReadiness,
7867
+ classifyRepoGroundedQuery,
7868
+ classifyWorkspaceHealth,
6919
7869
  createMcpServer,
6920
7870
  createWhisperMcpClient,
6921
7871
  createWhisperMcpRuntimeClient,
6922
- renderScopedMcpConfig
7872
+ extractSignature,
7873
+ renderScopedMcpConfig,
7874
+ resolveForgetQueryCandidates,
7875
+ resolveWorkspaceIdentity,
7876
+ runSearchCodeSearch
6923
7877
  };