@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
|
@@ -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
|
|
@@ -2556,6 +2625,7 @@ function wireRetrievalCommands(program: Command): void {
|
|
|
2556
2625
|
"Skip first N results",
|
|
2557
2626
|
parsePositiveInt.bind(null, "offset")
|
|
2558
2627
|
)
|
|
2628
|
+
.option("--filter <json>", "typed custom metadata predicate (JSON)")
|
|
2559
2629
|
.option("--json", "JSON output")
|
|
2560
2630
|
.option("--files", "File protocol output")
|
|
2561
2631
|
.option("--md", "Markdown output")
|
|
@@ -2564,6 +2634,7 @@ function wireRetrievalCommands(program: Command): void {
|
|
|
2564
2634
|
const format = getFormat(cmdOpts);
|
|
2565
2635
|
assertFormatSupported(CMD.ls, format);
|
|
2566
2636
|
const globals = getGlobals();
|
|
2637
|
+
const filter = parseCliMetadataFilter(cmdOpts.filter);
|
|
2567
2638
|
|
|
2568
2639
|
const { ls, formatLs } = await import("./commands/ls");
|
|
2569
2640
|
const result = await ls(scope, {
|
|
@@ -2571,6 +2642,7 @@ function wireRetrievalCommands(program: Command): void {
|
|
|
2571
2642
|
indexName: globals.index,
|
|
2572
2643
|
limit: cmdOpts.limit as number | undefined,
|
|
2573
2644
|
offset: cmdOpts.offset as number | undefined,
|
|
2645
|
+
filter,
|
|
2574
2646
|
json: format === "json",
|
|
2575
2647
|
files: format === "files",
|
|
2576
2648
|
md: format === "md",
|
|
@@ -2583,6 +2655,14 @@ function wireRetrievalCommands(program: Command): void {
|
|
|
2583
2655
|
);
|
|
2584
2656
|
}
|
|
2585
2657
|
|
|
2658
|
+
// JSON carries warnings in meta; other formats keep stdout clean.
|
|
2659
|
+
if (format !== "json") {
|
|
2660
|
+
for (const warning of result.data.meta.warnings ?? []) {
|
|
2661
|
+
process.stderr.write(
|
|
2662
|
+
`Warning: ${warning.code}: ${warning.message}\n`
|
|
2663
|
+
);
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2586
2666
|
const output = formatLs(result, {
|
|
2587
2667
|
json: format === "json",
|
|
2588
2668
|
files: format === "files",
|
package/src/config/loader.ts
CHANGED
|
@@ -126,11 +126,21 @@ export async function loadConfigFromPath(
|
|
|
126
126
|
const result = ConfigSchema.safeParse(parsed);
|
|
127
127
|
|
|
128
128
|
if (!result.success) {
|
|
129
|
+
// Collection `audit` mistakes name the setting; other issues keep the
|
|
130
|
+
// generic message.
|
|
131
|
+
const auditIssues = result.error.issues.filter((issue) =>
|
|
132
|
+
issue.path.includes("audit")
|
|
133
|
+
);
|
|
129
134
|
return {
|
|
130
135
|
ok: false,
|
|
131
136
|
error: {
|
|
132
137
|
code: "VALIDATION_ERROR",
|
|
133
|
-
message:
|
|
138
|
+
message:
|
|
139
|
+
auditIssues.length > 0
|
|
140
|
+
? `Config validation failed: ${auditIssues
|
|
141
|
+
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
142
|
+
.join("; ")}`
|
|
143
|
+
: "Config validation failed",
|
|
134
144
|
issues: result.error.issues,
|
|
135
145
|
},
|
|
136
146
|
};
|
package/src/config/types.ts
CHANGED
|
@@ -114,6 +114,74 @@ const COLLECTION_NAME_REGEX = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
|
114
114
|
/** Collection scope key pattern: name (1-64 chars) followed by colon */
|
|
115
115
|
const COLLECTION_SCOPE_REGEX = /^[a-z0-9][a-z0-9_-]{0,63}:$/;
|
|
116
116
|
|
|
117
|
+
/** Most globs one collection's `audit.paths` may list. */
|
|
118
|
+
export const COLLECTION_AUDIT_MAX_PATHS = 64;
|
|
119
|
+
const COLLECTION_AUDIT_MAX_GLOB_CHARS = 1024;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A path glob in `pattern` syntax: relative, no `..` segment, balanced
|
|
123
|
+
* `{}` / `[]`. Bun.Glob accepts nearly any string, so the shape is checked.
|
|
124
|
+
*/
|
|
125
|
+
export function isValidAuditPathGlob(glob: string): boolean {
|
|
126
|
+
if (glob.trim() !== glob || glob.length === 0) return false;
|
|
127
|
+
if (glob.includes("\0") || glob.startsWith("/")) return false;
|
|
128
|
+
if (glob.split("/").includes("..")) return false;
|
|
129
|
+
let braces = 0;
|
|
130
|
+
let inClass = false;
|
|
131
|
+
for (const char of glob) {
|
|
132
|
+
if (inClass) {
|
|
133
|
+
if (char === "]") inClass = false;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (char === "[") inClass = true;
|
|
137
|
+
else if (char === "{") braces += 1;
|
|
138
|
+
else if (char === "}") {
|
|
139
|
+
braces -= 1;
|
|
140
|
+
if (braces < 0) return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return braces === 0 && !inClass;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Opt-in, report-only audit rules for one collection (`gno audit links`).
|
|
148
|
+
* They never block indexing, retrieval or editing.
|
|
149
|
+
*/
|
|
150
|
+
export const CollectionAuditSchema = z
|
|
151
|
+
.object({
|
|
152
|
+
/** Other collections this one may link to; its own is always allowed. */
|
|
153
|
+
linkCollections: z
|
|
154
|
+
.array(
|
|
155
|
+
z
|
|
156
|
+
.string()
|
|
157
|
+
.regex(
|
|
158
|
+
COLLECTION_NAME_REGEX,
|
|
159
|
+
"audit.linkCollections entries must be collection names"
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
.max(256)
|
|
163
|
+
.optional(),
|
|
164
|
+
/** `report` lists external URLs written in documents. Not egress. */
|
|
165
|
+
externalUrls: z.enum(["report", "ignore"]).optional(),
|
|
166
|
+
/** Each document path must match at least one glob. */
|
|
167
|
+
paths: z
|
|
168
|
+
.array(
|
|
169
|
+
z
|
|
170
|
+
.string()
|
|
171
|
+
.max(COLLECTION_AUDIT_MAX_GLOB_CHARS)
|
|
172
|
+
.refine(isValidAuditPathGlob, {
|
|
173
|
+
message:
|
|
174
|
+
"audit.paths entries must be relative globs without '..' and with balanced {} and []",
|
|
175
|
+
})
|
|
176
|
+
)
|
|
177
|
+
.min(1, "audit.paths must list at least one glob")
|
|
178
|
+
.max(COLLECTION_AUDIT_MAX_PATHS)
|
|
179
|
+
.optional(),
|
|
180
|
+
})
|
|
181
|
+
.strict();
|
|
182
|
+
|
|
183
|
+
export type CollectionAuditConfig = z.infer<typeof CollectionAuditSchema>;
|
|
184
|
+
|
|
117
185
|
export const CollectionSchema = z.object({
|
|
118
186
|
/** Unique collection identifier (lowercase) */
|
|
119
187
|
name: z
|
|
@@ -184,6 +252,9 @@ export const CollectionSchema = z.object({
|
|
|
184
252
|
*/
|
|
185
253
|
memoryManaged: z.boolean().optional(),
|
|
186
254
|
|
|
255
|
+
/** Opt-in report-only link and path rules for `gno audit links`. */
|
|
256
|
+
audit: CollectionAuditSchema.optional(),
|
|
257
|
+
|
|
187
258
|
/** Optional per-collection model overrides */
|
|
188
259
|
models: z
|
|
189
260
|
.object({
|
|
@@ -616,7 +687,25 @@ export const ConfigSchema = z.object({
|
|
|
616
687
|
editorUriTemplate: z.string().min(1).optional(),
|
|
617
688
|
|
|
618
689
|
/** Collection definitions */
|
|
619
|
-
collections: z
|
|
690
|
+
collections: z
|
|
691
|
+
.array(CollectionSchema)
|
|
692
|
+
.superRefine((collections, ctx) => {
|
|
693
|
+
const names = new Set(collections.map(({ name }) => name));
|
|
694
|
+
for (const [index, collection] of collections.entries()) {
|
|
695
|
+
for (const [
|
|
696
|
+
position,
|
|
697
|
+
name,
|
|
698
|
+
] of collection.audit?.linkCollections?.entries() ?? []) {
|
|
699
|
+
if (names.has(name)) continue;
|
|
700
|
+
ctx.addIssue({
|
|
701
|
+
code: "custom",
|
|
702
|
+
message: `Collection "${collection.name}" audit.linkCollections names unknown collection "${name}"`,
|
|
703
|
+
path: [index, "audit", "linkCollections", position],
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
})
|
|
708
|
+
.default([]),
|
|
620
709
|
|
|
621
710
|
/** Context metadata */
|
|
622
711
|
contexts: z.array(ContextSchema).default([]),
|