@zokizuan/satori-mcp 4.6.0 → 4.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -157,7 +157,7 @@ No parameters.
157
157
  "mcpServers": {
158
158
  "satori": {
159
159
  "command": "npx",
160
- "args": ["-y", "@zokizuan/satori-mcp@4.4.1"],
160
+ "args": ["-y", "@zokizuan/satori-mcp@4.8.0"],
161
161
  "timeout": 180000,
162
162
  "env": {
163
163
  "EMBEDDING_PROVIDER": "VoyageAI",
@@ -178,7 +178,7 @@ No parameters.
178
178
  ```toml
179
179
  [mcp_servers.satori]
180
180
  command = "npx"
181
- args = ["-y", "@zokizuan/satori-mcp@4.4.1"]
181
+ args = ["-y", "@zokizuan/satori-mcp@4.8.0"]
182
182
  startup_timeout_ms = 180000
183
183
  env = { EMBEDDING_PROVIDER = "VoyageAI", EMBEDDING_MODEL = "voyage-4-large", EMBEDDING_OUTPUT_DIMENSION = "1024", VOYAGEAI_API_KEY = "your-api-key", VOYAGEAI_RERANKER_MODEL = "rerank-2.5", MILVUS_ADDRESS = "your-milvus-endpoint", MILVUS_TOKEN = "your-milvus-token" }
184
184
  ```
@@ -228,10 +228,10 @@ Supported installer targets in Phase 1:
228
228
  Examples:
229
229
 
230
230
  ```bash
231
- npx -y @zokizuan/satori-cli@0.1.1 install --client codex
232
- npx -y @zokizuan/satori-cli@0.1.1 install --client claude
233
- npx -y @zokizuan/satori-cli@0.1.1 install --client all --dry-run
234
- npx -y @zokizuan/satori-cli@0.1.1 uninstall --client codex
231
+ npx -y @zokizuan/satori-cli@0.2.0 install --client codex
232
+ npx -y @zokizuan/satori-cli@0.2.0 install --client claude
233
+ npx -y @zokizuan/satori-cli@0.2.0 install --client all --dry-run
234
+ npx -y @zokizuan/satori-cli@0.2.0 uninstall --client codex
235
235
  ```
236
236
 
237
237
  Install and uninstall run before MCP session startup, only touch Satori-managed config, and copy/remove these packaged skills:
@@ -55,7 +55,7 @@ function assertPublishedVersion(packageName, version, ownerPackageName, ownerPac
55
55
  stdio: ["ignore", "pipe", "pipe"],
56
56
  });
57
57
  }
58
- catch (error) {
58
+ catch {
59
59
  if (relation === "self") {
60
60
  throw new CliError("E_USAGE", `Cannot install ${ownerPackageName}@${ownerPackageVersion} because that package version is not published on npm. Publish ${ownerPackageName}@${ownerPackageVersion} first or use a local dev server config instead.`, 2);
61
61
  }
package/dist/config.d.ts CHANGED
@@ -42,6 +42,10 @@ export interface CodebaseIndexManifest {
42
42
  indexedPaths: string[];
43
43
  updatedAt: string;
44
44
  }
45
+ export interface CodebaseClearTombstone {
46
+ clearedAt: string;
47
+ collectionName?: string;
48
+ }
45
49
  export interface CodebaseSnapshotV1 {
46
50
  indexedCodebases: string[];
47
51
  indexingCodebases: string[] | Record<string, number>;
@@ -94,6 +98,7 @@ export interface CodebaseSnapshotV2 {
94
98
  export interface CodebaseSnapshotV3 {
95
99
  formatVersion: 'v3';
96
100
  codebases: Record<string, CodebaseInfo>;
101
+ clearTombstones?: Record<string, CodebaseClearTombstone>;
97
102
  lastUpdated: string;
98
103
  }
99
104
  export type CodebaseSnapshot = CodebaseSnapshotV1 | CodebaseSnapshotV2 | CodebaseSnapshotV3;
package/dist/config.js CHANGED
@@ -169,7 +169,7 @@ export function showHelpMessage() {
169
169
  console.log(`
170
170
  Satori MCP Server
171
171
 
172
- Usage: npx -y @zokizuan/satori-mcp@4.4.1 [options]
172
+ Usage: npx -y @zokizuan/satori-mcp@4.8.0 [options]
173
173
 
174
174
  Options:
175
175
  --help, -h Show this help message
@@ -206,16 +206,16 @@ Environment Variables:
206
206
 
207
207
  Examples:
208
208
  # Start MCP server with OpenAI and explicit Milvus address
209
- OPENAI_API_KEY=sk-xxx MILVUS_ADDRESS=localhost:19530 npx -y @zokizuan/satori-mcp@4.4.1
209
+ OPENAI_API_KEY=sk-xxx MILVUS_ADDRESS=localhost:19530 npx -y @zokizuan/satori-mcp@4.8.0
210
210
 
211
211
  # Start MCP server with VoyageAI and specific model
212
- EMBEDDING_PROVIDER=VoyageAI VOYAGEAI_API_KEY=pa-xxx EMBEDDING_MODEL=voyage-4-large MILVUS_TOKEN=your-token npx -y @zokizuan/satori-mcp@4.4.1
212
+ EMBEDDING_PROVIDER=VoyageAI VOYAGEAI_API_KEY=pa-xxx EMBEDDING_MODEL=voyage-4-large MILVUS_TOKEN=your-token npx -y @zokizuan/satori-mcp@4.8.0
213
213
 
214
214
  # Start MCP server with Gemini and specific model
215
- EMBEDDING_PROVIDER=Gemini GEMINI_API_KEY=xxx EMBEDDING_MODEL=gemini-embedding-001 MILVUS_TOKEN=your-token npx -y @zokizuan/satori-mcp@4.4.1
215
+ EMBEDDING_PROVIDER=Gemini GEMINI_API_KEY=xxx EMBEDDING_MODEL=gemini-embedding-001 MILVUS_TOKEN=your-token npx -y @zokizuan/satori-mcp@4.8.0
216
216
 
217
217
  # Start MCP server with Ollama and specific model
218
- EMBEDDING_PROVIDER=Ollama EMBEDDING_MODEL=nomic-embed-text MILVUS_TOKEN=your-token npx -y @zokizuan/satori-mcp@4.4.1
218
+ EMBEDDING_PROVIDER=Ollama EMBEDDING_MODEL=nomic-embed-text MILVUS_TOKEN=your-token npx -y @zokizuan/satori-mcp@4.8.0
219
219
  `);
220
220
  }
221
221
  //# sourceMappingURL=config.js.map
@@ -0,0 +1,24 @@
1
+ import type { IndexFingerprint } from "../config.js";
2
+ export type CompletionProofOutcome = "valid" | "stale_local" | "fingerprint_mismatch" | "probe_failed";
3
+ export type CompletionProofReason = "missing_marker_doc" | "invalid_marker_kind" | "path_mismatch" | "invalid_payload" | "fingerprint_mismatch" | "probe_failed";
4
+ export type CompletionProofValidationResult = {
5
+ outcome: CompletionProofOutcome;
6
+ reason?: CompletionProofReason;
7
+ marker?: {
8
+ kind?: string;
9
+ codebasePath?: string;
10
+ fingerprint?: unknown;
11
+ indexedFiles?: number;
12
+ totalChunks?: number;
13
+ completedAt?: string;
14
+ runId?: string;
15
+ };
16
+ };
17
+ export type CompletionMarkerReader = (codebasePath: string) => Promise<unknown>;
18
+ export declare function validateCompletionProof(args: {
19
+ codebasePath: string;
20
+ runtimeFingerprint?: IndexFingerprint;
21
+ getIndexCompletionMarker?: CompletionMarkerReader;
22
+ onProbeError?: (error: unknown) => void;
23
+ }): Promise<CompletionProofValidationResult>;
24
+ //# sourceMappingURL=completion-proof.d.ts.map
@@ -0,0 +1,106 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ function trimTrailingSeparators(inputPath) {
4
+ const normalized = path.normalize(inputPath);
5
+ const parsedRoot = path.parse(normalized).root;
6
+ if (normalized === parsedRoot) {
7
+ return normalized;
8
+ }
9
+ return normalized.replace(/[\\/]+$/, "");
10
+ }
11
+ function canonicalizeCodebasePath(codebasePath) {
12
+ const resolved = path.resolve(codebasePath);
13
+ try {
14
+ const realPath = typeof fs.realpathSync.native === "function"
15
+ ? fs.realpathSync.native(resolved)
16
+ : fs.realpathSync(resolved);
17
+ return trimTrailingSeparators(realPath);
18
+ }
19
+ catch {
20
+ return trimTrailingSeparators(resolved);
21
+ }
22
+ }
23
+ function markerMatchesRuntimeFingerprint(marker, runtimeFingerprint) {
24
+ if (!runtimeFingerprint || typeof runtimeFingerprint !== "object") {
25
+ return true;
26
+ }
27
+ const fingerprint = marker?.fingerprint;
28
+ if (!fingerprint || typeof fingerprint !== "object") {
29
+ return false;
30
+ }
31
+ const record = fingerprint;
32
+ return record.embeddingProvider === runtimeFingerprint.embeddingProvider
33
+ && record.embeddingModel === runtimeFingerprint.embeddingModel
34
+ && Number(record.embeddingDimension) === Number(runtimeFingerprint.embeddingDimension)
35
+ && record.vectorStoreProvider === runtimeFingerprint.vectorStoreProvider
36
+ && record.schemaVersion === runtimeFingerprint.schemaVersion;
37
+ }
38
+ function isNonNegativeInteger(value) {
39
+ return typeof value === "number"
40
+ && Number.isInteger(value)
41
+ && value >= 0;
42
+ }
43
+ function validateMarkerShape(expectedCodebasePath, marker) {
44
+ if (!marker || typeof marker !== "object") {
45
+ return { ok: false, reason: "invalid_payload" };
46
+ }
47
+ const record = marker;
48
+ if (record.kind !== "satori_index_completion_v1") {
49
+ return { ok: false, reason: "invalid_marker_kind" };
50
+ }
51
+ if (typeof record.codebasePath !== "string" || record.codebasePath.trim().length === 0) {
52
+ return { ok: false, reason: "invalid_payload" };
53
+ }
54
+ if (!record.fingerprint || typeof record.fingerprint !== "object") {
55
+ return { ok: false, reason: "invalid_payload" };
56
+ }
57
+ if (!isNonNegativeInteger(record.indexedFiles) || !isNonNegativeInteger(record.totalChunks)) {
58
+ return { ok: false, reason: "invalid_payload" };
59
+ }
60
+ if (typeof record.completedAt !== "string" || Number.isNaN(Date.parse(record.completedAt))) {
61
+ return { ok: false, reason: "invalid_payload" };
62
+ }
63
+ const expectedCanonical = canonicalizeCodebasePath(expectedCodebasePath);
64
+ const markerCanonical = canonicalizeCodebasePath(record.codebasePath);
65
+ if (expectedCanonical !== markerCanonical) {
66
+ return { ok: false, reason: "path_mismatch" };
67
+ }
68
+ return { ok: true };
69
+ }
70
+ export async function validateCompletionProof(args) {
71
+ const { codebasePath, runtimeFingerprint, getIndexCompletionMarker, onProbeError } = args;
72
+ if (typeof getIndexCompletionMarker !== "function") {
73
+ return { outcome: "probe_failed", reason: "probe_failed" };
74
+ }
75
+ let marker;
76
+ try {
77
+ marker = await getIndexCompletionMarker(codebasePath);
78
+ }
79
+ catch (error) {
80
+ onProbeError?.(error);
81
+ return { outcome: "probe_failed", reason: "probe_failed" };
82
+ }
83
+ if (!marker) {
84
+ return { outcome: "stale_local", reason: "missing_marker_doc" };
85
+ }
86
+ const markerShape = validateMarkerShape(codebasePath, marker);
87
+ if (!markerShape.ok) {
88
+ return {
89
+ outcome: "stale_local",
90
+ reason: markerShape.reason,
91
+ marker: marker
92
+ };
93
+ }
94
+ if (!markerMatchesRuntimeFingerprint(marker, runtimeFingerprint)) {
95
+ return {
96
+ outcome: "fingerprint_mismatch",
97
+ reason: "fingerprint_mismatch",
98
+ marker: marker
99
+ };
100
+ }
101
+ return {
102
+ outcome: "valid",
103
+ marker: marker
104
+ };
105
+ }
106
+ //# sourceMappingURL=completion-proof.js.map
@@ -33,10 +33,6 @@ export declare class ToolHandlers {
33
33
  private isIndexingStateStale;
34
34
  private recoverStaleIndexingStateIfNeeded;
35
35
  private buildManageActionBlockedMessage;
36
- private markerMatchesRuntimeFingerprint;
37
- private trimTrailingSeparators;
38
- private canonicalizeCodebasePath;
39
- private validateMarkerShape;
40
36
  private buildStaleLocalHint;
41
37
  private buildStaleLocalMessage;
42
38
  private withProofDebugHint;
@@ -72,6 +68,8 @@ export declare class ToolHandlers {
72
68
  private classifyNoiseCategory;
73
69
  private roundRatio;
74
70
  private normalizeRelativePathForIgnoreCheck;
71
+ private trimTrailingSeparators;
72
+ private canonicalizeCodebasePath;
75
73
  private loadRootGitignoreMatcher;
76
74
  private patternMatchesAnyPath;
77
75
  private filterNoiseHintPatternsByRootGitignore;
@@ -3,13 +3,14 @@ import * as path from "path";
3
3
  import crypto from "node:crypto";
4
4
  import { execFileSync } from "node:child_process";
5
5
  import ignore from "ignore";
6
- import { COLLECTION_LIMIT_MESSAGE, getSupportedExtensionsForCapability, isLanguageCapabilitySupportedForExtension, isLanguageCapabilitySupportedForLanguage, } from "@zokizuan/satori-core";
6
+ import { COLLECTION_LIMIT_MESSAGE, RemoteCollectionDeletePendingError, deleteCollectionWithVerification, getSupportedExtensionsForCapability, isLanguageCapabilitySupportedForExtension, isLanguageCapabilitySupportedForLanguage, } from "@zokizuan/satori-core";
7
7
  import { ensureAbsolutePath, truncateContent, trackCodebasePath } from "../utils.js";
8
8
  import { DEFAULT_WATCH_DEBOUNCE_MS } from "../config.js";
9
9
  import { SEARCH_CHANGED_FILES_CACHE_TTL_MS, SEARCH_CHANGED_FIRST_MULTIPLIER, SEARCH_CHANGED_FIRST_MAX_CHANGED_FILES, SEARCH_DIVERSITY_MAX_PER_FILE, SEARCH_DIVERSITY_MAX_PER_SYMBOL, SEARCH_DIVERSITY_RELAXED_FILE_CAP, SEARCH_MAX_CANDIDATES, SEARCH_MUST_RETRY_MULTIPLIER, SEARCH_MUST_RETRY_ROUNDS, SEARCH_NOISE_HINT_PATTERNS, SEARCH_GITIGNORE_FORCE_RELOAD_EVERY_N, SEARCH_NOISE_HINT_THRESHOLD, SEARCH_NOISE_HINT_TOP_K, SEARCH_OPERATOR_PREFIX_MAX_CHARS, SEARCH_PROXIMITY_WINDOW, SEARCH_RERANK_DOC_MAX_CHARS, SEARCH_RERANK_DOC_MAX_LINES, SEARCH_RERANK_RRF_K, SEARCH_RERANK_TOP_K, SEARCH_RERANK_WEIGHT, SEARCH_RRF_K, SCOPE_PATH_MULTIPLIERS, STALENESS_THRESHOLDS_MS } from "./search-constants.js";
10
10
  import { WARNING_CODES } from "./warnings.js";
11
11
  import { CallGraphSidecarManager } from "./call-graph.js";
12
12
  import { decideInterruptedIndexingRecovery } from "./indexing-recovery.js";
13
+ import { validateCompletionProof as validateIndexCompletionProof } from "./completion-proof.js";
13
14
  const COLLECTION_LIMIT_PATTERNS = [
14
15
  /exceeded the limit number of collections/i,
15
16
  /collection limit/i,
@@ -110,6 +111,10 @@ function isCollectionLimitError(error) {
110
111
  }
111
112
  return COLLECTION_LIMIT_PATTERNS.some((pattern) => pattern.test(message));
112
113
  }
114
+ function isBackendTimeoutError(error) {
115
+ const message = formatUnknownError(error);
116
+ return /DEADLINE_EXCEEDED|deadline exceeded|timeout|timed out/i.test(message);
117
+ }
113
118
  export class ToolHandlers {
114
119
  constructor(context, snapshotManager, syncManager, runtimeFingerprint, capabilities, now = () => Date.now(), callGraphManager, reranker, gitignoreForceReloadEveryN = SEARCH_GITIGNORE_FORCE_RELOAD_EVERY_N) {
115
120
  this.indexingStats = null;
@@ -296,62 +301,6 @@ export class ToolHandlers {
296
301
  }
297
302
  return lines.join('\n');
298
303
  }
299
- markerMatchesRuntimeFingerprint(marker) {
300
- const fingerprint = marker?.fingerprint;
301
- if (!fingerprint || typeof fingerprint !== 'object') {
302
- return false;
303
- }
304
- return fingerprint.embeddingProvider === this.runtimeFingerprint.embeddingProvider
305
- && fingerprint.embeddingModel === this.runtimeFingerprint.embeddingModel
306
- && Number(fingerprint.embeddingDimension) === Number(this.runtimeFingerprint.embeddingDimension)
307
- && fingerprint.vectorStoreProvider === this.runtimeFingerprint.vectorStoreProvider
308
- && fingerprint.schemaVersion === this.runtimeFingerprint.schemaVersion;
309
- }
310
- trimTrailingSeparators(inputPath) {
311
- const parsedRoot = path.parse(inputPath).root;
312
- if (inputPath === parsedRoot) {
313
- return inputPath;
314
- }
315
- return inputPath.replace(/[\\/]+$/, '');
316
- }
317
- canonicalizeCodebasePath(codebasePath) {
318
- const resolved = path.resolve(codebasePath);
319
- try {
320
- const realPath = typeof fs.realpathSync.native === 'function'
321
- ? fs.realpathSync.native(resolved)
322
- : fs.realpathSync(resolved);
323
- return this.trimTrailingSeparators(path.normalize(realPath));
324
- }
325
- catch {
326
- return this.trimTrailingSeparators(path.normalize(resolved));
327
- }
328
- }
329
- validateMarkerShape(expectedCodebasePath, marker) {
330
- if (!marker || typeof marker !== 'object') {
331
- return { ok: false, reason: 'invalid_payload' };
332
- }
333
- if (marker.kind !== 'satori_index_completion_v1') {
334
- return { ok: false, reason: 'invalid_marker_kind' };
335
- }
336
- if (typeof marker.codebasePath !== 'string' || marker.codebasePath.trim().length === 0) {
337
- return { ok: false, reason: 'invalid_payload' };
338
- }
339
- if (!marker.fingerprint || typeof marker.fingerprint !== 'object') {
340
- return { ok: false, reason: 'invalid_payload' };
341
- }
342
- if (!Number.isFinite(Number(marker.indexedFiles)) || !Number.isFinite(Number(marker.totalChunks))) {
343
- return { ok: false, reason: 'invalid_payload' };
344
- }
345
- if (typeof marker.completedAt !== 'string' || Number.isNaN(Date.parse(marker.completedAt))) {
346
- return { ok: false, reason: 'invalid_payload' };
347
- }
348
- const expectedCanonical = this.canonicalizeCodebasePath(expectedCodebasePath);
349
- const markerCanonical = this.canonicalizeCodebasePath(marker.codebasePath);
350
- if (expectedCanonical !== markerCanonical) {
351
- return { ok: false, reason: 'path_mismatch' };
352
- }
353
- return { ok: true };
354
- }
355
304
  buildStaleLocalHint(codebasePath, reason) {
356
305
  return {
357
306
  completionProof: reason,
@@ -381,48 +330,16 @@ export class ToolHandlers {
381
330
  };
382
331
  }
383
332
  async validateCompletionProof(codebasePath) {
384
- if (typeof this.context.getIndexCompletionMarker !== 'function') {
385
- return {
386
- outcome: 'probe_failed',
387
- reason: 'probe_failed'
388
- };
389
- }
390
- let marker;
391
- try {
392
- marker = await this.context.getIndexCompletionMarker(codebasePath);
393
- }
394
- catch (error) {
395
- console.warn(`[INDEX-PROOF] Completion marker probe failed for '${codebasePath}': ${formatUnknownError(error)}`);
396
- return {
397
- outcome: 'probe_failed',
398
- reason: 'probe_failed'
399
- };
400
- }
401
- if (!marker) {
402
- return {
403
- outcome: 'stale_local',
404
- reason: 'missing_marker_doc'
405
- };
406
- }
407
- const markerShape = this.validateMarkerShape(codebasePath, marker);
408
- if (!markerShape.ok) {
409
- return {
410
- outcome: 'stale_local',
411
- reason: markerShape.reason,
412
- marker
413
- };
414
- }
415
- if (!this.markerMatchesRuntimeFingerprint(marker)) {
416
- return {
417
- outcome: 'fingerprint_mismatch',
418
- reason: 'fingerprint_mismatch',
419
- marker
420
- };
421
- }
422
- return {
423
- outcome: 'valid',
424
- marker
425
- };
333
+ return validateIndexCompletionProof({
334
+ codebasePath,
335
+ runtimeFingerprint: this.runtimeFingerprint,
336
+ getIndexCompletionMarker: typeof this.context.getIndexCompletionMarker === 'function'
337
+ ? (markerPath) => this.context.getIndexCompletionMarker(markerPath)
338
+ : undefined,
339
+ onProbeError: (error) => {
340
+ console.warn(`[INDEX-PROOF] Completion marker probe failed for '${codebasePath}': ${formatUnknownError(error)}`);
341
+ }
342
+ });
426
343
  }
427
344
  isPathWithinCodebase(targetPath, rootPath) {
428
345
  return targetPath === rootPath || targetPath.startsWith(`${rootPath}${path.sep}`);
@@ -933,6 +850,25 @@ export class ToolHandlers {
933
850
  }
934
851
  return normalized;
935
852
  }
853
+ trimTrailingSeparators(inputPath) {
854
+ const parsedRoot = path.parse(inputPath).root;
855
+ if (inputPath === parsedRoot) {
856
+ return inputPath;
857
+ }
858
+ return inputPath.replace(/[\\/]+$/, '');
859
+ }
860
+ canonicalizeCodebasePath(codebasePath) {
861
+ const resolved = path.resolve(codebasePath);
862
+ try {
863
+ const realPath = typeof fs.realpathSync.native === 'function'
864
+ ? fs.realpathSync.native(resolved)
865
+ : fs.realpathSync(resolved);
866
+ return this.trimTrailingSeparators(path.normalize(realPath));
867
+ }
868
+ catch {
869
+ return this.trimTrailingSeparators(path.normalize(resolved));
870
+ }
871
+ }
936
872
  loadRootGitignoreMatcher(codebaseRoot) {
937
873
  const cacheKey = this.canonicalizeCodebasePath(codebaseRoot);
938
874
  const gitignorePath = path.join(cacheKey, '.gitignore');
@@ -2302,17 +2238,23 @@ Agent instructions:
2302
2238
  console.warn(`[FORCE-REINDEX] Failed to list cloud collections while preparing cleanup: ${formatUnknownError(error)}`);
2303
2239
  }
2304
2240
  const droppedCollections = [];
2241
+ const dropErrors = [];
2305
2242
  for (const candidateName of candidateNames) {
2306
2243
  try {
2307
- if (await vectorDb.hasCollection(candidateName)) {
2308
- await vectorDb.dropCollection(candidateName);
2244
+ const result = await deleteCollectionWithVerification(vectorDb, candidateName);
2245
+ if (result.attempts > 0) {
2309
2246
  droppedCollections.push(candidateName);
2310
2247
  }
2311
2248
  }
2312
2249
  catch (error) {
2313
- console.warn(`[FORCE-REINDEX] Failed to drop collection '${candidateName}': ${formatUnknownError(error)}`);
2250
+ const message = formatUnknownError(error);
2251
+ dropErrors.push(`${candidateName}: ${message}`);
2252
+ console.warn(`[FORCE-REINDEX] Failed to drop collection '${candidateName}': ${message}`);
2314
2253
  }
2315
2254
  }
2255
+ if (dropErrors.length > 0) {
2256
+ throw new Error(`Force reindex cleanup failed before local state changes: ${dropErrors.join('; ')}`);
2257
+ }
2316
2258
  // Ensure local Merkle/snapshot state is cleared for this codebase.
2317
2259
  try {
2318
2260
  await this.context.clearIndex(codebasePath);
@@ -2335,9 +2277,12 @@ Agent instructions:
2335
2277
  throw new Error(`Collection '${trimmedName}' does not exist in the connected Zilliz cluster.`);
2336
2278
  }
2337
2279
  const droppedCodebasePath = await this.resolveCollectionCodebasePath(vectorDb, trimmedName, new Map());
2338
- await vectorDb.dropCollection(trimmedName);
2280
+ await deleteCollectionWithVerification(vectorDb, trimmedName);
2339
2281
  if (droppedCodebasePath) {
2340
2282
  this.snapshotManager.removeCodebaseCompletely(droppedCodebasePath);
2283
+ if (typeof this.snapshotManager.markCodebaseCleared === 'function') {
2284
+ this.snapshotManager.markCodebaseCleared(droppedCodebasePath, trimmedName);
2285
+ }
2341
2286
  this.snapshotManager.saveCodebaseSnapshot();
2342
2287
  try {
2343
2288
  await this.unwatchCodebase(droppedCodebasePath);
@@ -2366,7 +2311,7 @@ Agent instructions:
2366
2311
  console.log(`[SYNC-CLOUD] ✅ No collections found in cloud (non-destructive reconcile keeps local snapshot unchanged)`);
2367
2312
  return;
2368
2313
  }
2369
- const cloudCodebases = new Set();
2314
+ const cloudCodebaseCollections = new Map();
2370
2315
  // Check each collection for codebase path
2371
2316
  for (const collectionName of collections) {
2372
2317
  try {
@@ -2390,7 +2335,9 @@ Agent instructions:
2390
2335
  const codebasePath = metadata.codebasePath;
2391
2336
  if (codebasePath && typeof codebasePath === 'string') {
2392
2337
  console.log(`[SYNC-CLOUD] 📍 Found codebase path: ${codebasePath} in collection: ${collectionName}`);
2393
- cloudCodebases.add(codebasePath);
2338
+ const collectionNames = cloudCodebaseCollections.get(codebasePath) ?? new Set();
2339
+ collectionNames.add(collectionName);
2340
+ cloudCodebaseCollections.set(codebasePath, collectionNames);
2394
2341
  }
2395
2342
  else {
2396
2343
  console.warn(`[SYNC-CLOUD] ⚠️ No codebasePath found in metadata for collection: ${collectionName}`);
@@ -2413,9 +2360,17 @@ Agent instructions:
2413
2360
  // Continue with next collection
2414
2361
  }
2415
2362
  }
2416
- console.log(`[SYNC-CLOUD] 📊 Found ${cloudCodebases.size} valid codebases in cloud`);
2363
+ console.log(`[SYNC-CLOUD] 📊 Found ${cloudCodebaseCollections.size} valid codebases in cloud`);
2417
2364
  let hasChanges = false;
2418
- for (const cloudCodebasePath of cloudCodebases) {
2365
+ for (const [cloudCodebasePath, collectionNames] of cloudCodebaseCollections.entries()) {
2366
+ const activeCollectionNames = Array.from(collectionNames)
2367
+ .filter((collectionName) => (typeof this.snapshotManager.isCodebaseCleared !== 'function'
2368
+ || !this.snapshotManager.isCodebaseCleared(cloudCodebasePath, collectionName)));
2369
+ if (typeof this.snapshotManager.isCodebaseCleared === 'function'
2370
+ && activeCollectionNames.length === 0) {
2371
+ console.log(`[SYNC-CLOUD] ⏭️ Skipping repair for intentionally cleared root '${cloudCodebasePath}'`);
2372
+ continue;
2373
+ }
2419
2374
  const localInfo = this.snapshotManager.getCodebaseInfo(cloudCodebasePath);
2420
2375
  if (localInfo?.status === 'indexing') {
2421
2376
  console.log(`[SYNC-CLOUD] ⏸️ Skipping repair for indexing root '${cloudCodebasePath}'`);
@@ -2524,15 +2479,15 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
2524
2479
  // If force reindex, always clear every previous collection for this codebase hash.
2525
2480
  if (forceReindex) {
2526
2481
  console.log(`[FORCE-REINDEX] 🔄 Preparing force cleanup for '${absolutePath}'`);
2482
+ const droppedCollections = await this.clearAllCollectionsForForceReindex(absolutePath);
2527
2483
  this.snapshotManager.removeCodebaseCompletely(absolutePath);
2528
2484
  this.snapshotManager.saveCodebaseSnapshot();
2529
2485
  try {
2530
2486
  await this.unwatchCodebase(absolutePath);
2531
2487
  }
2532
2488
  catch {
2533
- // Best-effort watcher cleanup before force rebuild.
2489
+ // Best-effort watcher cleanup after successful force cleanup.
2534
2490
  }
2535
- const droppedCollections = await this.clearAllCollectionsForForceReindex(absolutePath);
2536
2491
  if (droppedCollections.length > 0) {
2537
2492
  const sortedDroppedCollections = [...droppedCollections].sort();
2538
2493
  dropSummaryLine += `\nForce reindex cleanup dropped ${sortedDroppedCollections.length} prior collection(s) for this codebase hash: ${sortedDroppedCollections.join(', ')}.`;
@@ -2549,7 +2504,25 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
2549
2504
  if (requestedDropCollection === targetCollectionName) {
2550
2505
  return this.manageResponse(manageAction, absolutePath, "error", `Error: zillizDropCollection cannot target '${targetCollectionName}' for this same codebase create flow. Use {"action":"create","path":"${absolutePath}","force":true} for reindexing this codebase.`, preflightOptions);
2551
2506
  }
2552
- const dropResult = await this.dropZillizCollectionForCreate(requestedDropCollection);
2507
+ let dropResult;
2508
+ try {
2509
+ dropResult = await this.dropZillizCollectionForCreate(requestedDropCollection);
2510
+ }
2511
+ catch (error) {
2512
+ if (error instanceof RemoteCollectionDeletePendingError) {
2513
+ return this.manageResponse(manageAction, absolutePath, "error", `Zilliz collection '${requestedDropCollection}' remote deletion is still pending. Local index state was not changed. Retry after the backend has converged. Details: ${formatUnknownError(error)}`, {
2514
+ ...preflightOptions,
2515
+ reason: "remote_delete_pending",
2516
+ hints: {
2517
+ retry: {
2518
+ tool: "manage_index",
2519
+ args: { action: manageAction, path: absolutePath, zillizDropCollection: requestedDropCollection }
2520
+ }
2521
+ }
2522
+ });
2523
+ }
2524
+ throw error;
2525
+ }
2553
2526
  dropSummaryLine += dropResult.droppedCodebasePath
2554
2527
  ? `\nDropped Zilliz collection '${requestedDropCollection}' (mapped codebase: '${dropResult.droppedCodebasePath}').`
2555
2528
  : `\nDropped Zilliz collection '${requestedDropCollection}'.`;
@@ -2572,8 +2545,36 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
2572
2545
  const guidanceMessage = await this.buildCollectionLimitMessage(absolutePath);
2573
2546
  return this.manageResponse(manageAction, absolutePath, "error", guidanceMessage, preflightOptions);
2574
2547
  }
2548
+ if (validationError instanceof RemoteCollectionDeletePendingError) {
2549
+ return this.manageResponse(manageAction, absolutePath, "error", `Zilliz/Milvus validation collection deletion is still pending. Local index state was not changed. Retry after the backend has converged. Details: ${formatUnknownError(validationError)}`, {
2550
+ ...preflightOptions,
2551
+ reason: "remote_delete_pending",
2552
+ hints: {
2553
+ retry: {
2554
+ tool: "manage_index",
2555
+ args: { action: manageAction, path: absolutePath }
2556
+ }
2557
+ }
2558
+ });
2559
+ }
2575
2560
  const validationMessage = formatUnknownError(validationError);
2576
- return this.manageResponse(manageAction, absolutePath, "error", `Error validating collection creation: ${validationMessage}`, preflightOptions);
2561
+ const backendTimeout = isBackendTimeoutError(validationError);
2562
+ const timeoutOptions = backendTimeout
2563
+ ? {
2564
+ ...preflightOptions,
2565
+ reason: "backend_timeout",
2566
+ hints: {
2567
+ retry: {
2568
+ tool: "manage_index",
2569
+ args: { action: manageAction, path: absolutePath }
2570
+ }
2571
+ }
2572
+ }
2573
+ : preflightOptions;
2574
+ const validationText = backendTimeout
2575
+ ? `Backend timeout while validating Zilliz/Milvus collection creation for '${absolutePath}'. The repo path is valid and local index state was not changed. This is retryable/operator-actionable: check backend availability or network latency, then retry manage_index action='${manageAction}'. Details: ${validationMessage}`
2576
+ : `Error validating collection creation: ${validationMessage}`;
2577
+ return this.manageResponse(manageAction, absolutePath, "error", validationText, timeoutOptions);
2577
2578
  }
2578
2579
  // Add custom extensions if provided
2579
2580
  if (customFileExtensions.length > 0) {
@@ -4004,12 +4005,26 @@ To force rebuild from scratch: call manage_index with {"action":"create","path":
4004
4005
  console.log(`[CLEAR] Successfully cleared index for: ${absolutePath}`);
4005
4006
  }
4006
4007
  catch (error) {
4008
+ if (error instanceof RemoteCollectionDeletePendingError) {
4009
+ const errorMsg = `Remote deletion is still pending for ${absolutePath}. Local index state was not changed. Details: ${formatUnknownError(error)}`;
4010
+ console.error(`[CLEAR] ${errorMsg}`);
4011
+ return this.manageResponse("clear", absolutePath, "error", errorMsg, {
4012
+ reason: "remote_delete_pending",
4013
+ hints: {
4014
+ retry: this.buildStatusHint(absolutePath),
4015
+ clear: { tool: "manage_index", args: { action: "clear", path: absolutePath } }
4016
+ }
4017
+ });
4018
+ }
4007
4019
  const errorMsg = `Failed to clear ${absolutePath}: ${error.message}`;
4008
4020
  console.error(`[CLEAR] ${errorMsg}`);
4009
4021
  return this.manageResponse("clear", absolutePath, "error", errorMsg);
4010
4022
  }
4011
4023
  // Completely remove the cleared codebase from snapshot
4012
4024
  this.snapshotManager.removeCodebaseCompletely(absolutePath);
4025
+ if (typeof this.snapshotManager.markCodebaseCleared === 'function') {
4026
+ this.snapshotManager.markCodebaseCleared(absolutePath, this.context.resolveCollectionName(absolutePath));
4027
+ }
4013
4028
  // Reset indexing stats if this was the active codebase
4014
4029
  this.indexingStats = null;
4015
4030
  // Save snapshot after clearing index
@@ -1,7 +1,7 @@
1
1
  import { WarningCode } from "./warnings.js";
2
2
  export type ManageIndexAction = "create" | "reindex" | "sync" | "status" | "clear";
3
3
  export type ManageIndexStatus = "ok" | "not_ready" | "not_indexed" | "requires_reindex" | "blocked" | "error";
4
- export type ManageIndexReason = "indexing" | "not_indexed" | "requires_reindex" | "unnecessary_reindex_ignore_only" | "preflight_unknown";
4
+ export type ManageIndexReason = "indexing" | "not_indexed" | "requires_reindex" | "unnecessary_reindex_ignore_only" | "preflight_unknown" | "backend_timeout" | "remote_delete_pending";
5
5
  export type ManageReindexPreflightOutcome = "reindex_required" | "reindex_unnecessary_ignore_only" | "unknown" | "probe_failed";
6
6
  export interface ManageIndexToolHint {
7
7
  tool: "manage_index";
@@ -16,7 +16,9 @@ export declare class SnapshotManager {
16
16
  private indexingCodebases;
17
17
  private codebaseFileCount;
18
18
  private codebaseInfoMap;
19
+ private clearTombstones;
19
20
  private pendingRemovals;
21
+ private pendingTombstoneRemovals;
20
22
  private isDirty;
21
23
  private runtimeFingerprint;
22
24
  constructor(runtimeFingerprint: IndexFingerprint);
@@ -41,16 +43,21 @@ export declare class SnapshotManager {
41
43
  private releaseSnapshotLock;
42
44
  private isValidIndexFingerprint;
43
45
  private isValidCodebaseInfoShape;
46
+ private isValidClearTombstoneShape;
44
47
  private toCodebaseInfo;
45
48
  private mapFromV1Snapshot;
46
49
  private mapFromV2Snapshot;
47
50
  private mapFromV3Snapshot;
51
+ private tombstoneMapFromV3Snapshot;
48
52
  private mapToCodebaseRecord;
53
+ private mapToTombstoneRecord;
49
54
  private codebaseRecordsEqual;
50
55
  private codebaseRecordEqualsUnknown;
51
56
  private canonicalizeUnknownRecord;
52
57
  private readCodebaseMapFromDisk;
58
+ private readTombstoneMapFromDisk;
53
59
  private mergeWithPersistedSnapshot;
60
+ private mergeTombstonesWithPersistedSnapshot;
54
61
  private loadV1Format;
55
62
  private loadV2Format;
56
63
  private loadV3Format;
@@ -67,6 +74,8 @@ export declare class SnapshotManager {
67
74
  path: string;
68
75
  info: CodebaseInfo;
69
76
  }>;
77
+ markCodebaseCleared(codebasePath: string, collectionName?: string): void;
78
+ isCodebaseCleared(codebasePath: string, collectionName?: string): boolean;
70
79
  getFailedCodebases(): string[];
71
80
  getCodebasesRequiringReindex(): string[];
72
81
  setCodebaseIndexing(codebasePath: string, progress?: number): void;
@@ -56,7 +56,9 @@ export class SnapshotManager {
56
56
  this.indexingCodebases = new Map();
57
57
  this.codebaseFileCount = new Map();
58
58
  this.codebaseInfoMap = new Map();
59
+ this.clearTombstones = new Map();
59
60
  this.pendingRemovals = new Set();
61
+ this.pendingTombstoneRemovals = new Set();
60
62
  this.isDirty = false;
61
63
  this.runtimeFingerprint = runtimeFingerprint;
62
64
  this.snapshotFilePath = path.join(os.homedir(), '.satori', 'mcp-codebase-snapshot.json');
@@ -110,6 +112,8 @@ export class SnapshotManager {
110
112
  }
111
113
  markCodebasePresent(codebasePath) {
112
114
  this.pendingRemovals.delete(codebasePath);
115
+ this.clearTombstones.delete(codebasePath);
116
+ this.pendingTombstoneRemovals.add(codebasePath);
113
117
  }
114
118
  markDirty() {
115
119
  this.isDirty = true;
@@ -356,6 +360,12 @@ export class SnapshotManager {
356
360
  return false;
357
361
  }
358
362
  }
363
+ isValidClearTombstoneShape(value) {
364
+ return isRecord(value)
365
+ && typeof value.clearedAt === "string"
366
+ && !Number.isNaN(Date.parse(value.clearedAt))
367
+ && (value.collectionName === undefined || typeof value.collectionName === "string");
368
+ }
359
369
  toCodebaseInfo(rawInfo, sourceLabel, codebasePath) {
360
370
  if (!this.isValidCodebaseInfoShape(rawInfo)) {
361
371
  console.warn(`[SNAPSHOT] Skipping malformed ${sourceLabel} entry for '${codebasePath}'`);
@@ -413,6 +423,20 @@ export class SnapshotManager {
413
423
  }
414
424
  return map;
415
425
  }
426
+ tombstoneMapFromV3Snapshot(snapshot) {
427
+ const map = new Map();
428
+ if (!isRecord(snapshot.clearTombstones)) {
429
+ return map;
430
+ }
431
+ for (const [codebasePath, rawTombstone] of Object.entries(snapshot.clearTombstones)) {
432
+ if (!this.isValidClearTombstoneShape(rawTombstone)) {
433
+ console.warn(`[SNAPSHOT] Skipping malformed clear tombstone for '${codebasePath}'`);
434
+ continue;
435
+ }
436
+ map.set(codebasePath, rawTombstone);
437
+ }
438
+ return map;
439
+ }
416
440
  mapToCodebaseRecord(map) {
417
441
  const entries = Array.from(map.entries()).sort(([a], [b]) => a.localeCompare(b));
418
442
  const codebases = {};
@@ -421,6 +445,17 @@ export class SnapshotManager {
421
445
  }
422
446
  return codebases;
423
447
  }
448
+ mapToTombstoneRecord(map) {
449
+ if (map.size === 0) {
450
+ return undefined;
451
+ }
452
+ const entries = Array.from(map.entries()).sort(([a], [b]) => a.localeCompare(b));
453
+ const tombstones = {};
454
+ for (const [codebasePath, tombstone] of entries) {
455
+ tombstones[codebasePath] = tombstone;
456
+ }
457
+ return tombstones;
458
+ }
424
459
  codebaseRecordsEqual(left, right) {
425
460
  return stableSerialize(left) === stableSerialize(right);
426
461
  }
@@ -457,6 +492,22 @@ export class SnapshotManager {
457
492
  return new Map();
458
493
  }
459
494
  }
495
+ readTombstoneMapFromDisk() {
496
+ if (!fs.existsSync(this.snapshotFilePath)) {
497
+ return new Map();
498
+ }
499
+ try {
500
+ const snapshotData = fs.readFileSync(this.snapshotFilePath, "utf8");
501
+ const snapshot = JSON.parse(snapshotData);
502
+ if (this.isV3Format(snapshot)) {
503
+ return this.tombstoneMapFromV3Snapshot(snapshot);
504
+ }
505
+ }
506
+ catch (error) {
507
+ console.warn("[SNAPSHOT] Unable to read persisted clear tombstones for merge:", error?.message || error);
508
+ }
509
+ return new Map();
510
+ }
460
511
  mergeWithPersistedSnapshot() {
461
512
  const merged = this.readCodebaseMapFromDisk();
462
513
  for (const removedPath of this.pendingRemovals) {
@@ -472,19 +523,32 @@ export class SnapshotManager {
472
523
  }
473
524
  return merged;
474
525
  }
526
+ mergeTombstonesWithPersistedSnapshot() {
527
+ const merged = this.readTombstoneMapFromDisk();
528
+ for (const removedPath of this.pendingTombstoneRemovals) {
529
+ merged.delete(removedPath);
530
+ }
531
+ for (const [codebasePath, tombstone] of this.clearTombstones.entries()) {
532
+ merged.set(codebasePath, tombstone);
533
+ }
534
+ return merged;
535
+ }
475
536
  loadV1Format(snapshot) {
476
537
  console.log('[SNAPSHOT] Loading v1 format snapshot');
477
538
  this.codebaseInfoMap = this.mapFromV1Snapshot(snapshot);
539
+ this.clearTombstones.clear();
478
540
  this.refreshDerivedState();
479
541
  }
480
542
  loadV2Format(snapshot) {
481
543
  console.log('[SNAPSHOT] Loading v2 format snapshot');
482
544
  this.codebaseInfoMap = this.mapFromV2Snapshot(snapshot);
545
+ this.clearTombstones.clear();
483
546
  this.refreshDerivedState();
484
547
  }
485
548
  loadV3Format(snapshot) {
486
549
  console.log('[SNAPSHOT] Loading v3 format snapshot');
487
550
  this.codebaseInfoMap = this.mapFromV3Snapshot(snapshot);
551
+ this.clearTombstones = this.tombstoneMapFromV3Snapshot(snapshot);
488
552
  this.refreshDerivedState();
489
553
  }
490
554
  quarantineCorruptSnapshot(error) {
@@ -522,6 +586,7 @@ export class SnapshotManager {
522
586
  console.log('[SNAPSHOT] Loading codebase snapshot from:', this.snapshotFilePath);
523
587
  try {
524
588
  this.pendingRemovals.clear();
589
+ this.pendingTombstoneRemovals.clear();
525
590
  this.isDirty = false;
526
591
  if (!fs.existsSync(this.snapshotFilePath)) {
527
592
  console.log('[SNAPSHOT] Snapshot file does not exist. Starting with empty codebase list.');
@@ -547,6 +612,7 @@ export class SnapshotManager {
547
612
  else {
548
613
  this.quarantineCorruptSnapshot(new Error('Snapshot format is malformed'));
549
614
  this.codebaseInfoMap.clear();
615
+ this.clearTombstones.clear();
550
616
  this.refreshDerivedState();
551
617
  this.isDirty = false;
552
618
  return;
@@ -559,13 +625,15 @@ export class SnapshotManager {
559
625
  catch (error) {
560
626
  this.quarantineCorruptSnapshot(error);
561
627
  this.codebaseInfoMap.clear();
628
+ this.clearTombstones.clear();
562
629
  this.pendingRemovals.clear();
630
+ this.pendingTombstoneRemovals.clear();
563
631
  this.refreshDerivedState();
564
632
  this.isDirty = false;
565
633
  }
566
634
  }
567
635
  saveCodebaseSnapshot(forceWrite = false) {
568
- if (!forceWrite && !this.isDirty && this.pendingRemovals.size === 0) {
636
+ if (!forceWrite && !this.isDirty && this.pendingRemovals.size === 0 && this.pendingTombstoneRemovals.size === 0) {
569
637
  return;
570
638
  }
571
639
  let lockHandle = null;
@@ -581,17 +649,21 @@ export class SnapshotManager {
581
649
  return;
582
650
  }
583
651
  const mergedCodebaseMap = this.mergeWithPersistedSnapshot();
652
+ const mergedTombstones = this.mergeTombstonesWithPersistedSnapshot();
584
653
  const codebases = this.mapToCodebaseRecord(mergedCodebaseMap);
585
654
  const snapshot = {
586
655
  formatVersion: 'v3',
587
656
  codebases,
657
+ clearTombstones: this.mapToTombstoneRecord(mergedTombstones),
588
658
  lastUpdated: new Date().toISOString(),
589
659
  };
590
660
  tempSnapshotPath = `${this.snapshotFilePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
591
661
  fs.writeFileSync(tempSnapshotPath, JSON.stringify(snapshot, null, 2));
592
662
  fs.renameSync(tempSnapshotPath, this.snapshotFilePath);
593
663
  this.codebaseInfoMap = mergedCodebaseMap;
664
+ this.clearTombstones = mergedTombstones;
594
665
  this.pendingRemovals.clear();
666
+ this.pendingTombstoneRemovals.clear();
595
667
  this.isDirty = false;
596
668
  this.refreshDerivedState();
597
669
  console.log(`[SNAPSHOT] Snapshot saved in v3 format. Indexed: ${this.indexedCodebases.length}, Indexing: ${this.indexingCodebases.size}, Failed: ${this.getFailedCodebases().length}, RequiresReindex: ${this.getCodebasesRequiringReindex().length}`);
@@ -637,6 +709,25 @@ export class SnapshotManager {
637
709
  getAllCodebases() {
638
710
  return Array.from(this.codebaseInfoMap.entries()).map(([p, info]) => ({ path: p, info }));
639
711
  }
712
+ markCodebaseCleared(codebasePath, collectionName) {
713
+ this.removeCodebaseCompletely(codebasePath);
714
+ this.clearTombstones.set(codebasePath, {
715
+ clearedAt: new Date().toISOString(),
716
+ collectionName,
717
+ });
718
+ this.pendingTombstoneRemovals.delete(codebasePath);
719
+ this.markDirty();
720
+ }
721
+ isCodebaseCleared(codebasePath, collectionName) {
722
+ const tombstone = this.clearTombstones.get(codebasePath);
723
+ if (!tombstone) {
724
+ return false;
725
+ }
726
+ if (collectionName === undefined || tombstone.collectionName === undefined) {
727
+ return true;
728
+ }
729
+ return tombstone.collectionName === collectionName;
730
+ }
640
731
  getFailedCodebases() {
641
732
  return Array.from(this.codebaseInfoMap.entries())
642
733
  .filter(([_, info]) => info.status === 'indexfailed')
package/dist/embedding.js CHANGED
@@ -3,7 +3,7 @@ import { OpenAIEmbedding, VoyageAIEmbedding, GeminiEmbedding, OllamaEmbedding }
3
3
  export function createEmbeddingInstance(config) {
4
4
  console.log(`[EMBEDDING] Creating ${config.encoderProvider} embedding instance...`);
5
5
  switch (config.encoderProvider) {
6
- case 'OpenAI':
6
+ case 'OpenAI': {
7
7
  if (!config.openaiKey) {
8
8
  console.error(`[EMBEDDING] ❌ OpenAI API key is required but not provided`);
9
9
  throw new Error('OPENAI_API_KEY is required for OpenAI embedding provider');
@@ -16,7 +16,8 @@ export function createEmbeddingInstance(config) {
16
16
  });
17
17
  console.log(`[EMBEDDING] ✅ OpenAI embedding instance created successfully`);
18
18
  return openaiEmbedding;
19
- case 'VoyageAI':
19
+ }
20
+ case 'VoyageAI': {
20
21
  if (!config.voyageKey) {
21
22
  console.error(`[EMBEDDING] ❌ VoyageAI API key is required but not provided`);
22
23
  throw new Error('VOYAGEAI_API_KEY is required for VoyageAI embedding provider');
@@ -32,7 +33,8 @@ export function createEmbeddingInstance(config) {
32
33
  });
33
34
  console.log(`[EMBEDDING] ✅ VoyageAI embedding instance created successfully`);
34
35
  return voyageEmbedding;
35
- case 'Gemini':
36
+ }
37
+ case 'Gemini': {
36
38
  if (!config.geminiKey) {
37
39
  console.error(`[EMBEDDING] ❌ Gemini API key is required but not provided`);
38
40
  throw new Error('GEMINI_API_KEY is required for Gemini embedding provider');
@@ -45,7 +47,8 @@ export function createEmbeddingInstance(config) {
45
47
  });
46
48
  console.log(`[EMBEDDING] ✅ Gemini embedding instance created successfully`);
47
49
  return geminiEmbedding;
48
- case 'Ollama':
50
+ }
51
+ case 'Ollama': {
49
52
  const ollamaEndpoint = config.ollamaEndpoint || 'http://127.0.0.1:11434';
50
53
  console.log(`[EMBEDDING] 🔧 Configuring Ollama with model: ${config.encoderModel}, host: ${ollamaEndpoint}`);
51
54
  const ollamaEmbedding = new OllamaEmbedding({
@@ -54,6 +57,7 @@ export function createEmbeddingInstance(config) {
54
57
  });
55
58
  console.log(`[EMBEDDING] ✅ Ollama embedding instance created successfully`);
56
59
  return ollamaEmbedding;
60
+ }
57
61
  default:
58
62
  console.error(`[EMBEDDING] ❌ Unsupported embedding provider: ${config.encoderProvider}`);
59
63
  throw new Error(`Unsupported embedding provider: ${config.encoderProvider}`);
@@ -1,94 +1,8 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
1
  import { z } from "zod";
4
2
  import { formatZodError } from "./types.js";
3
+ import { validateCompletionProof } from "../core/completion-proof.js";
5
4
  const listCodebasesInputSchema = z.object({}).strict();
6
5
  const comparePathAsc = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
7
- function trimTrailingSeparators(inputPath) {
8
- const normalized = path.normalize(inputPath);
9
- const parsedRoot = path.parse(normalized).root;
10
- if (normalized === parsedRoot) {
11
- return normalized;
12
- }
13
- return normalized.replace(/[\\/]+$/, "");
14
- }
15
- function canonicalizeCodebasePath(codebasePath) {
16
- const resolved = path.resolve(codebasePath);
17
- try {
18
- const realPath = typeof fs.realpathSync.native === "function"
19
- ? fs.realpathSync.native(resolved)
20
- : fs.realpathSync(resolved);
21
- return trimTrailingSeparators(path.normalize(realPath));
22
- }
23
- catch {
24
- return trimTrailingSeparators(path.normalize(resolved));
25
- }
26
- }
27
- function markerMatchesRuntimeFingerprint(marker, ctx) {
28
- const runtimeFingerprint = ctx.runtimeFingerprint;
29
- if (!runtimeFingerprint || typeof runtimeFingerprint !== "object") {
30
- return true;
31
- }
32
- const fingerprint = marker?.fingerprint;
33
- if (!fingerprint || typeof fingerprint !== "object") {
34
- return false;
35
- }
36
- return fingerprint.embeddingProvider === runtimeFingerprint.embeddingProvider
37
- && fingerprint.embeddingModel === runtimeFingerprint.embeddingModel
38
- && Number(fingerprint.embeddingDimension) === Number(runtimeFingerprint.embeddingDimension)
39
- && fingerprint.vectorStoreProvider === runtimeFingerprint.vectorStoreProvider
40
- && fingerprint.schemaVersion === runtimeFingerprint.schemaVersion;
41
- }
42
- function validateMarkerShape(expectedCodebasePath, marker) {
43
- if (!marker || typeof marker !== "object") {
44
- return { ok: false, reason: "invalid_payload" };
45
- }
46
- if (marker.kind !== "satori_index_completion_v1") {
47
- return { ok: false, reason: "invalid_marker_kind" };
48
- }
49
- if (typeof marker.codebasePath !== "string" || marker.codebasePath.trim().length === 0) {
50
- return { ok: false, reason: "invalid_payload" };
51
- }
52
- if (!marker.fingerprint || typeof marker.fingerprint !== "object") {
53
- return { ok: false, reason: "invalid_payload" };
54
- }
55
- if (!Number.isFinite(Number(marker.indexedFiles)) || !Number.isFinite(Number(marker.totalChunks))) {
56
- return { ok: false, reason: "invalid_payload" };
57
- }
58
- if (typeof marker.completedAt !== "string" || Number.isNaN(Date.parse(marker.completedAt))) {
59
- return { ok: false, reason: "invalid_payload" };
60
- }
61
- const expectedCanonical = canonicalizeCodebasePath(expectedCodebasePath);
62
- const markerCanonical = canonicalizeCodebasePath(marker.codebasePath);
63
- if (expectedCanonical !== markerCanonical) {
64
- return { ok: false, reason: "path_mismatch" };
65
- }
66
- return { ok: true };
67
- }
68
- async function validateCompletionProof(codebasePath, ctx) {
69
- const getMarker = ctx.context?.getIndexCompletionMarker;
70
- if (typeof getMarker !== "function") {
71
- return { outcome: "probe_failed", reason: "probe_failed" };
72
- }
73
- let marker;
74
- try {
75
- marker = await getMarker(codebasePath);
76
- }
77
- catch {
78
- return { outcome: "probe_failed", reason: "probe_failed" };
79
- }
80
- if (!marker) {
81
- return { outcome: "stale_local", reason: "missing_marker_doc" };
82
- }
83
- const markerShape = validateMarkerShape(codebasePath, marker);
84
- if (!markerShape.ok) {
85
- return { outcome: "stale_local", reason: markerShape.reason };
86
- }
87
- if (!markerMatchesRuntimeFingerprint(marker, ctx)) {
88
- return { outcome: "fingerprint_mismatch", reason: "fingerprint_mismatch" };
89
- }
90
- return { outcome: "valid" };
91
- }
92
6
  export const listCodebasesTool = {
93
7
  name: "list_codebases",
94
8
  description: () => "List tracked codebases and their indexing state.",
@@ -121,7 +35,13 @@ export const listCodebasesTool = {
121
35
  .filter((e) => e.info.status === "indexed" || e.info.status === "sync_completed");
122
36
  const completionProofChecks = await Promise.all(readyCandidates.map(async (entry) => ({
123
37
  entry,
124
- proof: await validateCompletionProof(entry.path, ctx)
38
+ proof: await validateCompletionProof({
39
+ codebasePath: entry.path,
40
+ runtimeFingerprint: ctx.runtimeFingerprint,
41
+ getIndexCompletionMarker: typeof ctx.context?.getIndexCompletionMarker === "function"
42
+ ? (markerPath) => ctx.context.getIndexCompletionMarker(markerPath)
43
+ : undefined
44
+ })
125
45
  })));
126
46
  const ready = [];
127
47
  const requiresReindex = all
@@ -147,7 +67,7 @@ export const listCodebasesTool = {
147
67
  }
148
68
  if (proof.outcome === "probe_failed") {
149
69
  // Probe failure is non-authoritative: keep local ready status stable.
150
- ready.push({ path: entry.path });
70
+ ready.push({ path: entry.path, probeFailed: true });
151
71
  continue;
152
72
  }
153
73
  if (proof.outcome === "fingerprint_mismatch") {
@@ -168,7 +88,10 @@ export const listCodebasesTool = {
168
88
  if (byStatus.indexed.length > 0) {
169
89
  lines.push('### Ready');
170
90
  for (const item of byStatus.indexed) {
171
- lines.push(`- \`${item.path}\``);
91
+ const suffix = item.probeFailed
92
+ ? " (completion proof probe failed; verify with manage_index action='status')"
93
+ : "";
94
+ lines.push(`- \`${item.path}\`${suffix}`);
172
95
  }
173
96
  lines.push('');
174
97
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zokizuan/satori-mcp",
3
- "version": "4.6.0",
3
+ "version": "4.8.0",
4
4
  "description": "MCP server for Satori with agent-safe semantic search and indexing",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -14,7 +14,7 @@
14
14
  "ignore": "^7.0.5",
15
15
  "zod": "^3.25.55",
16
16
  "zod-to-json-schema": "^3.25.1",
17
- "@zokizuan/satori-core": "1.3.0"
17
+ "@zokizuan/satori-core": "1.5.0"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@types/node": "^20.0.0",