@gmickel/gno 2.12.2 → 2.14.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 +1 -1
- package/assets/skill/SKILL.md +36 -5
- package/assets/skill/cli-reference.md +62 -11
- package/assets/skill/examples.md +30 -4
- package/assets/skill/mcp-reference.md +16 -0
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.12.2.zip → gno-browser-clipper-v2.14.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.14.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +139 -16
- package/spec/mcp.md +111 -3
- package/spec/output-schemas/collection-list.schema.json +28 -0
- package/spec/output-schemas/convert.schema.json +97 -0
- package/spec/output-schemas/ls.schema.json +65 -0
- package/spec/output-schemas/mcp-job-status.schema.json +17 -1
- package/spec/output-schemas/mcp-sync-result.schema.json +9 -2
- package/spec/output-schemas/search-results.schema.json +7 -0
- package/src/cli/commands/index-cmd.ts +50 -12
- package/src/cli/commands/ls.ts +20 -3
- package/src/cli/commands/profile-apply.ts +2 -1
- package/src/cli/commands/query.ts +21 -8
- package/src/cli/commands/search.ts +11 -0
- package/src/cli/commands/vsearch.ts +17 -4
- package/src/cli/options.ts +19 -0
- package/src/cli/program.ts +90 -10
- package/src/config/loader.ts +11 -1
- package/src/config/types.ts +90 -1
- package/src/core/audit-collection-rules.ts +331 -0
- package/src/core/audit-links.ts +3 -1
- package/src/core/audit-workspace.ts +69 -5
- package/src/core/collection-egress-policy-service.ts +4 -1
- package/src/core/collection-scope.ts +105 -0
- package/src/core/egress-enforcement.ts +20 -4
- package/src/core/egress-policy.ts +4 -0
- package/src/core/egress-provenance.ts +33 -11
- package/src/core/errors.ts +5 -0
- package/src/core/job-manager.ts +238 -19
- package/src/core/project-profile-apply-state.ts +6 -15
- package/src/core/project-profile-apply.ts +32 -1
- package/src/core/reindex-paths.ts +167 -0
- package/src/core/retrieval-trace-request.ts +1 -0
- package/src/ingestion/convert-file.ts +181 -0
- package/src/llm/registry.ts +20 -0
- package/src/mcp/http-egress.ts +21 -2
- package/src/mcp/tools/collection-scope.ts +25 -0
- package/src/mcp/tools/index-cmd.ts +66 -48
- package/src/mcp/tools/index.ts +74 -10
- package/src/mcp/tools/job-status.ts +7 -0
- package/src/mcp/tools/query.ts +28 -7
- package/src/mcp/tools/search.ts +4 -0
- package/src/mcp/tools/sync.ts +68 -60
- package/src/mcp/tools/vsearch.ts +9 -3
- package/src/pipeline/filters.ts +48 -5
- package/src/pipeline/graph-retrieval.ts +1 -0
- package/src/pipeline/hybrid.ts +15 -2
- package/src/pipeline/hydration.ts +7 -2
- package/src/pipeline/owner-fusion.ts +1 -0
- package/src/pipeline/search.ts +2 -0
- package/src/pipeline/types.ts +7 -0
- package/src/pipeline/vsearch.ts +10 -5
- package/src/sdk/client.ts +45 -12
- package/src/sdk/convert.ts +27 -0
- package/src/sdk/documents.ts +15 -2
- package/src/sdk/index.ts +2 -0
- package/src/sdk/types.ts +4 -1
- package/src/serve/jobs.ts +4 -1
- package/src/serve/public/components/IndexingProgress.tsx +25 -3
- package/src/serve/routes/api.ts +164 -24
- package/src/store/sqlite/adapter.ts +97 -27
- package/src/store/sqlite/eligibility.ts +4 -0
- package/src/store/types.ts +7 -0
- package/src/store/vector/sqlite-vec.ts +67 -15
- package/src/store/vector/variant-search.ts +41 -4
- package/browser-extension/artifacts/gno-browser-clipper-v2.12.2.zip.sha256 +0 -1
package/src/mcp/tools/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
readMetadataSummary,
|
|
28
28
|
} from "../../core/metadata-discovery";
|
|
29
29
|
import { NOTE_PRESETS, type NotePresetId } from "../../core/note-presets";
|
|
30
|
+
import { MAX_REINDEX_PATHS } from "../../core/reindex-paths";
|
|
30
31
|
import { RETRIEVAL_TRACE_METADATA } from "../../core/retrieval-trace-session";
|
|
31
32
|
import { normalizeTag } from "../../core/tags";
|
|
32
33
|
import {
|
|
@@ -255,6 +256,12 @@ export const searchInputSchema = z.object({
|
|
|
255
256
|
.string()
|
|
256
257
|
.optional()
|
|
257
258
|
.describe("Filter to a single collection name"),
|
|
259
|
+
collections: z
|
|
260
|
+
.array(z.string())
|
|
261
|
+
.optional()
|
|
262
|
+
.describe(
|
|
263
|
+
"Filter to any of these collection names (1-256); use instead of collection"
|
|
264
|
+
),
|
|
258
265
|
limit: z
|
|
259
266
|
.number()
|
|
260
267
|
.int()
|
|
@@ -437,6 +444,13 @@ const syncInputSchema = z.object({
|
|
|
437
444
|
.boolean()
|
|
438
445
|
.default(false)
|
|
439
446
|
.describe("Run the collection's configured update command before syncing"),
|
|
447
|
+
paths: z
|
|
448
|
+
.array(z.string())
|
|
449
|
+
.max(MAX_REINDEX_PATHS)
|
|
450
|
+
.optional()
|
|
451
|
+
.describe(
|
|
452
|
+
"Reindex only these files (1-256, relative to the collection root, or absolute inside it). Requires collection"
|
|
453
|
+
),
|
|
440
454
|
});
|
|
441
455
|
|
|
442
456
|
const embedInputSchema = z.object({
|
|
@@ -452,6 +466,13 @@ const indexInputSchema = z.object({
|
|
|
452
466
|
.optional()
|
|
453
467
|
.describe("Collection name to index. Omit to index all collections"),
|
|
454
468
|
gitPull: z.boolean().default(false).describe("Run git pull before indexing"),
|
|
469
|
+
paths: z
|
|
470
|
+
.array(z.string())
|
|
471
|
+
.max(MAX_REINDEX_PATHS)
|
|
472
|
+
.optional()
|
|
473
|
+
.describe(
|
|
474
|
+
"Reindex only these files (1-256, relative to the collection root, or absolute inside it). Requires collection"
|
|
475
|
+
),
|
|
455
476
|
});
|
|
456
477
|
|
|
457
478
|
const removeCollectionInputSchema = z.object({
|
|
@@ -496,6 +517,12 @@ export const vsearchInputSchema = z.object({
|
|
|
496
517
|
.string()
|
|
497
518
|
.optional()
|
|
498
519
|
.describe("Filter to a single collection name"),
|
|
520
|
+
collections: z
|
|
521
|
+
.array(z.string())
|
|
522
|
+
.optional()
|
|
523
|
+
.describe(
|
|
524
|
+
"Filter to any of these collection names (1-256); use instead of collection"
|
|
525
|
+
),
|
|
499
526
|
limit: z
|
|
500
527
|
.number()
|
|
501
528
|
.int()
|
|
@@ -570,6 +597,12 @@ export const queryInputSchema = z.object({
|
|
|
570
597
|
.string()
|
|
571
598
|
.optional()
|
|
572
599
|
.describe("Filter to a single collection name"),
|
|
600
|
+
collections: z
|
|
601
|
+
.array(z.string())
|
|
602
|
+
.optional()
|
|
603
|
+
.describe(
|
|
604
|
+
"Filter to any of these collection names (1-256); use instead of collection"
|
|
605
|
+
),
|
|
573
606
|
limit: z
|
|
574
607
|
.number()
|
|
575
608
|
.int()
|
|
@@ -672,15 +705,17 @@ export const queryInputSchema = z.object({
|
|
|
672
705
|
tagsAny: z.array(z.string()).optional().describe("Require ANY of these tags"),
|
|
673
706
|
});
|
|
674
707
|
|
|
675
|
-
export const queryDiagnoseInputSchema = queryInputSchema
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
"Target
|
|
682
|
-
|
|
683
|
-
|
|
708
|
+
export const queryDiagnoseInputSchema = queryInputSchema
|
|
709
|
+
.omit({ collections: true })
|
|
710
|
+
.extend({
|
|
711
|
+
target: z
|
|
712
|
+
.string()
|
|
713
|
+
.trim()
|
|
714
|
+
.min(1, "Target reference cannot be empty")
|
|
715
|
+
.describe(
|
|
716
|
+
"Target document reference to diagnose (gno URI, docid, or collection/path)"
|
|
717
|
+
),
|
|
718
|
+
});
|
|
684
719
|
|
|
685
720
|
const getInputSchema = z.object({
|
|
686
721
|
ref: z
|
|
@@ -1019,7 +1054,8 @@ export async function runTool<T>(
|
|
|
1019
1054
|
try {
|
|
1020
1055
|
assertConfiguredCollections(ctx, name);
|
|
1021
1056
|
assertInferenceActive();
|
|
1022
|
-
const
|
|
1057
|
+
const read = READ_CONNECTION_TOOLS.has(name) ? readConnection(ctx, fn) : fn;
|
|
1058
|
+
const raw = await (ctx.runWithSnapshot?.(read) ?? read());
|
|
1023
1059
|
assertInferenceActive();
|
|
1024
1060
|
const traceMetadata =
|
|
1025
1061
|
raw !== null && typeof raw === "object"
|
|
@@ -1048,6 +1084,34 @@ export async function runTool<T>(
|
|
|
1048
1084
|
}
|
|
1049
1085
|
}
|
|
1050
1086
|
|
|
1087
|
+
/**
|
|
1088
|
+
* Read-only retrieval tools. They read on the store's read connection, so a
|
|
1089
|
+
* sync batch that is still writing is never visible to them.
|
|
1090
|
+
*/
|
|
1091
|
+
const READ_CONNECTION_TOOLS = new Set([
|
|
1092
|
+
"gno_search",
|
|
1093
|
+
"gno_vsearch",
|
|
1094
|
+
"gno_query",
|
|
1095
|
+
"gno_get",
|
|
1096
|
+
"gno_multi_get",
|
|
1097
|
+
"gno_status",
|
|
1098
|
+
"gno_list_tags",
|
|
1099
|
+
"gno_links",
|
|
1100
|
+
"gno_backlinks",
|
|
1101
|
+
"gno_similar",
|
|
1102
|
+
]);
|
|
1103
|
+
|
|
1104
|
+
/** Store test doubles may lack the read connection; they read as before. */
|
|
1105
|
+
function readConnection<T>(
|
|
1106
|
+
ctx: ToolContext,
|
|
1107
|
+
fn: () => Promise<T>
|
|
1108
|
+
): () => Promise<T> {
|
|
1109
|
+
const store = ctx.store as Partial<ToolContext["store"]> | undefined;
|
|
1110
|
+
return typeof store?.withReadConnection === "function"
|
|
1111
|
+
? () => ctx.store.withReadConnection(fn)
|
|
1112
|
+
: fn;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1051
1115
|
/**
|
|
1052
1116
|
* Run a tool without acquiring the mutex.
|
|
1053
1117
|
* For read-only in-memory tools (job_status, list_jobs) that should not block.
|
|
@@ -24,6 +24,7 @@ interface JobStatusResult {
|
|
|
24
24
|
result?: SyncResult;
|
|
25
25
|
typedResult?: JobResult;
|
|
26
26
|
error?: string;
|
|
27
|
+
scope?: JobRecord["scope"];
|
|
27
28
|
serverInstanceId: string;
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -34,6 +35,11 @@ function formatJobStatus(result: JobStatusResult): string {
|
|
|
34
35
|
lines.push(`Type: ${result.type}`);
|
|
35
36
|
lines.push(`Status: ${result.status}`);
|
|
36
37
|
lines.push(`Started: ${result.startedAt}`);
|
|
38
|
+
if (result.scope) {
|
|
39
|
+
lines.push(`Collection: ${result.scope.collection ?? "(all)"}`);
|
|
40
|
+
if (result.scope.paths)
|
|
41
|
+
lines.push(`Paths: ${result.scope.paths.join(", ")}`);
|
|
42
|
+
}
|
|
37
43
|
|
|
38
44
|
if (result.completedAt) {
|
|
39
45
|
lines.push(`Completed: ${result.completedAt}`);
|
|
@@ -92,6 +98,7 @@ function toJobStatusResult(job: JobRecord): JobStatusResult {
|
|
|
92
98
|
result: job.result,
|
|
93
99
|
typedResult: job.typedResult,
|
|
94
100
|
error: job.error,
|
|
101
|
+
...(job.scope ? { scope: job.scope } : {}),
|
|
95
102
|
serverInstanceId: job.serverInstanceId,
|
|
96
103
|
};
|
|
97
104
|
}
|
package/src/mcp/tools/query.ts
CHANGED
|
@@ -44,17 +44,23 @@ import {
|
|
|
44
44
|
} from "../../core/typed-metadata";
|
|
45
45
|
import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
|
|
46
46
|
import { resolveDownloadPolicy } from "../../llm/policy";
|
|
47
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
getActivePreset,
|
|
49
|
+
modelCollectionForList,
|
|
50
|
+
resolveModelUri,
|
|
51
|
+
} from "../../llm/registry";
|
|
48
52
|
import { diagnoseQueryTarget } from "../../pipeline/diagnose";
|
|
49
53
|
import { type HybridSearchDeps, searchHybrid } from "../../pipeline/hybrid";
|
|
50
54
|
import { createLazyVectorIndex } from "../../store/vector/lazy";
|
|
51
55
|
import { appendRetrievalWarnings } from "../retrieval-warnings";
|
|
56
|
+
import { requestCollections } from "./collection-scope";
|
|
52
57
|
import { normalizeTagFilters, runTool, type ToolResult } from "./index";
|
|
53
58
|
|
|
54
59
|
interface QueryInput {
|
|
55
60
|
query: string;
|
|
56
61
|
projectHints?: string[];
|
|
57
62
|
collection?: string;
|
|
63
|
+
collections?: string[];
|
|
58
64
|
limit?: number;
|
|
59
65
|
minScore?: number;
|
|
60
66
|
lang?: string;
|
|
@@ -198,6 +204,7 @@ export function handleQuery(
|
|
|
198
204
|
throw new Error(`Collection not found: ${args.collection}`);
|
|
199
205
|
}
|
|
200
206
|
}
|
|
207
|
+
const collections = requestCollections(args, ctx);
|
|
201
208
|
|
|
202
209
|
const normalizedInput = normalizeStructuredQueryInput(
|
|
203
210
|
args.query,
|
|
@@ -219,11 +226,14 @@ export function handleQuery(
|
|
|
219
226
|
let rerankPort: RerankPort | null = null;
|
|
220
227
|
let vectorIndex: VectorIndexPort | null = null;
|
|
221
228
|
let traceSession: RetrievalTraceSession | undefined;
|
|
229
|
+
const modelCollection = (type: "embed" | "expand" | "rerank") =>
|
|
230
|
+
args.collection ??
|
|
231
|
+
modelCollectionForList(ctx.config, type, collections);
|
|
222
232
|
const embedUri = resolveModelUri(
|
|
223
233
|
ctx.config,
|
|
224
234
|
"embed",
|
|
225
235
|
undefined,
|
|
226
|
-
|
|
236
|
+
modelCollection("embed")
|
|
227
237
|
);
|
|
228
238
|
const hasStructuredModes = Boolean(queryModes?.length);
|
|
229
239
|
const depthPolicy = resolveDepthPolicy({
|
|
@@ -242,15 +252,26 @@ export function handleQuery(
|
|
|
242
252
|
);
|
|
243
253
|
const expandUri =
|
|
244
254
|
!noExpand && !hasStructuredModes
|
|
245
|
-
? resolveModelUri(
|
|
255
|
+
? resolveModelUri(
|
|
256
|
+
ctx.config,
|
|
257
|
+
"expand",
|
|
258
|
+
undefined,
|
|
259
|
+
modelCollection("expand")
|
|
260
|
+
)
|
|
246
261
|
: undefined;
|
|
247
262
|
const rerankUri = !noRerank
|
|
248
|
-
? resolveModelUri(
|
|
263
|
+
? resolveModelUri(
|
|
264
|
+
ctx.config,
|
|
265
|
+
"rerank",
|
|
266
|
+
undefined,
|
|
267
|
+
modelCollection("rerank")
|
|
268
|
+
)
|
|
249
269
|
: undefined;
|
|
250
270
|
const options = {
|
|
251
271
|
limit: args.limit ?? 5,
|
|
252
272
|
minScore: args.minScore,
|
|
253
273
|
collection: args.collection,
|
|
274
|
+
collections,
|
|
254
275
|
queryLanguageHint: args.lang,
|
|
255
276
|
intent: args.intent,
|
|
256
277
|
candidateLimit: depthPolicy.candidateLimit,
|
|
@@ -289,9 +310,9 @@ export function handleQuery(
|
|
|
289
310
|
if (!traceStart.ok) throw new Error(traceStart.error.message);
|
|
290
311
|
traceSession = traceStart.value ?? undefined;
|
|
291
312
|
const llm = ctx.getModelAdapter?.() ?? new LlmAdapter(ctx.config);
|
|
292
|
-
const egressCollections =
|
|
293
|
-
|
|
294
|
-
: ("all" as const);
|
|
313
|
+
const egressCollections =
|
|
314
|
+
collections ??
|
|
315
|
+
(args.collection ? [args.collection] : ("all" as const));
|
|
295
316
|
|
|
296
317
|
// Resolve download policy from env (MCP has no CLI flags)
|
|
297
318
|
const policy = resolveDownloadPolicy(process.env, {});
|
package/src/mcp/tools/search.ts
CHANGED
|
@@ -25,12 +25,14 @@ import {
|
|
|
25
25
|
} from "../../core/typed-metadata";
|
|
26
26
|
import { searchBm25 } from "../../pipeline/search";
|
|
27
27
|
import { appendRetrievalWarnings } from "../retrieval-warnings";
|
|
28
|
+
import { requestCollections } from "./collection-scope";
|
|
28
29
|
import { normalizeTagFilters, runTool, type ToolResult } from "./index";
|
|
29
30
|
|
|
30
31
|
interface SearchInput {
|
|
31
32
|
query: string;
|
|
32
33
|
projectHints?: string[];
|
|
33
34
|
collection?: string;
|
|
35
|
+
collections?: string[];
|
|
34
36
|
limit?: number;
|
|
35
37
|
minScore?: number;
|
|
36
38
|
lang?: string;
|
|
@@ -123,6 +125,7 @@ export function handleSearch(
|
|
|
123
125
|
throw new Error(`Collection not found: ${args.collection}`);
|
|
124
126
|
}
|
|
125
127
|
}
|
|
128
|
+
const collections = requestCollections(args, ctx);
|
|
126
129
|
|
|
127
130
|
const projectAffinity = await resolveRemoteProjectAffinity(
|
|
128
131
|
ctx.config,
|
|
@@ -132,6 +135,7 @@ export function handleSearch(
|
|
|
132
135
|
limit: args.limit ?? 5,
|
|
133
136
|
minScore: args.minScore,
|
|
134
137
|
collection: args.collection,
|
|
138
|
+
collections,
|
|
135
139
|
lang: args.lang,
|
|
136
140
|
intent: args.intent,
|
|
137
141
|
exclude: args.exclude,
|
package/src/mcp/tools/sync.ts
CHANGED
|
@@ -9,9 +9,9 @@ import type { CollectionSyncResult, SyncResult } from "../../ingestion";
|
|
|
9
9
|
import type { ToolContext } from "../server";
|
|
10
10
|
|
|
11
11
|
import { MCP_ERRORS } from "../../core/errors";
|
|
12
|
-
import { acquireWriteLock, type WriteLockHandle } from "../../core/file-lock";
|
|
13
12
|
import { JobError } from "../../core/job-manager";
|
|
14
13
|
import { recordContentMutation } from "../../core/mutation-generations";
|
|
14
|
+
import { resolveReindexScope } from "../../core/reindex-paths";
|
|
15
15
|
import { normalizeCollectionName } from "../../core/validation";
|
|
16
16
|
import { defaultSyncService, withContentTypeRules } from "../../ingestion";
|
|
17
17
|
import { enforceSyncCommandEgress } from "../sync-egress";
|
|
@@ -21,12 +21,14 @@ interface SyncInput {
|
|
|
21
21
|
collection?: string;
|
|
22
22
|
gitPull?: boolean;
|
|
23
23
|
runUpdateCmd?: boolean;
|
|
24
|
+
paths?: string[];
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
interface SyncResultOutput {
|
|
27
28
|
jobId: string;
|
|
28
29
|
collections: string[];
|
|
29
|
-
status: "started";
|
|
30
|
+
status: "started" | "queued";
|
|
31
|
+
paths?: string[];
|
|
30
32
|
options: {
|
|
31
33
|
gitPull: boolean;
|
|
32
34
|
runUpdateCmd: boolean;
|
|
@@ -38,6 +40,7 @@ function formatSyncResult(result: SyncResultOutput): string {
|
|
|
38
40
|
lines.push(`Job: ${result.jobId}`);
|
|
39
41
|
lines.push(`Status: ${result.status}`);
|
|
40
42
|
lines.push(`Collections: ${result.collections.join(", ") || "(none)"}`);
|
|
43
|
+
if (result.paths) lines.push(`Paths: ${result.paths.join(", ")}`);
|
|
41
44
|
lines.push(`Git pull: ${result.options.gitPull ? "yes" : "no"}`);
|
|
42
45
|
lines.push(`Run update cmd: ${result.options.runUpdateCmd ? "yes" : "no"}`);
|
|
43
46
|
return lines.join("\n");
|
|
@@ -77,73 +80,82 @@ export function handleSync(
|
|
|
77
80
|
throw new Error("Write tools disabled. Start MCP with --enable-write.");
|
|
78
81
|
}
|
|
79
82
|
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
const requestedCollection = args.collection?.trim();
|
|
84
|
+
const collection = requestedCollection
|
|
85
|
+
? resolveCollection(requestedCollection, ctx.collections)
|
|
86
|
+
: null;
|
|
82
87
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
throw new Error(
|
|
87
|
-
`${MCP_ERRORS.LOCKED.code}: ${MCP_ERRORS.LOCKED.message}`
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const requestedCollection = args.collection?.trim();
|
|
92
|
-
const collection = requestedCollection
|
|
93
|
-
? resolveCollection(requestedCollection, ctx.collections)
|
|
94
|
-
: null;
|
|
95
|
-
|
|
96
|
-
if (requestedCollection && !collection) {
|
|
97
|
-
throw new Error(
|
|
98
|
-
`${MCP_ERRORS.NOT_FOUND.code}: Collection not found: ${requestedCollection}`
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const collections = collection
|
|
103
|
-
? [collection.name]
|
|
104
|
-
: ctx.collections.map((entry) => entry.name);
|
|
105
|
-
|
|
106
|
-
const options = withContentTypeRules(
|
|
107
|
-
{
|
|
108
|
-
gitPull: args.gitPull ?? false,
|
|
109
|
-
runUpdateCmd: args.runUpdateCmd ?? false,
|
|
110
|
-
},
|
|
111
|
-
ctx.config
|
|
88
|
+
if (requestedCollection && !collection) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`${MCP_ERRORS.NOT_FOUND.code}: Collection not found: ${requestedCollection}`
|
|
112
91
|
);
|
|
113
|
-
|
|
114
|
-
collectionNames: collections,
|
|
115
|
-
gitPull: options.gitPull,
|
|
116
|
-
runUpdateCmd: options.runUpdateCmd,
|
|
117
|
-
});
|
|
92
|
+
}
|
|
118
93
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
94
|
+
const collections = collection
|
|
95
|
+
? [collection.name]
|
|
96
|
+
: ctx.collections.map((entry) => entry.name);
|
|
97
|
+
|
|
98
|
+
const options = withContentTypeRules(
|
|
99
|
+
{
|
|
100
|
+
gitPull: args.gitPull ?? false,
|
|
101
|
+
runUpdateCmd: args.runUpdateCmd ?? false,
|
|
102
|
+
},
|
|
103
|
+
ctx.config
|
|
104
|
+
);
|
|
105
|
+
const paths = await resolveReindexScope(collection, args.paths, options);
|
|
106
|
+
enforceSyncCommandEgress(ctx, {
|
|
107
|
+
collectionNames: collections,
|
|
108
|
+
gitPull: options.gitPull,
|
|
109
|
+
runUpdateCmd: options.runUpdateCmd,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Captured now: a queued pass runs after this call's context is gone.
|
|
113
|
+
const config = ctx.config;
|
|
114
|
+
const allCollections = ctx.collections;
|
|
115
|
+
try {
|
|
116
|
+
const accepted = await ctx.jobManager.submitReindex({
|
|
117
|
+
type: "sync",
|
|
118
|
+
collection: collection?.name ?? null,
|
|
119
|
+
paths,
|
|
120
|
+
options: {
|
|
121
|
+
gitPull: options.gitPull ?? false,
|
|
122
|
+
runUpdateCmd: options.runUpdateCmd ?? false,
|
|
123
|
+
},
|
|
124
|
+
run: async (scope) => {
|
|
125
|
+
const passOptions = withContentTypeRules(
|
|
126
|
+
{ ...scope.options },
|
|
127
|
+
config
|
|
128
|
+
);
|
|
123
129
|
const result = collection
|
|
124
130
|
? wrapSingleSync(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
131
|
+
scope.paths
|
|
132
|
+
? await defaultSyncService.syncPaths(
|
|
133
|
+
collection,
|
|
134
|
+
ctx.store,
|
|
135
|
+
scope.paths,
|
|
136
|
+
passOptions
|
|
137
|
+
)
|
|
138
|
+
: await defaultSyncService.syncCollection(
|
|
139
|
+
collection,
|
|
140
|
+
ctx.store,
|
|
141
|
+
passOptions
|
|
142
|
+
)
|
|
130
143
|
)
|
|
131
144
|
: await defaultSyncService.syncAll(
|
|
132
|
-
|
|
145
|
+
allCollections,
|
|
133
146
|
ctx.store,
|
|
134
|
-
|
|
147
|
+
passOptions
|
|
135
148
|
);
|
|
136
149
|
recordContentMutation(result, ctx.markContentMutation);
|
|
137
|
-
return result;
|
|
138
|
-
}
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
handedOff = true;
|
|
150
|
+
return { kind: "sync" as const, value: result };
|
|
151
|
+
},
|
|
152
|
+
});
|
|
142
153
|
|
|
143
154
|
const result: SyncResultOutput = {
|
|
144
|
-
jobId,
|
|
155
|
+
jobId: accepted.jobId,
|
|
145
156
|
collections,
|
|
146
|
-
status: "started",
|
|
157
|
+
status: accepted.status === "running" ? "started" : "queued",
|
|
158
|
+
...(paths ? { paths } : {}),
|
|
147
159
|
options: {
|
|
148
160
|
gitPull: options.gitPull ?? false,
|
|
149
161
|
runUpdateCmd: options.runUpdateCmd ?? false,
|
|
@@ -156,10 +168,6 @@ export function handleSync(
|
|
|
156
168
|
throw new Error(`${error.code}: ${error.message}`);
|
|
157
169
|
}
|
|
158
170
|
throw error;
|
|
159
|
-
} finally {
|
|
160
|
-
if (lock && !handedOff) {
|
|
161
|
-
await lock.release();
|
|
162
|
-
}
|
|
163
171
|
}
|
|
164
172
|
},
|
|
165
173
|
formatSyncResult
|
package/src/mcp/tools/vsearch.ts
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
} from "../../core/typed-metadata";
|
|
29
29
|
import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
|
|
30
30
|
import { resolveDownloadPolicy } from "../../llm/policy";
|
|
31
|
-
import { resolveModelUri } from "../../llm/registry";
|
|
31
|
+
import { modelCollectionForList, resolveModelUri } from "../../llm/registry";
|
|
32
32
|
import { formatQueryForEmbedding } from "../../pipeline/contextual";
|
|
33
33
|
import {
|
|
34
34
|
searchVectorWithEmbedding,
|
|
@@ -36,12 +36,14 @@ import {
|
|
|
36
36
|
} from "../../pipeline/vsearch";
|
|
37
37
|
import { createVectorIndexPort } from "../../store/vector";
|
|
38
38
|
import { appendRetrievalWarnings } from "../retrieval-warnings";
|
|
39
|
+
import { requestCollections } from "./collection-scope";
|
|
39
40
|
import { normalizeTagFilters, runTool, type ToolResult } from "./index";
|
|
40
41
|
|
|
41
42
|
interface VsearchInput {
|
|
42
43
|
query: string;
|
|
43
44
|
projectHints?: string[];
|
|
44
45
|
collection?: string;
|
|
46
|
+
collections?: string[];
|
|
45
47
|
limit?: number;
|
|
46
48
|
minScore?: number;
|
|
47
49
|
lang?: string;
|
|
@@ -137,13 +139,15 @@ export function handleVsearch(
|
|
|
137
139
|
throw new Error(`Collection not found: ${args.collection}`);
|
|
138
140
|
}
|
|
139
141
|
}
|
|
142
|
+
const collections = requestCollections(args, ctx);
|
|
140
143
|
|
|
141
144
|
// Get model from active preset
|
|
142
145
|
const modelUri = resolveModelUri(
|
|
143
146
|
ctx.config,
|
|
144
147
|
"embed",
|
|
145
148
|
undefined,
|
|
146
|
-
args.collection
|
|
149
|
+
args.collection ??
|
|
150
|
+
modelCollectionForList(ctx.config, "embed", collections)
|
|
147
151
|
);
|
|
148
152
|
const projectAffinity = await resolveRemoteProjectAffinity(
|
|
149
153
|
ctx.config,
|
|
@@ -153,6 +157,7 @@ export function handleVsearch(
|
|
|
153
157
|
limit: args.limit ?? 5,
|
|
154
158
|
minScore: args.minScore,
|
|
155
159
|
collection: args.collection,
|
|
160
|
+
collections,
|
|
156
161
|
intent: args.intent,
|
|
157
162
|
exclude: args.exclude,
|
|
158
163
|
since: args.since,
|
|
@@ -192,7 +197,8 @@ export function handleVsearch(
|
|
|
192
197
|
// Create LLM adapter for embeddings
|
|
193
198
|
const llm = ctx.getModelAdapter?.() ?? new LlmAdapter(ctx.config);
|
|
194
199
|
const embedResult = await llm.createEmbeddingPort(modelUri, {
|
|
195
|
-
egressCollections:
|
|
200
|
+
egressCollections:
|
|
201
|
+
collections ?? (args.collection ? [args.collection] : "all"),
|
|
196
202
|
policy,
|
|
197
203
|
onProgress: (progress) => downloadProgress("embed", progress),
|
|
198
204
|
});
|
package/src/pipeline/filters.ts
CHANGED
|
@@ -25,13 +25,27 @@ export interface QueryFilterEvaluation {
|
|
|
25
25
|
* not replace complete ownership lineage or change ranking/deduplication.
|
|
26
26
|
* Managed-memory scopes/supersession must already be enforced by the store.
|
|
27
27
|
*/
|
|
28
|
+
/** True when `collection` falls outside the request's collection scope. */
|
|
29
|
+
function outsideCollectionScope(
|
|
30
|
+
scope: Pick<HybridSearchOptions, "collection" | "collections">,
|
|
31
|
+
collection: string
|
|
32
|
+
): boolean {
|
|
33
|
+
if (scope.collection && collection !== scope.collection) return true;
|
|
34
|
+
return (
|
|
35
|
+
scope.collections !== undefined && !scope.collections.includes(collection)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
28
39
|
export async function evaluateRetrievalEligibility(
|
|
29
40
|
store: StorePort,
|
|
30
41
|
query: string,
|
|
31
42
|
doc: DocumentRow | undefined,
|
|
32
43
|
chunks: ChunkRow[] | undefined,
|
|
33
44
|
options: HybridSearchOptions,
|
|
34
|
-
callerScope?: Pick<
|
|
45
|
+
callerScope?: Pick<
|
|
46
|
+
HybridSearchOptions,
|
|
47
|
+
"collection" | "collections" | "retrievalScope"
|
|
48
|
+
>
|
|
35
49
|
): Promise<QueryFilterEvaluation & { chunks: ChunkRow[] }> {
|
|
36
50
|
if (!doc || !chunks || !doc.mirrorHash) {
|
|
37
51
|
return { matches: false, reasons: ["metadata"], chunks: [] };
|
|
@@ -40,7 +54,7 @@ export async function evaluateRetrievalEligibility(
|
|
|
40
54
|
if (!doc.active) reasons.push("inactive");
|
|
41
55
|
const sourcePath = doc.recordSourcePath ?? doc.relPath;
|
|
42
56
|
for (const scope of [callerScope, options]) {
|
|
43
|
-
if (scope
|
|
57
|
+
if (scope && outsideCollectionScope(scope, doc.collection)) {
|
|
44
58
|
reasons.push("collection");
|
|
45
59
|
}
|
|
46
60
|
if (scope?.retrievalScope) {
|
|
@@ -98,7 +112,7 @@ export function evaluateDocumentChunkFilters(
|
|
|
98
112
|
options.until
|
|
99
113
|
);
|
|
100
114
|
|
|
101
|
-
if (options
|
|
115
|
+
if (outsideCollectionScope(options, doc.collection)) {
|
|
102
116
|
reasons.push("collection");
|
|
103
117
|
}
|
|
104
118
|
if (!isWithinTemporalRange(doc.sourceMtime, temporalRange)) {
|
|
@@ -219,6 +233,7 @@ export async function typedMetadataWarnings(
|
|
|
219
233
|
const range = resolveTemporalRange(query, options.since, options.until);
|
|
220
234
|
const result = await store.getTypedMetadataCoverage({
|
|
221
235
|
collection: options.collection,
|
|
236
|
+
collections: options.collections,
|
|
222
237
|
relPathPrefix: options.retrievalScope?.relPathPrefix,
|
|
223
238
|
allowedMirrorHashes: options.retrievalScope?.allowedMirrorHashes,
|
|
224
239
|
chunkLanguage: options.lang,
|
|
@@ -235,13 +250,41 @@ export async function typedMetadataWarnings(
|
|
|
235
250
|
excludeSuperseded: options.memoryFilter?.excludeSuperseded,
|
|
236
251
|
});
|
|
237
252
|
if (!result.ok) return unknown;
|
|
238
|
-
|
|
253
|
+
return metadataCoverageWarnings(result.value, "query");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** The coverage warning for documents a typed metadata filter had to skip. */
|
|
257
|
+
export function metadataCoverageWarnings(
|
|
258
|
+
coverage: { pending: number; invalid: number },
|
|
259
|
+
scope: "query" | "listing"
|
|
260
|
+
): { code: string; message: string }[] | undefined {
|
|
261
|
+
const { pending, invalid } = coverage;
|
|
239
262
|
return pending || invalid
|
|
240
263
|
? [
|
|
241
264
|
{
|
|
242
265
|
code: "METADATA_COVERAGE_INCOMPLETE",
|
|
243
|
-
message: `Typed metadata coverage is incomplete: ${pending} documents need re-ingestion and ${invalid} have invalid metadata within the
|
|
266
|
+
message: `Typed metadata coverage is incomplete: ${pending} documents need re-ingestion and ${invalid} have invalid metadata within the ${scope} scope.`,
|
|
244
267
|
},
|
|
245
268
|
]
|
|
246
269
|
: undefined;
|
|
247
270
|
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Keep the documents a typed metadata predicate matches, counting the ones it
|
|
274
|
+
* could not judge (pending re-ingestion or invalid metadata) for the warning.
|
|
275
|
+
*/
|
|
276
|
+
export function filterDocumentsByMetadata<T extends DocumentRow>(
|
|
277
|
+
docs: readonly T[],
|
|
278
|
+
filter: MetadataPredicate
|
|
279
|
+
): { matches: T[]; pending: number; invalid: number } {
|
|
280
|
+
const matches: T[] = [];
|
|
281
|
+
let pending = 0;
|
|
282
|
+
let invalid = 0;
|
|
283
|
+
for (const doc of docs) {
|
|
284
|
+
const reason = typedMetadataFilterReason(doc, filter);
|
|
285
|
+
if (reason === undefined) matches.push(doc);
|
|
286
|
+
else if (reason === "metadata_backfill") pending += 1;
|
|
287
|
+
else if (reason === "metadata_invalid") invalid += 1;
|
|
288
|
+
}
|
|
289
|
+
return { matches, pending, invalid };
|
|
290
|
+
}
|