@gmickel/gno 2.12.2 → 2.13.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 +1 -1
- package/assets/skill/cli-reference.md +23 -10
- package/assets/skill/mcp-reference.md +2 -0
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.12.2.zip → gno-browser-clipper-v2.13.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.13.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +66 -5
- package/spec/mcp.md +96 -1
- package/spec/output-schemas/convert.schema.json +97 -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/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 +79 -10
- package/src/core/collection-scope.ts +105 -0
- package/src/core/errors.ts +5 -0
- package/src/core/job-manager.ts +238 -19
- 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 +18 -3
- 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 +39 -11
- package/src/sdk/convert.ts +27 -0
- package/src/sdk/index.ts +2 -0
- package/src/sdk/types.ts +1 -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
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* @module src/cli/commands/indexCmd
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { resolveReindexScope } from "../../core/reindex-paths";
|
|
10
11
|
import {
|
|
11
12
|
type CliWriteLeaseOptions,
|
|
12
13
|
type WriteLeaseContention,
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
type SyncResult,
|
|
28
29
|
withContentTypeRules,
|
|
29
30
|
} from "../../ingestion";
|
|
31
|
+
import { CliError } from "../errors";
|
|
30
32
|
import { type EmbedResult, embedStageOutcome } from "./embed";
|
|
31
33
|
import {
|
|
32
34
|
formatSyncResultLines,
|
|
@@ -44,6 +46,8 @@ export interface IndexOptions extends CliWriteLeaseOptions {
|
|
|
44
46
|
indexName?: string;
|
|
45
47
|
/** Scope to single collection */
|
|
46
48
|
collection?: string;
|
|
49
|
+
/** Reindex only these files of `collection` */
|
|
50
|
+
paths?: string[];
|
|
47
51
|
/** Run ingestion only, skip embedding */
|
|
48
52
|
noEmbed?: boolean;
|
|
49
53
|
/** Download models if missing */
|
|
@@ -186,6 +190,18 @@ export async function index(options: IndexOptions = {}): Promise<IndexResult> {
|
|
|
186
190
|
}
|
|
187
191
|
|
|
188
192
|
const { store, collections, config } = initResult;
|
|
193
|
+
let paths: string[] | null;
|
|
194
|
+
try {
|
|
195
|
+
paths = await resolveReindexScope(
|
|
196
|
+
options.collection ? (collections[0] ?? null) : null,
|
|
197
|
+
options.paths,
|
|
198
|
+
{ gitPull: options.gitPull }
|
|
199
|
+
);
|
|
200
|
+
} catch (e) {
|
|
201
|
+
await store.close();
|
|
202
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
203
|
+
throw new CliError("VALIDATION", message.replace(/^[A-Z_]+: /, ""));
|
|
204
|
+
}
|
|
189
205
|
const db = store.getRawDb();
|
|
190
206
|
const embedSkipped = options.noEmbed ?? false;
|
|
191
207
|
|
|
@@ -202,18 +218,40 @@ export async function index(options: IndexOptions = {}): Promise<IndexResult> {
|
|
|
202
218
|
markIndexStageRunning(db, "lexical", { collection: options.collection });
|
|
203
219
|
let syncResult: SyncResult;
|
|
204
220
|
try {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
onSlowConversion: reportSlowConversion,
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
)
|
|
216
|
-
|
|
221
|
+
const collection = collections[0];
|
|
222
|
+
if (paths && collection) {
|
|
223
|
+
const pathResult = await defaultSyncService.syncPaths(
|
|
224
|
+
collection,
|
|
225
|
+
store,
|
|
226
|
+
paths,
|
|
227
|
+
withContentTypeRules(
|
|
228
|
+
{ onSlowConversion: reportSlowConversion },
|
|
229
|
+
config
|
|
230
|
+
)
|
|
231
|
+
);
|
|
232
|
+
syncResult = {
|
|
233
|
+
collections: [pathResult],
|
|
234
|
+
totalDurationMs: pathResult.durationMs,
|
|
235
|
+
totalFilesProcessed: pathResult.filesProcessed,
|
|
236
|
+
totalFilesAdded: pathResult.filesAdded,
|
|
237
|
+
totalFilesUpdated: pathResult.filesUpdated,
|
|
238
|
+
totalFilesErrored: pathResult.filesErrored,
|
|
239
|
+
totalFilesSkipped: pathResult.filesSkipped,
|
|
240
|
+
};
|
|
241
|
+
} else {
|
|
242
|
+
syncResult = await defaultSyncService.syncAll(
|
|
243
|
+
collections,
|
|
244
|
+
store,
|
|
245
|
+
withContentTypeRules(
|
|
246
|
+
{
|
|
247
|
+
gitPull: options.gitPull,
|
|
248
|
+
runUpdateCmd: true,
|
|
249
|
+
onSlowConversion: reportSlowConversion,
|
|
250
|
+
},
|
|
251
|
+
config
|
|
252
|
+
)
|
|
253
|
+
);
|
|
254
|
+
}
|
|
217
255
|
} catch (e) {
|
|
218
256
|
const message = e instanceof Error ? e.message : String(e);
|
|
219
257
|
markIndexStageFinished(db, "lexical", "failed");
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
fingerprintContentTypeMetadataRules,
|
|
20
20
|
normalizeContentTypes,
|
|
21
21
|
} from "../../config";
|
|
22
|
+
import { resolveCollectionScope } from "../../core/collection-scope";
|
|
22
23
|
import { resolveCliProjectAffinity } from "../../core/project-affinity-surface";
|
|
23
24
|
import {
|
|
24
25
|
finishRetrievalTraceAfterError,
|
|
@@ -27,7 +28,7 @@ import {
|
|
|
27
28
|
} from "../../core/retrieval-trace-request";
|
|
28
29
|
import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
|
|
29
30
|
import { resolveDownloadPolicy } from "../../llm/policy";
|
|
30
|
-
import { resolveModelUri } from "../../llm/registry";
|
|
31
|
+
import { modelCollectionForList, resolveModelUri } from "../../llm/registry";
|
|
31
32
|
import {
|
|
32
33
|
diagnoseQueryTarget,
|
|
33
34
|
type QueryDiagnoseResult as PipelineQueryDiagnoseResult,
|
|
@@ -91,7 +92,7 @@ export type QueryResult =
|
|
|
91
92
|
data: SearchResults;
|
|
92
93
|
metadata?: RetrievalTraceSurfaceMetadata;
|
|
93
94
|
}
|
|
94
|
-
| { success: false; error: string };
|
|
95
|
+
| { success: false; error: string; isValidation?: boolean };
|
|
95
96
|
|
|
96
97
|
export type QueryDiagnoseCommandOptions = Omit<
|
|
97
98
|
HybridSearchOptions,
|
|
@@ -151,17 +152,29 @@ export async function query(
|
|
|
151
152
|
let traceSession: RetrievalTraceSession | undefined;
|
|
152
153
|
|
|
153
154
|
try {
|
|
155
|
+
const scope = resolveCollectionScope(options, config.collections);
|
|
156
|
+
if (!scope.ok) {
|
|
157
|
+
return {
|
|
158
|
+
success: false,
|
|
159
|
+
error: scope.message,
|
|
160
|
+
isValidation: scope.kind === "validation",
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
options = { ...options, collections: scope.collections };
|
|
154
164
|
const { projectAffinityDisabled, projectRoots, ...queryOptions } = options;
|
|
155
165
|
const projectAffinity = await resolveCliProjectAffinity(config, {
|
|
156
166
|
cwd: process.cwd(),
|
|
157
167
|
disabled: projectAffinityDisabled,
|
|
158
168
|
projectRoots,
|
|
159
169
|
});
|
|
170
|
+
const modelCollection = (type: "embed" | "expand" | "rerank") =>
|
|
171
|
+
options.collection ??
|
|
172
|
+
modelCollectionForList(config, type, options.collections);
|
|
160
173
|
const embedUri = resolveModelUri(
|
|
161
174
|
config,
|
|
162
175
|
"embed",
|
|
163
176
|
options.embedModel,
|
|
164
|
-
|
|
177
|
+
modelCollection("embed")
|
|
165
178
|
);
|
|
166
179
|
const expandUri =
|
|
167
180
|
!options.noExpand && !options.queryModes?.length
|
|
@@ -169,7 +182,7 @@ export async function query(
|
|
|
169
182
|
config,
|
|
170
183
|
"expand",
|
|
171
184
|
options.expandModel ?? options.genModel,
|
|
172
|
-
|
|
185
|
+
modelCollection("expand")
|
|
173
186
|
)
|
|
174
187
|
: undefined;
|
|
175
188
|
const rerankUri = !options.noRerank
|
|
@@ -177,7 +190,7 @@ export async function query(
|
|
|
177
190
|
config,
|
|
178
191
|
"rerank",
|
|
179
192
|
options.rerankModel,
|
|
180
|
-
|
|
193
|
+
modelCollection("rerank")
|
|
181
194
|
)
|
|
182
195
|
: undefined;
|
|
183
196
|
const traceStart = await startRetrievalTraceRequest({
|
|
@@ -196,9 +209,9 @@ export async function query(
|
|
|
196
209
|
}
|
|
197
210
|
traceSession = traceStart.value ?? undefined;
|
|
198
211
|
const llm = new LlmAdapter(config);
|
|
199
|
-
const egressCollections =
|
|
200
|
-
|
|
201
|
-
: ("all" as const);
|
|
212
|
+
const egressCollections =
|
|
213
|
+
options.collections ??
|
|
214
|
+
(options.collection ? [options.collection] : ("all" as const));
|
|
202
215
|
|
|
203
216
|
// Resolve download policy from env/flags
|
|
204
217
|
const globals = getGlobals();
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
import type { SearchOptions, SearchResults } from "../../pipeline/types";
|
|
14
14
|
|
|
15
15
|
import { normalizeContentTypes } from "../../config";
|
|
16
|
+
import { resolveCollectionScope } from "../../core/collection-scope";
|
|
16
17
|
import { resolveCliProjectAffinity } from "../../core/project-affinity-surface";
|
|
17
18
|
import {
|
|
18
19
|
finishRetrievalTraceAfterError,
|
|
@@ -91,6 +92,15 @@ export async function search(
|
|
|
91
92
|
let traceSession: RetrievalTraceSession | undefined;
|
|
92
93
|
|
|
93
94
|
try {
|
|
95
|
+
const scope = resolveCollectionScope(options, config.collections);
|
|
96
|
+
if (!scope.ok) {
|
|
97
|
+
return {
|
|
98
|
+
success: false,
|
|
99
|
+
error: scope.message,
|
|
100
|
+
isValidation: scope.kind === "validation",
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
options = { ...options, collections: scope.collections };
|
|
94
104
|
const { projectAffinityDisabled, projectRoots, ...searchOptions } = options;
|
|
95
105
|
const projectAffinity = await resolveCliProjectAffinity(config, {
|
|
96
106
|
cwd: process.cwd(),
|
|
@@ -104,6 +114,7 @@ export async function search(
|
|
|
104
114
|
filters: {
|
|
105
115
|
limit,
|
|
106
116
|
collection: options.collection,
|
|
117
|
+
collections: options.collections,
|
|
107
118
|
lang: options.lang,
|
|
108
119
|
full: options.full,
|
|
109
120
|
lineNumbers: options.lineNumbers,
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
import type { EmbeddingPort } from "../../llm/types";
|
|
14
14
|
import type { SearchOptions, SearchResults } from "../../pipeline/types";
|
|
15
15
|
|
|
16
|
+
import { resolveCollectionScope } from "../../core/collection-scope";
|
|
16
17
|
import { resolveCliProjectAffinity } from "../../core/project-affinity-surface";
|
|
17
18
|
import {
|
|
18
19
|
finishRetrievalTraceAfterError,
|
|
@@ -20,7 +21,7 @@ import {
|
|
|
20
21
|
startRetrievalTraceRequest,
|
|
21
22
|
} from "../../core/retrieval-trace-request";
|
|
22
23
|
import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
|
|
23
|
-
import { resolveModelUri } from "../../llm/registry";
|
|
24
|
+
import { modelCollectionForList, resolveModelUri } from "../../llm/registry";
|
|
24
25
|
import { formatQueryForEmbedding } from "../../pipeline/contextual";
|
|
25
26
|
import {
|
|
26
27
|
searchVectorWithEmbedding,
|
|
@@ -65,7 +66,7 @@ export type VsearchResult =
|
|
|
65
66
|
data: SearchResults;
|
|
66
67
|
metadata?: RetrievalTraceSurfaceMetadata;
|
|
67
68
|
}
|
|
68
|
-
| { success: false; error: string };
|
|
69
|
+
| { success: false; error: string; isValidation?: boolean };
|
|
69
70
|
|
|
70
71
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
71
72
|
// Command Implementation
|
|
@@ -99,6 +100,15 @@ export async function vsearch(
|
|
|
99
100
|
let traceSession: RetrievalTraceSession | undefined;
|
|
100
101
|
|
|
101
102
|
try {
|
|
103
|
+
const scope = resolveCollectionScope(options, config.collections);
|
|
104
|
+
if (!scope.ok) {
|
|
105
|
+
return {
|
|
106
|
+
success: false,
|
|
107
|
+
error: scope.message,
|
|
108
|
+
isValidation: scope.kind === "validation",
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
options = { ...options, collections: scope.collections };
|
|
102
112
|
const { projectAffinityDisabled, projectRoots, ...searchOptions } = options;
|
|
103
113
|
const projectAffinity = await resolveCliProjectAffinity(config, {
|
|
104
114
|
cwd: process.cwd(),
|
|
@@ -110,7 +120,8 @@ export async function vsearch(
|
|
|
110
120
|
config,
|
|
111
121
|
"embed",
|
|
112
122
|
options.model,
|
|
113
|
-
options.collection
|
|
123
|
+
options.collection ??
|
|
124
|
+
modelCollectionForList(config, "embed", options.collections)
|
|
114
125
|
);
|
|
115
126
|
const traceStart = await startRetrievalTraceRequest({
|
|
116
127
|
store,
|
|
@@ -129,7 +140,9 @@ export async function vsearch(
|
|
|
129
140
|
// Create LLM adapter for embeddings
|
|
130
141
|
const llm = new LlmAdapter(config);
|
|
131
142
|
const embedResult = await llm.createEmbeddingPort(modelUri, {
|
|
132
|
-
egressCollections:
|
|
143
|
+
egressCollections:
|
|
144
|
+
options.collections ??
|
|
145
|
+
(options.collection ? [options.collection] : "all"),
|
|
133
146
|
});
|
|
134
147
|
if (!embedResult.ok) {
|
|
135
148
|
await traceSession?.finish("failed");
|
package/src/cli/options.ts
CHANGED
|
@@ -26,6 +26,23 @@ export interface CliProjectAffinityOptions {
|
|
|
26
26
|
projectRoots: string[];
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Map repeatable `-c` values for search/vsearch/query: one value keeps the
|
|
31
|
+
* single-collection `collection` path; two or more form the allowlist.
|
|
32
|
+
*/
|
|
33
|
+
export const splitCollectionFlags = (
|
|
34
|
+
value: unknown
|
|
35
|
+
): { collection?: string; collections?: string[] } => {
|
|
36
|
+
const values = Array.isArray(value)
|
|
37
|
+
? (value as string[])
|
|
38
|
+
: typeof value === "string"
|
|
39
|
+
? [value]
|
|
40
|
+
: [];
|
|
41
|
+
if (values.length === 0) return {};
|
|
42
|
+
if (values.length === 1) return { collection: values[0] };
|
|
43
|
+
return { collections: values };
|
|
44
|
+
};
|
|
45
|
+
|
|
29
46
|
export const collectRepeatableValue = (
|
|
30
47
|
value: string,
|
|
31
48
|
previous: string[] = []
|
|
@@ -70,6 +87,7 @@ export const CMD = {
|
|
|
70
87
|
ask: "ask",
|
|
71
88
|
get: "get",
|
|
72
89
|
multiGet: "multi-get",
|
|
90
|
+
convert: "convert",
|
|
73
91
|
ls: "ls",
|
|
74
92
|
status: "status",
|
|
75
93
|
peek: "peek",
|
|
@@ -107,6 +125,7 @@ const FORMAT_SUPPORT: Record<CommandId, OutputFormat[]> = {
|
|
|
107
125
|
[CMD.ask]: ["terminal", "json", "md"],
|
|
108
126
|
[CMD.get]: ["terminal", "json", "md"],
|
|
109
127
|
[CMD.multiGet]: ["terminal", "json", "files", "md"],
|
|
128
|
+
[CMD.convert]: ["terminal", "json"],
|
|
110
129
|
[CMD.ls]: ["terminal", "json", "files", "md"],
|
|
111
130
|
[CMD.status]: ["terminal", "json"],
|
|
112
131
|
[CMD.peek]: ["terminal", "json"],
|
package/src/cli/program.ts
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
assertFormatSupported,
|
|
53
53
|
CMD,
|
|
54
54
|
collectRepeatableValue,
|
|
55
|
+
splitCollectionFlags,
|
|
55
56
|
getDefaultLimit,
|
|
56
57
|
parseCliProjectAffinityOptions,
|
|
57
58
|
parseCliMetadataFilter,
|
|
@@ -325,6 +326,18 @@ function validateContextOutputPath(value: unknown): string | undefined {
|
|
|
325
326
|
// Program Factory
|
|
326
327
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
327
328
|
|
|
329
|
+
/** `query --diagnose` explains one collection scope; several `-c` are refused. */
|
|
330
|
+
function singleDiagnoseCollection(value: unknown): string | undefined {
|
|
331
|
+
const { collection, collections } = splitCollectionFlags(value);
|
|
332
|
+
if (collections) {
|
|
333
|
+
throw new CliError(
|
|
334
|
+
"VALIDATION",
|
|
335
|
+
"query --diagnose accepts a single -c collection"
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
return collection;
|
|
339
|
+
}
|
|
340
|
+
|
|
328
341
|
export function createProgram(): Command {
|
|
329
342
|
const program = new Command();
|
|
330
343
|
|
|
@@ -349,7 +362,7 @@ export function createProgram(): Command {
|
|
|
349
362
|
.option("--no-pager", "disable automatic paging of long output");
|
|
350
363
|
|
|
351
364
|
// Resolve globals ONCE before any command runs (ensures consistency)
|
|
352
|
-
program.hook("preAction", async (thisCommand) => {
|
|
365
|
+
program.hook("preAction", async (thisCommand, actionCommand) => {
|
|
353
366
|
const rootOpts = thisCommand.optsWithGlobals();
|
|
354
367
|
const globals = parseGlobalOptions(rootOpts);
|
|
355
368
|
if (!isValidIndexName(globals.index)) {
|
|
@@ -360,6 +373,8 @@ export function createProgram(): Command {
|
|
|
360
373
|
}
|
|
361
374
|
applyGlobalOptions(globals);
|
|
362
375
|
globalState.current = globals;
|
|
376
|
+
// convert reads no config or index, so there is no binding to check.
|
|
377
|
+
if (actionCommand === convertCommand) return;
|
|
363
378
|
const { assertCliSessionBinding } = await import("./session-binding");
|
|
364
379
|
await assertCliSessionBinding(globals.config, globals.index);
|
|
365
380
|
});
|
|
@@ -375,6 +390,7 @@ export function createProgram(): Command {
|
|
|
375
390
|
wirePublishCommand(program);
|
|
376
391
|
wireVecCommands(program);
|
|
377
392
|
wireRetrievalCommands(program);
|
|
393
|
+
const convertCommand = wireConvertCommand(program);
|
|
378
394
|
wireTagsCommands(program);
|
|
379
395
|
wireLinksCommands(program);
|
|
380
396
|
wireGraphCommand(program);
|
|
@@ -650,7 +666,11 @@ function wireSearchCommands(program: Command): void {
|
|
|
650
666
|
)
|
|
651
667
|
.option("-n, --limit <num>", "max results")
|
|
652
668
|
.option("--min-score <num>", "minimum score threshold")
|
|
653
|
-
.option(
|
|
669
|
+
.option(
|
|
670
|
+
"-c, --collection <name>",
|
|
671
|
+
"filter by collection (repeat for several)",
|
|
672
|
+
collectRepeatableValue
|
|
673
|
+
)
|
|
654
674
|
.option("--lang <code>", "language filter/hint (BCP-47)")
|
|
655
675
|
.option(
|
|
656
676
|
"--since <date>",
|
|
@@ -740,7 +760,7 @@ function wireSearchCommands(program: Command): void {
|
|
|
740
760
|
indexName: globals.index,
|
|
741
761
|
limit,
|
|
742
762
|
minScore,
|
|
743
|
-
|
|
763
|
+
...splitCollectionFlags(cmdOpts.collection),
|
|
744
764
|
lang: cmdOpts.lang as string | undefined,
|
|
745
765
|
since: cmdOpts.since as string | undefined,
|
|
746
766
|
until: cmdOpts.until as string | undefined,
|
|
@@ -790,7 +810,11 @@ function wireSearchCommands(program: Command): void {
|
|
|
790
810
|
.description("Vector similarity search")
|
|
791
811
|
.option("-n, --limit <num>", "max results")
|
|
792
812
|
.option("--min-score <num>", "minimum score threshold")
|
|
793
|
-
.option(
|
|
813
|
+
.option(
|
|
814
|
+
"-c, --collection <name>",
|
|
815
|
+
"filter by collection (repeat for several)",
|
|
816
|
+
collectRepeatableValue
|
|
817
|
+
)
|
|
794
818
|
.option("--lang <code>", "language filter/hint (BCP-47)")
|
|
795
819
|
.option(
|
|
796
820
|
"--since <date>",
|
|
@@ -870,7 +894,7 @@ function wireSearchCommands(program: Command): void {
|
|
|
870
894
|
indexName: globals.index,
|
|
871
895
|
limit,
|
|
872
896
|
minScore,
|
|
873
|
-
|
|
897
|
+
...splitCollectionFlags(cmdOpts.collection),
|
|
874
898
|
lang: cmdOpts.lang as string | undefined,
|
|
875
899
|
since: cmdOpts.since as string | undefined,
|
|
876
900
|
until: cmdOpts.until as string | undefined,
|
|
@@ -892,7 +916,10 @@ function wireSearchCommands(program: Command): void {
|
|
|
892
916
|
});
|
|
893
917
|
|
|
894
918
|
if (!result.success) {
|
|
895
|
-
throw new CliError(
|
|
919
|
+
throw new CliError(
|
|
920
|
+
result.isValidation ? "VALIDATION" : "RUNTIME",
|
|
921
|
+
result.error
|
|
922
|
+
);
|
|
896
923
|
}
|
|
897
924
|
const output = formatVsearch(result, {
|
|
898
925
|
json: format === "json",
|
|
@@ -918,7 +945,11 @@ function wireSearchCommands(program: Command): void {
|
|
|
918
945
|
)
|
|
919
946
|
.option("-n, --limit <num>", "max results")
|
|
920
947
|
.option("--min-score <num>", "minimum score threshold")
|
|
921
|
-
.option(
|
|
948
|
+
.option(
|
|
949
|
+
"-c, --collection <name>",
|
|
950
|
+
"filter by collection (repeat for several)",
|
|
951
|
+
collectRepeatableValue
|
|
952
|
+
)
|
|
922
953
|
.option("--lang <code>", "language hint (BCP-47)")
|
|
923
954
|
.option(
|
|
924
955
|
"--since <date>",
|
|
@@ -1088,7 +1119,7 @@ function wireSearchCommands(program: Command): void {
|
|
|
1088
1119
|
target: cmdOpts.target as string,
|
|
1089
1120
|
limit,
|
|
1090
1121
|
minScore,
|
|
1091
|
-
collection: cmdOpts.collection
|
|
1122
|
+
collection: singleDiagnoseCollection(cmdOpts.collection),
|
|
1092
1123
|
lang: cmdOpts.lang as string | undefined,
|
|
1093
1124
|
since: cmdOpts.since as string | undefined,
|
|
1094
1125
|
until: cmdOpts.until as string | undefined,
|
|
@@ -1125,7 +1156,7 @@ function wireSearchCommands(program: Command): void {
|
|
|
1125
1156
|
indexName: globals.index,
|
|
1126
1157
|
limit,
|
|
1127
1158
|
minScore,
|
|
1128
|
-
|
|
1159
|
+
...splitCollectionFlags(cmdOpts.collection),
|
|
1129
1160
|
lang: cmdOpts.lang as string | undefined,
|
|
1130
1161
|
since: cmdOpts.since as string | undefined,
|
|
1131
1162
|
until: cmdOpts.until as string | undefined,
|
|
@@ -1154,7 +1185,10 @@ function wireSearchCommands(program: Command): void {
|
|
|
1154
1185
|
});
|
|
1155
1186
|
|
|
1156
1187
|
if (!result.success) {
|
|
1157
|
-
throw new CliError(
|
|
1188
|
+
throw new CliError(
|
|
1189
|
+
result.isValidation ? "VALIDATION" : "RUNTIME",
|
|
1190
|
+
result.error
|
|
1191
|
+
);
|
|
1158
1192
|
}
|
|
1159
1193
|
const output = formatQuery(result, {
|
|
1160
1194
|
format,
|
|
@@ -1624,6 +1658,10 @@ function wireOnboardingCommands(program: Command): void {
|
|
|
1624
1658
|
program
|
|
1625
1659
|
.command("index [collection]")
|
|
1626
1660
|
.description("Index files from collections")
|
|
1661
|
+
.option(
|
|
1662
|
+
"--path <path...>",
|
|
1663
|
+
"reindex only these files of the collection (relative to its root)"
|
|
1664
|
+
)
|
|
1627
1665
|
.option("--no-embed", "skip embedding after sync")
|
|
1628
1666
|
.option("--git-pull", "run git pull in git repositories")
|
|
1629
1667
|
.option("--models-pull", "download models if missing")
|
|
@@ -1640,6 +1678,7 @@ function wireOnboardingCommands(program: Command): void {
|
|
|
1640
1678
|
configPath: globals.config,
|
|
1641
1679
|
indexName: globals.index,
|
|
1642
1680
|
collection,
|
|
1681
|
+
paths: cmdOpts.path as string[] | undefined,
|
|
1643
1682
|
noEmbed: cmdOpts.embed === false,
|
|
1644
1683
|
gitPull: Boolean(cmdOpts.gitPull),
|
|
1645
1684
|
modelsPull: Boolean(cmdOpts.modelsPull),
|
|
@@ -2436,6 +2475,36 @@ function wireSessionsAutomationCommands(
|
|
|
2436
2475
|
// Retrieval Commands (get, multi-get, ls)
|
|
2437
2476
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2438
2477
|
|
|
2478
|
+
function wireConvertCommand(program: Command): Command {
|
|
2479
|
+
return program
|
|
2480
|
+
.command("convert <file>")
|
|
2481
|
+
.description("Print the Markdown GNO would index for one file")
|
|
2482
|
+
.option("--json", "JSON output")
|
|
2483
|
+
.action(async (file: string, cmdOpts: Record<string, unknown>) => {
|
|
2484
|
+
const format = getFormat(cmdOpts);
|
|
2485
|
+
assertFormatSupported(CMD.convert, format);
|
|
2486
|
+
|
|
2487
|
+
const { convertFile } = await import("../ingestion/convert-file");
|
|
2488
|
+
const result = await convertFile(file);
|
|
2489
|
+
if (!result.ok) {
|
|
2490
|
+
const { reason, message, validation, details } = result.error;
|
|
2491
|
+
throw new CliError(validation ? "VALIDATION" : "RUNTIME", message, {
|
|
2492
|
+
details: { reason, ...details },
|
|
2493
|
+
});
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
if (format === "json") {
|
|
2497
|
+
process.stdout.write(`${JSON.stringify(result.value, null, 2)}\n`);
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2500
|
+
// Warnings go to stderr so stdout stays exactly the Markdown.
|
|
2501
|
+
for (const warning of result.value.warnings) {
|
|
2502
|
+
process.stderr.write(`Warning: ${warning.code}: ${warning.message}\n`);
|
|
2503
|
+
}
|
|
2504
|
+
process.stdout.write(result.value.markdown);
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2439
2508
|
function wireRetrievalCommands(program: Command): void {
|
|
2440
2509
|
// get - Retrieve document by URI or docid
|
|
2441
2510
|
program
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request-level collection allowlist shared by search, vsearch and query on
|
|
3
|
+
* every surface (CLI, MCP, REST, SDK).
|
|
4
|
+
*
|
|
5
|
+
* @module src/core/collection-scope
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Most collections one request may list; matches the audit scope bound. */
|
|
9
|
+
export const MAX_REQUEST_COLLECTIONS = 256;
|
|
10
|
+
|
|
11
|
+
export type CollectionListResult =
|
|
12
|
+
| { ok: true; value: string[] }
|
|
13
|
+
| { ok: false; message: string };
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Normalize a `collections` allowlist: trim, lowercase, de-duplicate, sort.
|
|
17
|
+
* Rejects non-arrays, non-string or blank members, an empty list, and lists
|
|
18
|
+
* longer than {@link MAX_REQUEST_COLLECTIONS} after normalization.
|
|
19
|
+
*/
|
|
20
|
+
export function normalizeCollectionList(input: unknown): CollectionListResult {
|
|
21
|
+
if (!Array.isArray(input)) {
|
|
22
|
+
return { ok: false, message: "collections must be an array of names" };
|
|
23
|
+
}
|
|
24
|
+
const names = new Set<string>();
|
|
25
|
+
for (const value of input) {
|
|
26
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
27
|
+
return {
|
|
28
|
+
ok: false,
|
|
29
|
+
message: "collections must contain only non-empty collection names",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
names.add(value.trim().toLowerCase());
|
|
33
|
+
}
|
|
34
|
+
if (names.size === 0) {
|
|
35
|
+
return { ok: false, message: "collections must list at least one name" };
|
|
36
|
+
}
|
|
37
|
+
if (names.size > MAX_REQUEST_COLLECTIONS) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
message: `collections must list at most ${MAX_REQUEST_COLLECTIONS} names`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
ok: true,
|
|
45
|
+
value: [...names].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Error text when a request names both `collection` and `collections`. */
|
|
50
|
+
export const COLLECTION_AND_COLLECTIONS_MESSAGE =
|
|
51
|
+
"Pass either collection or collections, not both";
|
|
52
|
+
|
|
53
|
+
/** Names in `requested` that are not configured, in request order. */
|
|
54
|
+
export function unknownCollections(
|
|
55
|
+
requested: readonly string[],
|
|
56
|
+
configured: readonly { name: string }[]
|
|
57
|
+
): string[] {
|
|
58
|
+
const known = new Set(configured.map(({ name }) => name.toLowerCase()));
|
|
59
|
+
return requested.filter((name) => !known.has(name));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The existing unknown-collection message, naming every unknown name. */
|
|
63
|
+
export function unknownCollectionsMessage(names: readonly string[]): string {
|
|
64
|
+
return names.length === 1
|
|
65
|
+
? `Collection not found: ${names[0]}`
|
|
66
|
+
: `Collections not found: ${names.join(", ")}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type CollectionScopeResult =
|
|
70
|
+
| { ok: true; collections: string[] | undefined }
|
|
71
|
+
| { ok: false; kind: "validation" | "not_found"; message: string };
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolve a request's `collections` allowlist against the configured
|
|
75
|
+
* collections. Absent `collections` resolves to undefined (unchanged
|
|
76
|
+
* single-collection behaviour). Both inputs together, a malformed list, or
|
|
77
|
+
* any unknown name is refused.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveCollectionScope(
|
|
80
|
+
input: { collection?: unknown; collections?: unknown },
|
|
81
|
+
configured: readonly { name: string }[]
|
|
82
|
+
): CollectionScopeResult {
|
|
83
|
+
if (input.collections === undefined)
|
|
84
|
+
return { ok: true, collections: undefined };
|
|
85
|
+
if (input.collection !== undefined) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
kind: "validation",
|
|
89
|
+
message: COLLECTION_AND_COLLECTIONS_MESSAGE,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const normalized = normalizeCollectionList(input.collections);
|
|
93
|
+
if (!normalized.ok) {
|
|
94
|
+
return { ok: false, kind: "validation", message: normalized.message };
|
|
95
|
+
}
|
|
96
|
+
const unknown = unknownCollections(normalized.value, configured);
|
|
97
|
+
if (unknown.length > 0) {
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
kind: "not_found",
|
|
101
|
+
message: unknownCollectionsMessage(unknown),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return { ok: true, collections: normalized.value };
|
|
105
|
+
}
|
package/src/core/errors.ts
CHANGED
|
@@ -13,6 +13,11 @@ export const MCP_ERRORS = {
|
|
|
13
13
|
code: "JOB_CONFLICT",
|
|
14
14
|
message: "Another job is already running.",
|
|
15
15
|
},
|
|
16
|
+
QUEUE_FULL: {
|
|
17
|
+
code: "QUEUE_FULL",
|
|
18
|
+
message:
|
|
19
|
+
"Too many reindex jobs are waiting. Retry after the running job finishes.",
|
|
20
|
+
},
|
|
16
21
|
INVALID_PATH: {
|
|
17
22
|
code: "INVALID_PATH",
|
|
18
23
|
message: "Path violates safety rules.",
|