@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.
Files changed (75) hide show
  1. package/README.md +1 -1
  2. package/assets/skill/SKILL.md +36 -5
  3. package/assets/skill/cli-reference.md +62 -11
  4. package/assets/skill/examples.md +30 -4
  5. package/assets/skill/mcp-reference.md +16 -0
  6. package/assets/spa-production.json.gz +0 -0
  7. package/browser-extension/artifacts/{gno-browser-clipper-v2.12.2.zip → gno-browser-clipper-v2.14.0.zip} +0 -0
  8. package/browser-extension/artifacts/gno-browser-clipper-v2.14.0.zip.sha256 +1 -0
  9. package/browser-extension/dist/manifest.json +1 -1
  10. package/package.json +1 -1
  11. package/spec/cli.md +139 -16
  12. package/spec/mcp.md +111 -3
  13. package/spec/output-schemas/collection-list.schema.json +28 -0
  14. package/spec/output-schemas/convert.schema.json +97 -0
  15. package/spec/output-schemas/ls.schema.json +65 -0
  16. package/spec/output-schemas/mcp-job-status.schema.json +17 -1
  17. package/spec/output-schemas/mcp-sync-result.schema.json +9 -2
  18. package/spec/output-schemas/search-results.schema.json +7 -0
  19. package/src/cli/commands/index-cmd.ts +50 -12
  20. package/src/cli/commands/ls.ts +20 -3
  21. package/src/cli/commands/profile-apply.ts +2 -1
  22. package/src/cli/commands/query.ts +21 -8
  23. package/src/cli/commands/search.ts +11 -0
  24. package/src/cli/commands/vsearch.ts +17 -4
  25. package/src/cli/options.ts +19 -0
  26. package/src/cli/program.ts +90 -10
  27. package/src/config/loader.ts +11 -1
  28. package/src/config/types.ts +90 -1
  29. package/src/core/audit-collection-rules.ts +331 -0
  30. package/src/core/audit-links.ts +3 -1
  31. package/src/core/audit-workspace.ts +69 -5
  32. package/src/core/collection-egress-policy-service.ts +4 -1
  33. package/src/core/collection-scope.ts +105 -0
  34. package/src/core/egress-enforcement.ts +20 -4
  35. package/src/core/egress-policy.ts +4 -0
  36. package/src/core/egress-provenance.ts +33 -11
  37. package/src/core/errors.ts +5 -0
  38. package/src/core/job-manager.ts +238 -19
  39. package/src/core/project-profile-apply-state.ts +6 -15
  40. package/src/core/project-profile-apply.ts +32 -1
  41. package/src/core/reindex-paths.ts +167 -0
  42. package/src/core/retrieval-trace-request.ts +1 -0
  43. package/src/ingestion/convert-file.ts +181 -0
  44. package/src/llm/registry.ts +20 -0
  45. package/src/mcp/http-egress.ts +21 -2
  46. package/src/mcp/tools/collection-scope.ts +25 -0
  47. package/src/mcp/tools/index-cmd.ts +66 -48
  48. package/src/mcp/tools/index.ts +74 -10
  49. package/src/mcp/tools/job-status.ts +7 -0
  50. package/src/mcp/tools/query.ts +28 -7
  51. package/src/mcp/tools/search.ts +4 -0
  52. package/src/mcp/tools/sync.ts +68 -60
  53. package/src/mcp/tools/vsearch.ts +9 -3
  54. package/src/pipeline/filters.ts +48 -5
  55. package/src/pipeline/graph-retrieval.ts +1 -0
  56. package/src/pipeline/hybrid.ts +15 -2
  57. package/src/pipeline/hydration.ts +7 -2
  58. package/src/pipeline/owner-fusion.ts +1 -0
  59. package/src/pipeline/search.ts +2 -0
  60. package/src/pipeline/types.ts +7 -0
  61. package/src/pipeline/vsearch.ts +10 -5
  62. package/src/sdk/client.ts +45 -12
  63. package/src/sdk/convert.ts +27 -0
  64. package/src/sdk/documents.ts +15 -2
  65. package/src/sdk/index.ts +2 -0
  66. package/src/sdk/types.ts +4 -1
  67. package/src/serve/jobs.ts +4 -1
  68. package/src/serve/public/components/IndexingProgress.tsx +25 -3
  69. package/src/serve/routes/api.ts +164 -24
  70. package/src/store/sqlite/adapter.ts +97 -27
  71. package/src/store/sqlite/eligibility.ts +4 -0
  72. package/src/store/types.ts +7 -0
  73. package/src/store/vector/sqlite-vec.ts +67 -15
  74. package/src/store/vector/variant-search.ts +41 -4
  75. package/browser-extension/artifacts/gno-browser-clipper-v2.12.2.zip.sha256 +0 -1
@@ -150,6 +150,7 @@ async function checkBm25Strength(
150
150
  query: string,
151
151
  options?: {
152
152
  collection?: string;
153
+ collections?: string[];
153
154
  lang?: string;
154
155
  tagsAll?: string[];
155
156
  tagsAny?: string[];
@@ -168,6 +169,7 @@ async function checkBm25Strength(
168
169
  const result = await store.searchFts(query, {
169
170
  limit: 5,
170
171
  collection: options?.collection,
172
+ collections: options?.collections,
171
173
  relPathPrefix: options?.relPathPrefix,
172
174
  language: options?.lang,
173
175
  chunkLanguage: options?.lang,
@@ -225,6 +227,7 @@ async function searchFtsChunks(
225
227
  options: {
226
228
  limit: number;
227
229
  collection?: string;
230
+ collections?: string[];
228
231
  lang?: string;
229
232
  tagsAll?: string[];
230
233
  tagsAny?: string[];
@@ -243,6 +246,7 @@ async function searchFtsChunks(
243
246
  const result = await store.searchFts(query, {
244
247
  limit: options.limit,
245
248
  collection: options.collection,
249
+ collections: options.collections,
246
250
  relPathPrefix: options.relPathPrefix,
247
251
  language: options.lang,
248
252
  chunkLanguage: options.lang,
@@ -500,6 +504,7 @@ async function searchHybridWithHydration(
500
504
  ? false
501
505
  : await checkBm25Strength(store, query, {
502
506
  collection: options.collection,
507
+ collections: options.collections,
503
508
  lang: options.lang,
504
509
  memoryScopesAny: options.memoryFilter?.scopes,
505
510
  excludeSuperseded: options.memoryFilter?.excludeSuperseded,
@@ -550,6 +555,7 @@ async function searchHybridWithHydration(
550
555
 
551
556
  const vectorEligibility: VectorSearchOptions["eligibility"] = {
552
557
  collection: options.collection,
558
+ collections: options.collections,
553
559
  memoryScopesAny: options.memoryFilter?.scopes,
554
560
  excludeSuperseded: options.memoryFilter?.excludeSuperseded,
555
561
  relPathPrefix: options.retrievalScope?.relPathPrefix,
@@ -575,6 +581,7 @@ async function searchHybridWithHydration(
575
581
  exclude: options.exclude,
576
582
  allowedMirrorHashes: options.retrievalScope?.allowedMirrorHashes,
577
583
  collection: options.collection,
584
+ collections: options.collections,
578
585
  lang: options.lang,
579
586
  tagsAll: options.tagsAll,
580
587
  tagsAny: options.tagsAny,
@@ -615,6 +622,7 @@ async function searchHybridWithHydration(
615
622
  exclude: options.exclude,
616
623
  allowedMirrorHashes: options.retrievalScope?.allowedMirrorHashes,
617
624
  collection: options.collection,
625
+ collections: options.collections,
618
626
  lang: options.lang,
619
627
  tagsAll: options.tagsAll,
620
628
  tagsAny: options.tagsAny,
@@ -806,7 +814,7 @@ async function searchHybridWithHydration(
806
814
  fusedCandidates,
807
815
  {
808
816
  collection: options.collection,
809
- collections: options.graphCollections,
817
+ collections: options.graphCollections ?? options.collections,
810
818
  includeSimilar: vectorAvailable,
811
819
  eligibility: vectorEligibility,
812
820
  limit,
@@ -879,6 +887,7 @@ async function searchHybridWithHydration(
879
887
  [...new Set(fusedCandidates.map((candidate) => candidate.mirrorHash))],
880
888
  {
881
889
  collection: options.collection,
890
+ collections: options.collections,
882
891
  activeOnly: true,
883
892
  }
884
893
  );
@@ -991,6 +1000,7 @@ async function searchHybridWithHydration(
991
1000
  [...neededHashes],
992
1001
  {
993
1002
  collection: options.collection,
1003
+ collections: options.collections,
994
1004
  activeOnly: true,
995
1005
  }
996
1006
  );
@@ -1374,7 +1384,9 @@ async function searchHybridWithHydration(
1374
1384
  {
1375
1385
  ownershipHashes: [...neededHashes],
1376
1386
  ownershipDocuments:
1377
- options.collection === undefined ? documents : undefined,
1387
+ options.collection === undefined && options.collections === undefined
1388
+ ? documents
1389
+ : undefined,
1378
1390
  collections: collectionsResult.ok ? collectionsResult.value : undefined,
1379
1391
  }
1380
1392
  );
@@ -1394,6 +1406,7 @@ async function searchHybridWithHydration(
1394
1406
  intent: options.intent,
1395
1407
  exclude: options.exclude,
1396
1408
  collection: options.collection,
1409
+ collections: options.collections,
1397
1410
  lang: options.lang,
1398
1411
  since: temporalRange.since,
1399
1412
  until: temporalRange.until,
@@ -142,13 +142,18 @@ export class RequestHydration {
142
142
  const store = this.store;
143
143
  if (!store) return err("QUERY_FAILED", "Hydration request released");
144
144
  // Capture caller-owned options before any asynchronous read. Collection
145
- // and activity are part of the lookup identity; titles never key by hash.
146
- const scope = { ...options };
145
+ // scope and activity are part of the lookup identity; titles never key by
146
+ // hash.
147
+ const scope = {
148
+ ...options,
149
+ collections: options?.collections ? [...options.collections] : undefined,
150
+ };
147
151
  // Cache the exact lookup, retaining the adapter's ordering (including SQL
148
152
  // batch boundaries). Regrouping by content hash would reorder documents.
149
153
  const requested = [...hashes];
150
154
  const identity = JSON.stringify([
151
155
  scope.collection ?? null,
156
+ scope.collections ?? null,
152
157
  scope.activeOnly ?? null,
153
158
  requested,
154
159
  ]);
@@ -30,6 +30,7 @@ export async function resolveFusionOwners(
30
30
  ];
31
31
  const documents = await hydration.getDocumentsByMirrorHashes(hashes, {
32
32
  collection: options.collection,
33
+ collections: options.collections,
33
34
  activeOnly: true,
34
35
  });
35
36
  const chunks = await hydration.getChunksBatch(hashes);
@@ -341,6 +341,7 @@ export async function searchBm25(
341
341
  const ftsResult = await store.searchFts(query, {
342
342
  limit: retrievalLimit,
343
343
  collection: options.collection,
344
+ collections: options.collections,
344
345
  allowedMirrorHashes: options.retrievalScope?.allowedMirrorHashes,
345
346
  exclude: options.exclude,
346
347
  relPathPrefix: options.retrievalScope?.relPathPrefix,
@@ -621,6 +622,7 @@ export async function searchBm25(
621
622
  intent: options.intent,
622
623
  exclude: options.exclude,
623
624
  collection: options.collection,
625
+ collections: options.collections,
624
626
  lang: options.lang,
625
627
  since: temporalRange.since,
626
628
  until: temporalRange.until,
@@ -114,6 +114,8 @@ export interface SearchMeta {
114
114
  totalResults: number;
115
115
  intent?: string;
116
116
  collection?: string;
117
+ /** Normalized collection allowlist; present only when the request set one. */
118
+ collections?: string[];
117
119
  lang?: string;
118
120
  /** Detected/overridden query language for prompt selection (typically BCP-47; may be user-provided via --lang) */
119
121
  queryLanguage?: string;
@@ -193,6 +195,11 @@ export interface SearchOptions extends InferenceOptions {
193
195
  minScore?: number;
194
196
  /** Filter by collection */
195
197
  collection?: string;
198
+ /**
199
+ * Filter to documents in ANY of these collections, before candidate limits.
200
+ * Never combined with `collection`; surfaces reject that pairing.
201
+ */
202
+ collections?: string[];
196
203
  /**
197
204
  * Internal graph allowlist for a request partitioned into one retrieval per
198
205
  * collection (Context Capsules, replay): graph neighbours may come from any
@@ -176,6 +176,7 @@ async function searchVectorWithEmbeddingOwned(
176
176
  excludeMetadata: true,
177
177
  semanticMetadata: true,
178
178
  collection: options.collection,
179
+ collections: options.collections,
179
180
  memoryScopesAny: options.memoryFilter?.scopes,
180
181
  excludeSuperseded: options.memoryFilter?.excludeSuperseded,
181
182
  relPathPrefix: options.retrievalScope?.relPathPrefix,
@@ -215,6 +216,7 @@ async function searchVectorWithEmbeddingOwned(
215
216
  const { documents: docsByMirrorHash, ownershipDocuments } =
216
217
  await buildDocumentMap(store, {
217
218
  collection: options.collection,
219
+ collections: options.collections,
218
220
  relPathPrefix: options.retrievalScope?.relPathPrefix,
219
221
  tagsAll: options.tagsAll,
220
222
  tagsAny: options.tagsAny,
@@ -547,6 +549,7 @@ async function searchVectorWithEmbeddingOwned(
547
549
  intent: options.intent,
548
550
  exclude: options.exclude,
549
551
  collection: options.collection,
552
+ collections: options.collections,
550
553
  lang: options.lang,
551
554
  since: temporalRange.since,
552
555
  until: temporalRange.until,
@@ -666,6 +669,7 @@ interface DocumentInfo {
666
669
 
667
670
  interface DocumentMapOptions {
668
671
  collection?: string;
672
+ collections?: string[];
669
673
  relPathPrefix?: string;
670
674
  tagsAll?: string[];
671
675
  tagsAny?: string[];
@@ -718,11 +722,12 @@ async function buildDocumentMap(
718
722
  const ownershipDocuments = options.mirrorHashes
719
723
  ? docs.value.filter((d) => d.mirrorHash)
720
724
  : docs.value.filter((d) => d.mirrorHash && d.active);
721
- const activeDocs = options.collection
722
- ? ownershipDocuments.filter(
723
- (document) => document.collection === options.collection
724
- )
725
- : ownershipDocuments;
725
+ const activeDocs = ownershipDocuments.filter(
726
+ (document) =>
727
+ (!options.collection || document.collection === options.collection) &&
728
+ (options.collections === undefined ||
729
+ options.collections.includes(document.collection))
730
+ );
726
731
  const temporalRange: TemporalRange = {
727
732
  since: options.since,
728
733
  until: options.until,
package/src/sdk/client.ts CHANGED
@@ -110,6 +110,7 @@ import {
110
110
  import { publishCapture } from "../core/capture-publish";
111
111
  import { projectCollectionEgressPolicy } from "../core/collection-egress-policy-projection";
112
112
  import { CollectionEgressPolicyService } from "../core/collection-egress-policy-service";
113
+ import { resolveCollectionScope } from "../core/collection-scope";
113
114
  import { applyConfigChange } from "../core/config-mutation";
114
115
  import { getDocumentCapabilities } from "../core/document-capabilities";
115
116
  import { EgressAuditService } from "../core/egress-audit";
@@ -196,7 +197,7 @@ import {
196
197
  } from "../llm/inference-scope";
197
198
  import { LlmAdapter } from "../llm/nodeLlamaCpp/adapter";
198
199
  import { resolveDownloadPolicy } from "../llm/policy";
199
- import { resolveModelUri } from "../llm/registry";
200
+ import { modelCollectionForList, resolveModelUri } from "../llm/registry";
200
201
  import {
201
202
  answerTraceTerminalStatus,
202
203
  generateGroundedAnswer,
@@ -476,6 +477,27 @@ class GnoClientImpl implements GnoClient {
476
477
  }
477
478
  }
478
479
 
480
+ /** Normalize and validate a search/vsearch/query `collections` allowlist. */
481
+ private withCollectionScope<
482
+ T extends { collection?: string; collections?: string[] },
483
+ >(options: T): T {
484
+ const scope = resolveCollectionScope(options, this.config.collections);
485
+ if (!scope.ok) throw sdkError("VALIDATION", scope.message);
486
+ return scope.collections === undefined
487
+ ? options
488
+ : { ...options, collections: scope.collections };
489
+ }
490
+
491
+ private modelCollection(
492
+ type: "embed" | "expand" | "rerank",
493
+ options: { collection?: string; collections?: string[] }
494
+ ): string | undefined {
495
+ return (
496
+ options.collection ??
497
+ modelCollectionForList(this.config, type, options.collections)
498
+ );
499
+ }
500
+
479
501
  private getCollections(collection?: string) {
480
502
  if (!collection) {
481
503
  return this.config.collections;
@@ -541,6 +563,7 @@ class GnoClientImpl implements GnoClient {
541
563
  answer?: boolean;
542
564
  rerank?: boolean;
543
565
  collection?: string;
566
+ collections?: string[];
544
567
  requiredEmbed?: boolean;
545
568
  requiredExpand?: boolean;
546
569
  requiredAnswer?: boolean;
@@ -551,9 +574,9 @@ class GnoClientImpl implements GnoClient {
551
574
  rerankModel?: string;
552
575
  }): Promise<RuntimePorts> {
553
576
  this.assertOpen();
554
- const egressCollections = options.collection
555
- ? [options.collection]
556
- : ("all" as const);
577
+ const egressCollections =
578
+ options.collections ??
579
+ (options.collection ? [options.collection] : ("all" as const));
557
580
 
558
581
  let embedPort: EmbeddingPort | null = null;
559
582
  let expandPort: GenerationPort | null = null;
@@ -567,7 +590,7 @@ class GnoClientImpl implements GnoClient {
567
590
  this.config,
568
591
  "embed",
569
592
  options.embedModel,
570
- options.collection
593
+ this.modelCollection("embed", options)
571
594
  ),
572
595
  {
573
596
  egressCollections,
@@ -612,7 +635,7 @@ class GnoClientImpl implements GnoClient {
612
635
  this.config,
613
636
  "expand",
614
637
  options.expandModel ?? options.genModel,
615
- options.collection
638
+ this.modelCollection("expand", options)
616
639
  ),
617
640
  {
618
641
  egressCollections,
@@ -665,7 +688,7 @@ class GnoClientImpl implements GnoClient {
665
688
  this.config,
666
689
  "rerank",
667
690
  options.rerankModel,
668
- options.collection
691
+ this.modelCollection("rerank", options)
669
692
  ),
670
693
  {
671
694
  egressCollections,
@@ -735,6 +758,7 @@ class GnoClientImpl implements GnoClient {
735
758
  options: GnoSearchOptions
736
759
  ): Promise<SearchResults> {
737
760
  this.assertOpen();
761
+ options = this.withCollectionScope(options);
738
762
  let traceSession: RetrievalTraceSession | null = null;
739
763
  try {
740
764
  const { projectHints, ...searchOptions } = options;
@@ -789,6 +813,7 @@ class GnoClientImpl implements GnoClient {
789
813
  options: GnoVectorSearchOptions
790
814
  ): Promise<SearchResults> {
791
815
  this.assertOpen();
816
+ options = this.withCollectionScope(options);
792
817
 
793
818
  let ports: RuntimePorts | null = null;
794
819
  let traceSession: RetrievalTraceSession | null = null;
@@ -805,7 +830,7 @@ class GnoClientImpl implements GnoClient {
805
830
  this.config,
806
831
  "embed",
807
832
  options.model,
808
- options.collection
833
+ this.modelCollection("embed", options)
809
834
  );
810
835
  traceSession = unwrapStore(
811
836
  await startRetrievalTraceRequest({
@@ -823,6 +848,7 @@ class GnoClientImpl implements GnoClient {
823
848
  requiredEmbed: true,
824
849
  embedModel: options.model,
825
850
  collection: options.collection,
851
+ collections: options.collections,
826
852
  });
827
853
  if (!ports.embedPort || !ports.vectorIndex) {
828
854
  throw sdkError(
@@ -882,6 +908,7 @@ class GnoClientImpl implements GnoClient {
882
908
  options: GnoQueryOptions
883
909
  ): Promise<SearchResults> {
884
910
  this.assertOpen();
911
+ options = this.withCollectionScope(options);
885
912
 
886
913
  const normalizedInput = normalizeStructuredQueryInput(
887
914
  query,
@@ -905,14 +932,14 @@ class GnoClientImpl implements GnoClient {
905
932
  this.config,
906
933
  "embed",
907
934
  options.embedModel,
908
- options.collection
935
+ this.modelCollection("embed", options)
909
936
  );
910
937
  const expandUri = expandRequested
911
938
  ? resolveModelUri(
912
939
  this.config,
913
940
  "expand",
914
941
  options.expandModel ?? options.genModel,
915
- options.collection
942
+ this.modelCollection("expand", options)
916
943
  )
917
944
  : undefined;
918
945
  const rerankUri = rerankRequested
@@ -920,7 +947,7 @@ class GnoClientImpl implements GnoClient {
920
947
  this.config,
921
948
  "rerank",
922
949
  options.rerankModel,
923
- options.collection
950
+ this.modelCollection("rerank", options)
924
951
  )
925
952
  : undefined;
926
953
  let ports: RuntimePorts | null = null;
@@ -956,6 +983,7 @@ class GnoClientImpl implements GnoClient {
956
983
  genModel: options.genModel,
957
984
  rerankModel: options.rerankModel,
958
985
  collection: options.collection,
986
+ collections: options.collections,
959
987
  });
960
988
  return attachRetrievalTraceMetadata(
961
989
  this.decorateSearchResults(
@@ -1496,7 +1524,12 @@ class GnoClientImpl implements GnoClient {
1496
1524
 
1497
1525
  async list(options: GnoListOptions = {}) {
1498
1526
  this.assertOpen();
1499
- const result = await listDocuments(this.store, options);
1527
+ const result = await listDocuments(
1528
+ this.store,
1529
+ options.filter === undefined
1530
+ ? options
1531
+ : { ...options, filter: normalizeSdkMetadataFilter(options.filter) }
1532
+ );
1500
1533
  return {
1501
1534
  ...result,
1502
1535
  documents: result.documents.map((doc) => ({
@@ -0,0 +1,27 @@
1
+ /**
2
+ * SDK conversion of one file to the Markdown GNO would index (no client,
3
+ * config or index needed).
4
+ *
5
+ * @module src/sdk/convert
6
+ */
7
+
8
+ import {
9
+ type ConvertFileResult,
10
+ convertFile as convertOneFile,
11
+ } from "../ingestion/convert-file";
12
+ import { sdkError } from "./errors";
13
+
14
+ /**
15
+ * Convert one file to the Markdown GNO would index for it; nothing is
16
+ * persisted. Failures throw `GnoSdkError` with `details.reason` set to the
17
+ * same reason `gno convert` reports.
18
+ */
19
+ export async function convertFile(path: string): Promise<ConvertFileResult> {
20
+ const result = await convertOneFile(path);
21
+ if (result.ok) return result.value;
22
+ const { reason, message, validation, details } = result.error;
23
+ let code: "NOT_FOUND" | "VALIDATION" | "RUNTIME" = "RUNTIME";
24
+ if (reason === "NOT_FOUND") code = "NOT_FOUND";
25
+ else if (validation) code = "VALIDATION";
26
+ throw sdkError(code, message, { details: { reason, ...details } });
27
+ }
@@ -30,6 +30,10 @@ import {
30
30
  evidenceFromExactDocument,
31
31
  RetrievalTraceSession,
32
32
  } from "../core/retrieval-trace-session";
33
+ import {
34
+ filterDocumentsByMetadata,
35
+ metadataCoverageWarnings,
36
+ } from "../pipeline/filters";
33
37
  import { supersedingUri } from "../pipeline/result-context";
34
38
  import { sdkError } from "./errors";
35
39
 
@@ -388,11 +392,19 @@ export async function listDocuments(
388
392
  });
389
393
  }
390
394
 
391
- const allActive = docsResult.value
395
+ const inScope = docsResult.value
392
396
  .filter((d) => d.active)
393
397
  .filter((d) =>
394
398
  !scope?.startsWith("gno://") ? true : d.uri.startsWith(scope)
395
- )
399
+ );
400
+ const filtered = options.filter
401
+ ? filterDocumentsByMetadata(inScope, options.filter)
402
+ : undefined;
403
+ const warnings = filtered
404
+ ? metadataCoverageWarnings(filtered, "listing")
405
+ : undefined;
406
+
407
+ const allActive = (filtered?.matches ?? inScope)
396
408
  .map((d) => ({
397
409
  docid: d.docid,
398
410
  uri: d.uri,
@@ -415,6 +427,7 @@ export async function listDocuments(
415
427
  total: allActive.length,
416
428
  returned: paged.length,
417
429
  offset,
430
+ ...(warnings ? { warnings } : {}),
418
431
  },
419
432
  };
420
433
  }
package/src/sdk/index.ts CHANGED
@@ -27,6 +27,8 @@ export type { IndexStatus } from "../store/types";
27
27
 
28
28
  export { GnoSdkError, sdkError } from "./errors";
29
29
  export { createGnoClient } from "./client";
30
+ export { convertFile } from "./convert";
31
+ export type { ConvertFileResult } from "../ingestion/convert-file";
30
32
  export type {
31
33
  DocumentSection,
32
34
  GnoAskOptions,
package/src/sdk/types.ts CHANGED
@@ -89,6 +89,7 @@ import type {
89
89
  SectionTargetResolveResult,
90
90
  SectionTargetV1,
91
91
  } from "../core/sections";
92
+ import type { MetadataPredicate } from "../core/typed-metadata";
92
93
  import type { SyncResult } from "../ingestion";
93
94
  import type { CollectionRootError } from "../ingestion/source-availability/root";
94
95
  import type { DownloadPolicy } from "../llm/policy";
@@ -179,7 +180,7 @@ export type GnoQueryOptions = Omit<
179
180
  GnoProjectHintOptions;
180
181
  export type GnoAskOptions = Omit<
181
182
  AskOptions,
182
- "contentTypeRules" | "projectAffinity"
183
+ "collections" | "contentTypeRules" | "projectAffinity"
183
184
  > &
184
185
  GnoModelOverrides &
185
186
  GnoProjectHintOptions;
@@ -223,6 +224,8 @@ export interface GnoListOptions {
223
224
  scope?: string;
224
225
  limit?: number;
225
226
  offset?: number;
227
+ /** Typed metadata predicate, as for search `filter`. */
228
+ filter?: MetadataPredicate;
226
229
  }
227
230
 
228
231
  export interface GnoUpdateOptions {
package/src/serve/jobs.ts CHANGED
@@ -16,11 +16,13 @@ export interface JobProgress {
16
16
  export interface JobStatus {
17
17
  id: string;
18
18
  type: JobType;
19
- status: "running" | "completed" | "failed";
19
+ status: "queued" | "running" | "completed" | "failed";
20
20
  createdAt: number;
21
21
  progress?: JobProgress;
22
22
  result?: SyncResult;
23
23
  error?: string;
24
+ /** Reindex jobs: the collection (null = all) and merged paths (null = whole). */
25
+ scope?: JobRecord["scope"];
24
26
  }
25
27
 
26
28
  export interface StartJobSuccess {
@@ -49,6 +51,7 @@ const fromResidentRecord = (record: JobRecord): JobStatus => ({
49
51
  progress: record.progress,
50
52
  result: record.result,
51
53
  error: record.error,
54
+ ...(record.scope ? { scope: record.scope } : {}),
52
55
  });
53
56
 
54
57
  function cleanupExpiredJobs(now = Date.now()): void {
@@ -24,7 +24,7 @@ import { Badge } from "./ui/badge";
24
24
  export interface JobStatus {
25
25
  id: string;
26
26
  type: "add" | "sync";
27
- status: "running" | "completed" | "failed";
27
+ status: "queued" | "running" | "completed" | "failed";
28
28
  createdAt: number;
29
29
  result?: SyncResult;
30
30
  error?: string;
@@ -138,7 +138,7 @@ export function IndexingProgress({
138
138
  setStatus(data);
139
139
  setFetchError(null);
140
140
 
141
- if (data.status === "running") {
141
+ if (data.status === "queued" || data.status === "running") {
142
142
  pollTimeout = setTimeout(() => {
143
143
  void poll();
144
144
  }, 1000);
@@ -187,6 +187,20 @@ export function IndexingProgress({
187
187
 
188
188
  // Compact mode
189
189
  if (compact) {
190
+ if (status?.status === "queued") {
191
+ return (
192
+ <div
193
+ aria-live="polite"
194
+ className={cn("flex items-center gap-2 text-sm", className)}
195
+ >
196
+ <Loader2Icon className="size-4 animate-spin text-muted-foreground" />
197
+ <span className="text-muted-foreground">
198
+ Queued... {formatElapsed(elapsed)}
199
+ </span>
200
+ </div>
201
+ );
202
+ }
203
+
190
204
  if (status?.status === "running") {
191
205
  return (
192
206
  <div
@@ -237,6 +251,14 @@ export function IndexingProgress({
237
251
  {/* Status header */}
238
252
  <div className="flex items-center justify-between">
239
253
  <div className="flex items-center gap-2">
254
+ {status?.status === "queued" && (
255
+ <>
256
+ <Loader2Icon className="size-5 animate-spin text-muted-foreground" />
257
+ <span className="font-medium">
258
+ Queued behind another indexing job
259
+ </span>
260
+ </>
261
+ )}
240
262
  {status?.status === "running" && (
241
263
  <>
242
264
  <Loader2Icon className="size-5 animate-spin text-primary" />
@@ -264,7 +286,7 @@ export function IndexingProgress({
264
286
  <div className="flex items-center gap-1.5 text-muted-foreground text-sm">
265
287
  <TimerIcon className="size-4" />
266
288
  <span>
267
- {status?.status === "running"
289
+ {status?.status === "queued" || status?.status === "running"
268
290
  ? formatElapsed(elapsed)
269
291
  : formatDuration(status?.result?.totalDurationMs ?? elapsed)}
270
292
  </span>