@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 +6 -6
- package/dist/cli/package-installability.js +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.js +5 -5
- package/dist/core/completion-proof.d.ts +24 -0
- package/dist/core/completion-proof.js +106 -0
- package/dist/core/handlers.d.ts +2 -4
- package/dist/core/handlers.js +126 -111
- package/dist/core/manage-types.d.ts +1 -1
- package/dist/core/snapshot.d.ts +9 -0
- package/dist/core/snapshot.js +92 -1
- package/dist/embedding.js +8 -4
- package/dist/tools/list_codebases.js +13 -90
- package/package.json +2 -2
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.
|
|
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.
|
|
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.
|
|
232
|
-
npx -y @zokizuan/satori-cli@0.
|
|
233
|
-
npx -y @zokizuan/satori-cli@0.
|
|
234
|
-
npx -y @zokizuan/satori-cli@0.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
package/dist/core/handlers.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/handlers.js
CHANGED
|
@@ -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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
2308
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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 ${
|
|
2363
|
+
console.log(`[SYNC-CLOUD] 📊 Found ${cloudCodebaseCollections.size} valid codebases in cloud`);
|
|
2417
2364
|
let hasChanges = false;
|
|
2418
|
-
for (const cloudCodebasePath of
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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";
|
package/dist/core/snapshot.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/snapshot.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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.
|
|
17
|
+
"@zokizuan/satori-core": "1.5.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^20.0.0",
|