@karmaniverous/jeeves-watcher 0.8.5 → 0.9.0-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.
@@ -3373,6 +3373,44 @@ function createRulesUnregisterParamHandler(deps) {
3373
3373
  }, deps.logger, 'RulesUnregister');
3374
3374
  }
3375
3375
 
3376
+ /**
3377
+ * @module api/handlers/scan
3378
+ * Fastify route handler for POST /scan. Filter-only point query without vector search.
3379
+ */
3380
+ /**
3381
+ * Create handler for POST /scan.
3382
+ *
3383
+ * @param deps - Route dependencies.
3384
+ */
3385
+ function createScanHandler(deps) {
3386
+ return wrapHandler(async (request, reply) => {
3387
+ const { filter, limit = 100, cursor, fields, countOnly } = request.body;
3388
+ if (!filter || typeof filter !== 'object') {
3389
+ deps.logger.warn('Scan rejected: missing or invalid filter');
3390
+ void reply
3391
+ .status(400)
3392
+ .send({ error: 'Missing required field: filter (object)' });
3393
+ return;
3394
+ }
3395
+ if (typeof limit !== 'number' || limit < 1 || limit > 1000) {
3396
+ deps.logger.warn({ limit }, 'Scan rejected: limit out of bounds');
3397
+ void reply
3398
+ .status(400)
3399
+ .send({ error: 'limit must be between 1 and 1000' });
3400
+ return;
3401
+ }
3402
+ if (countOnly) {
3403
+ const count = await deps.vectorStore.count(filter);
3404
+ return { count };
3405
+ }
3406
+ const result = await deps.vectorStore.scrollPage(filter, limit, cursor, fields);
3407
+ return {
3408
+ points: result.points,
3409
+ cursor: result.nextCursor ?? null,
3410
+ };
3411
+ }, deps.logger, 'Scan');
3412
+ }
3413
+
3376
3414
  /**
3377
3415
  * @module api/handlers/search
3378
3416
  * Fastify route handler for POST /search. Embeds a query and performs vector store similarity search.
@@ -3554,6 +3592,10 @@ function createApiServer(options) {
3554
3592
  textWeight: config.search.hybrid.textWeight,
3555
3593
  }
3556
3594
  : undefined;
3595
+ app.post('/scan', createScanHandler({
3596
+ vectorStore,
3597
+ logger,
3598
+ }));
3557
3599
  app.post('/search', createSearchHandler({
3558
3600
  embeddingProvider,
3559
3601
  vectorStore,
@@ -4811,6 +4853,28 @@ async function getCollectionInfo(client, collectionName) {
4811
4853
  return { pointCount, dimensions, payloadFields };
4812
4854
  }
4813
4855
 
4856
+ /**
4857
+ * @module vectorStore/count
4858
+ * Count utility for Qdrant collection points.
4859
+ */
4860
+ /**
4861
+ * Count points in a Qdrant collection matching an optional filter.
4862
+ *
4863
+ * Uses exact counting for accurate results.
4864
+ *
4865
+ * @param client - The Qdrant client instance.
4866
+ * @param collectionName - The collection to count.
4867
+ * @param filter - Optional Qdrant filter.
4868
+ * @returns The number of matching points.
4869
+ */
4870
+ async function countPoints(client, collectionName, filter) {
4871
+ const result = await client.count(collectionName, {
4872
+ ...(filter ? { filter } : {}),
4873
+ exact: true,
4874
+ });
4875
+ return result.count;
4876
+ }
4877
+
4814
4878
  /**
4815
4879
  * @module vectorStore/hybridSearch
4816
4880
  * Hybrid search and text index helpers for Qdrant vector store.
@@ -4910,11 +4974,43 @@ async function hybridSearch(client, collectionName, vector, queryText, limit, te
4910
4974
 
4911
4975
  /**
4912
4976
  * @module vectorStore/scroll
4913
- * Standalone scroll utility for paginating through Qdrant collection points.
4977
+ * Scroll utilities for paginating through Qdrant collection points.
4978
+ */
4979
+ /**
4980
+ * Scroll one page of points matching a filter.
4981
+ *
4982
+ * @param client - The Qdrant client instance.
4983
+ * @param collectionName - The collection to scroll.
4984
+ * @param filter - Optional Qdrant filter.
4985
+ * @param limit - Page size.
4986
+ * @param offset - Cursor offset from previous page.
4987
+ * @param fields - Optional payload field projection (array of field names).
4988
+ * @returns Page of points and next cursor.
4914
4989
  */
4990
+ async function scrollPage(client, collectionName, filter, limit = 100, offset, fields) {
4991
+ const result = await client.scroll(collectionName, {
4992
+ limit,
4993
+ with_payload: fields ? fields : true,
4994
+ with_vector: false,
4995
+ ...(filter ? { filter } : {}),
4996
+ ...(offset !== undefined ? { offset } : {}),
4997
+ });
4998
+ return {
4999
+ points: result.points.map((p) => ({
5000
+ id: String(p.id),
5001
+ payload: p.payload,
5002
+ })),
5003
+ nextCursor: typeof result.next_page_offset === 'string' ||
5004
+ typeof result.next_page_offset === 'number'
5005
+ ? result.next_page_offset
5006
+ : undefined,
5007
+ };
5008
+ }
4915
5009
  /**
4916
5010
  * Scroll through all points in a Qdrant collection matching a filter.
4917
5011
  *
5012
+ * Iterates over pages using {@link scrollPage}.
5013
+ *
4918
5014
  * @param client - The Qdrant client instance.
4919
5015
  * @param collectionName - The collection to scroll.
4920
5016
  * @param filter - Optional Qdrant filter.
@@ -4922,32 +5018,14 @@ async function hybridSearch(client, collectionName, vector, queryText, limit, te
4922
5018
  * @yields Scrolled points.
4923
5019
  */
4924
5020
  async function* scrollCollection(client, collectionName, filter, limit = 100) {
4925
- let offset = undefined;
4926
- for (;;) {
4927
- const result = await client.scroll(collectionName, {
4928
- limit,
4929
- with_payload: true,
4930
- with_vector: false,
4931
- ...(filter ? { filter } : {}),
4932
- ...(offset !== undefined ? { offset } : {}),
4933
- });
4934
- for (const point of result.points) {
4935
- yield {
4936
- id: String(point.id),
4937
- payload: point.payload,
4938
- };
4939
- }
4940
- const nextOffset = result.next_page_offset;
4941
- if (nextOffset === null || nextOffset === undefined) {
4942
- break;
4943
- }
4944
- if (typeof nextOffset === 'string' || typeof nextOffset === 'number') {
4945
- offset = nextOffset;
4946
- }
4947
- else {
4948
- break;
5021
+ let cursor;
5022
+ do {
5023
+ const page = await scrollPage(client, collectionName, filter, limit, cursor);
5024
+ for (const point of page.points) {
5025
+ yield point;
4949
5026
  }
4950
- }
5027
+ cursor = page.nextCursor;
5028
+ } while (cursor !== undefined);
4951
5029
  }
4952
5030
 
4953
5031
  /**
@@ -4991,6 +5069,15 @@ class VectorStoreClient {
4991
5069
  checkCompatibility: false,
4992
5070
  });
4993
5071
  }
5072
+ /**
5073
+ * Count points matching a filter.
5074
+ *
5075
+ * @param filter - Optional Qdrant filter.
5076
+ * @returns The number of matching points.
5077
+ */
5078
+ async count(filter) {
5079
+ return countPoints(this.client, this.collectionName, filter);
5080
+ }
4994
5081
  /**
4995
5082
  * Ensure the collection exists with correct dimensions and Cosine distance.
4996
5083
  */
@@ -5171,6 +5258,18 @@ class VectorStoreClient {
5171
5258
  async hybridSearch(vector, queryText, limit, textWeight, filter) {
5172
5259
  return hybridSearch(this.client, this.collectionName, vector, queryText, limit, textWeight, filter);
5173
5260
  }
5261
+ /**
5262
+ * Scroll one page of points matching a filter.
5263
+ *
5264
+ * @param filter - Optional Qdrant filter.
5265
+ * @param limit - Page size.
5266
+ * @param offset - Cursor offset from previous page.
5267
+ * @param fields - Optional field projection.
5268
+ * @returns Page of points and next cursor.
5269
+ */
5270
+ async scrollPage(filter, limit = 100, offset, fields) {
5271
+ return scrollPage(this.client, this.collectionName, filter, limit, offset, fields);
5272
+ }
5174
5273
  /**
5175
5274
  * Scroll through all points matching a filter.
5176
5275
  *
@@ -6519,6 +6618,50 @@ function registerReindexCommand(cli) {
6519
6618
  });
6520
6619
  }
6521
6620
 
6621
+ /**
6622
+ * @module commands/scan
6623
+ *
6624
+ * CLI command: scan.
6625
+ */
6626
+ function registerScanCommand(cli) {
6627
+ const command = cli
6628
+ .command('scan')
6629
+ .description('Scan the vector store (POST /scan)')
6630
+ .option('-f, --filter <filter>', 'Qdrant filter (JSON string)', '{}')
6631
+ .option('-l, --limit <limit>', 'Max results', '100')
6632
+ .option('-c, --cursor <cursor>', 'Cursor from previous response')
6633
+ .option('--fields <fields>', 'Fields to return (comma-separated)')
6634
+ .option('--count-only', 'Return count only');
6635
+ withApiOptions(command).action(async (options) => {
6636
+ let filterObj = {};
6637
+ try {
6638
+ if (options.filter) {
6639
+ filterObj = JSON.parse(options.filter);
6640
+ }
6641
+ }
6642
+ catch (error) {
6643
+ console.error('Invalid filter JSON:', error);
6644
+ process.exit(1);
6645
+ }
6646
+ const fieldsArray = options.fields
6647
+ ? options.fields.split(',').map((f) => f.trim())
6648
+ : undefined;
6649
+ await runApiCommand({
6650
+ host: options.host,
6651
+ port: options.port,
6652
+ method: 'POST',
6653
+ path: '/scan',
6654
+ body: {
6655
+ filter: filterObj,
6656
+ limit: Number(options.limit),
6657
+ cursor: options.cursor,
6658
+ fields: fieldsArray,
6659
+ countOnly: options.countOnly,
6660
+ },
6661
+ });
6662
+ });
6663
+ }
6664
+
6522
6665
  /**
6523
6666
  * @module commands/search
6524
6667
  *
@@ -6684,6 +6827,7 @@ registerStatusCommand(cli);
6684
6827
  registerReindexCommand(cli);
6685
6828
  registerRebuildMetadataCommand(cli);
6686
6829
  registerSearchCommand(cli);
6830
+ registerScanCommand(cli);
6687
6831
  registerEnrichCommand(cli);
6688
6832
  registerConfigReindexCommand(cli);
6689
6833
  registerServiceCommand(cli);
package/dist/index.d.ts CHANGED
@@ -676,6 +676,13 @@ interface ScrolledPoint {
676
676
  /** The payload metadata. */
677
677
  payload: Record<string, unknown>;
678
678
  }
679
+ /** Result of a single scroll page. */
680
+ interface ScrollPageResult {
681
+ /** Matched points. */
682
+ points: ScrolledPoint[];
683
+ /** Cursor for next page, or `undefined` when no more pages. */
684
+ nextCursor?: string | number;
685
+ }
679
686
  /** Payload field schema information as reported by Qdrant. */
680
687
  interface PayloadFieldSchema {
681
688
  /** Qdrant data type for the field (e.g. `keyword`, `text`, `integer`). */
@@ -702,6 +709,13 @@ interface VectorStore {
702
709
  * Ensure the collection exists with correct configuration.
703
710
  */
704
711
  ensureCollection(): Promise<void>;
712
+ /**
713
+ * Count points matching a filter.
714
+ *
715
+ * @param filter - Optional Qdrant filter.
716
+ * @returns The number of matching points.
717
+ */
718
+ count(filter?: Record<string, unknown>): Promise<number>;
705
719
  /**
706
720
  * Upsert points into the collection.
707
721
  *
@@ -742,6 +756,16 @@ interface VectorStore {
742
756
  * @returns An array of search results.
743
757
  */
744
758
  search(vector: number[], limit: number, filter?: Record<string, unknown>, offset?: number): Promise<SearchResult[]>;
759
+ /**
760
+ * Scroll one page of points matching a filter.
761
+ *
762
+ * @param filter - Optional Qdrant filter.
763
+ * @param limit - Page size.
764
+ * @param offset - Cursor offset from previous page.
765
+ * @param fields - Optional field projection.
766
+ * @returns Page of points and next cursor.
767
+ */
768
+ scrollPage(filter?: Record<string, unknown>, limit?: number, offset?: string | number, fields?: string[]): Promise<ScrollPageResult>;
745
769
  /**
746
770
  * Scroll through all points matching a filter.
747
771
  *
@@ -798,6 +822,13 @@ declare class VectorStoreClient implements VectorStore {
798
822
  * Creating a fresh client for write operations ensures clean TCP connections.
799
823
  */
800
824
  private createClient;
825
+ /**
826
+ * Count points matching a filter.
827
+ *
828
+ * @param filter - Optional Qdrant filter.
829
+ * @returns The number of matching points.
830
+ */
831
+ count(filter?: Record<string, unknown>): Promise<number>;
801
832
  /**
802
833
  * Ensure the collection exists with correct dimensions and Cosine distance.
803
834
  */
@@ -877,6 +908,16 @@ declare class VectorStoreClient implements VectorStore {
877
908
  * @returns An array of search results.
878
909
  */
879
910
  hybridSearch(vector: number[], queryText: string, limit: number, textWeight: number, filter?: Record<string, unknown>): Promise<SearchResult[]>;
911
+ /**
912
+ * Scroll one page of points matching a filter.
913
+ *
914
+ * @param filter - Optional Qdrant filter.
915
+ * @param limit - Page size.
916
+ * @param offset - Cursor offset from previous page.
917
+ * @param fields - Optional field projection.
918
+ * @returns Page of points and next cursor.
919
+ */
920
+ scrollPage(filter?: Record<string, unknown>, limit?: number, offset?: string | number, fields?: string[]): Promise<ScrollPageResult>;
880
921
  /**
881
922
  * Scroll through all points matching a filter.
882
923
  *
package/dist/index.js CHANGED
@@ -3059,6 +3059,44 @@ function createRulesUnregisterParamHandler(deps) {
3059
3059
  }, deps.logger, 'RulesUnregister');
3060
3060
  }
3061
3061
 
3062
+ /**
3063
+ * @module api/handlers/scan
3064
+ * Fastify route handler for POST /scan. Filter-only point query without vector search.
3065
+ */
3066
+ /**
3067
+ * Create handler for POST /scan.
3068
+ *
3069
+ * @param deps - Route dependencies.
3070
+ */
3071
+ function createScanHandler(deps) {
3072
+ return wrapHandler(async (request, reply) => {
3073
+ const { filter, limit = 100, cursor, fields, countOnly } = request.body;
3074
+ if (!filter || typeof filter !== 'object') {
3075
+ deps.logger.warn('Scan rejected: missing or invalid filter');
3076
+ void reply
3077
+ .status(400)
3078
+ .send({ error: 'Missing required field: filter (object)' });
3079
+ return;
3080
+ }
3081
+ if (typeof limit !== 'number' || limit < 1 || limit > 1000) {
3082
+ deps.logger.warn({ limit }, 'Scan rejected: limit out of bounds');
3083
+ void reply
3084
+ .status(400)
3085
+ .send({ error: 'limit must be between 1 and 1000' });
3086
+ return;
3087
+ }
3088
+ if (countOnly) {
3089
+ const count = await deps.vectorStore.count(filter);
3090
+ return { count };
3091
+ }
3092
+ const result = await deps.vectorStore.scrollPage(filter, limit, cursor, fields);
3093
+ return {
3094
+ points: result.points,
3095
+ cursor: result.nextCursor ?? null,
3096
+ };
3097
+ }, deps.logger, 'Scan');
3098
+ }
3099
+
3062
3100
  /**
3063
3101
  * @module api/handlers/search
3064
3102
  * Fastify route handler for POST /search. Embeds a query and performs vector store similarity search.
@@ -3240,6 +3278,10 @@ function createApiServer(options) {
3240
3278
  textWeight: config.search.hybrid.textWeight,
3241
3279
  }
3242
3280
  : undefined;
3281
+ app.post('/scan', createScanHandler({
3282
+ vectorStore,
3283
+ logger,
3284
+ }));
3243
3285
  app.post('/search', createSearchHandler({
3244
3286
  embeddingProvider,
3245
3287
  vectorStore,
@@ -4789,6 +4831,28 @@ async function getCollectionInfo(client, collectionName) {
4789
4831
  return { pointCount, dimensions, payloadFields };
4790
4832
  }
4791
4833
 
4834
+ /**
4835
+ * @module vectorStore/count
4836
+ * Count utility for Qdrant collection points.
4837
+ */
4838
+ /**
4839
+ * Count points in a Qdrant collection matching an optional filter.
4840
+ *
4841
+ * Uses exact counting for accurate results.
4842
+ *
4843
+ * @param client - The Qdrant client instance.
4844
+ * @param collectionName - The collection to count.
4845
+ * @param filter - Optional Qdrant filter.
4846
+ * @returns The number of matching points.
4847
+ */
4848
+ async function countPoints(client, collectionName, filter) {
4849
+ const result = await client.count(collectionName, {
4850
+ ...(filter ? { filter } : {}),
4851
+ exact: true,
4852
+ });
4853
+ return result.count;
4854
+ }
4855
+
4792
4856
  /**
4793
4857
  * @module vectorStore/hybridSearch
4794
4858
  * Hybrid search and text index helpers for Qdrant vector store.
@@ -4888,11 +4952,43 @@ async function hybridSearch(client, collectionName, vector, queryText, limit, te
4888
4952
 
4889
4953
  /**
4890
4954
  * @module vectorStore/scroll
4891
- * Standalone scroll utility for paginating through Qdrant collection points.
4955
+ * Scroll utilities for paginating through Qdrant collection points.
4956
+ */
4957
+ /**
4958
+ * Scroll one page of points matching a filter.
4959
+ *
4960
+ * @param client - The Qdrant client instance.
4961
+ * @param collectionName - The collection to scroll.
4962
+ * @param filter - Optional Qdrant filter.
4963
+ * @param limit - Page size.
4964
+ * @param offset - Cursor offset from previous page.
4965
+ * @param fields - Optional payload field projection (array of field names).
4966
+ * @returns Page of points and next cursor.
4892
4967
  */
4968
+ async function scrollPage(client, collectionName, filter, limit = 100, offset, fields) {
4969
+ const result = await client.scroll(collectionName, {
4970
+ limit,
4971
+ with_payload: fields ? fields : true,
4972
+ with_vector: false,
4973
+ ...(filter ? { filter } : {}),
4974
+ ...(offset !== undefined ? { offset } : {}),
4975
+ });
4976
+ return {
4977
+ points: result.points.map((p) => ({
4978
+ id: String(p.id),
4979
+ payload: p.payload,
4980
+ })),
4981
+ nextCursor: typeof result.next_page_offset === 'string' ||
4982
+ typeof result.next_page_offset === 'number'
4983
+ ? result.next_page_offset
4984
+ : undefined,
4985
+ };
4986
+ }
4893
4987
  /**
4894
4988
  * Scroll through all points in a Qdrant collection matching a filter.
4895
4989
  *
4990
+ * Iterates over pages using {@link scrollPage}.
4991
+ *
4896
4992
  * @param client - The Qdrant client instance.
4897
4993
  * @param collectionName - The collection to scroll.
4898
4994
  * @param filter - Optional Qdrant filter.
@@ -4900,32 +4996,14 @@ async function hybridSearch(client, collectionName, vector, queryText, limit, te
4900
4996
  * @yields Scrolled points.
4901
4997
  */
4902
4998
  async function* scrollCollection(client, collectionName, filter, limit = 100) {
4903
- let offset = undefined;
4904
- for (;;) {
4905
- const result = await client.scroll(collectionName, {
4906
- limit,
4907
- with_payload: true,
4908
- with_vector: false,
4909
- ...(filter ? { filter } : {}),
4910
- ...(offset !== undefined ? { offset } : {}),
4911
- });
4912
- for (const point of result.points) {
4913
- yield {
4914
- id: String(point.id),
4915
- payload: point.payload,
4916
- };
4999
+ let cursor;
5000
+ do {
5001
+ const page = await scrollPage(client, collectionName, filter, limit, cursor);
5002
+ for (const point of page.points) {
5003
+ yield point;
4917
5004
  }
4918
- const nextOffset = result.next_page_offset;
4919
- if (nextOffset === null || nextOffset === undefined) {
4920
- break;
4921
- }
4922
- if (typeof nextOffset === 'string' || typeof nextOffset === 'number') {
4923
- offset = nextOffset;
4924
- }
4925
- else {
4926
- break;
4927
- }
4928
- }
5005
+ cursor = page.nextCursor;
5006
+ } while (cursor !== undefined);
4929
5007
  }
4930
5008
 
4931
5009
  /**
@@ -4969,6 +5047,15 @@ class VectorStoreClient {
4969
5047
  checkCompatibility: false,
4970
5048
  });
4971
5049
  }
5050
+ /**
5051
+ * Count points matching a filter.
5052
+ *
5053
+ * @param filter - Optional Qdrant filter.
5054
+ * @returns The number of matching points.
5055
+ */
5056
+ async count(filter) {
5057
+ return countPoints(this.client, this.collectionName, filter);
5058
+ }
4972
5059
  /**
4973
5060
  * Ensure the collection exists with correct dimensions and Cosine distance.
4974
5061
  */
@@ -5149,6 +5236,18 @@ class VectorStoreClient {
5149
5236
  async hybridSearch(vector, queryText, limit, textWeight, filter) {
5150
5237
  return hybridSearch(this.client, this.collectionName, vector, queryText, limit, textWeight, filter);
5151
5238
  }
5239
+ /**
5240
+ * Scroll one page of points matching a filter.
5241
+ *
5242
+ * @param filter - Optional Qdrant filter.
5243
+ * @param limit - Page size.
5244
+ * @param offset - Cursor offset from previous page.
5245
+ * @param fields - Optional field projection.
5246
+ * @returns Page of points and next cursor.
5247
+ */
5248
+ async scrollPage(filter, limit = 100, offset, fields) {
5249
+ return scrollPage(this.client, this.collectionName, filter, limit, offset, fields);
5250
+ }
5152
5251
  /**
5153
5252
  * Scroll through all points matching a filter.
5154
5253
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karmaniverous/jeeves-watcher",
3
- "version": "0.8.5",
3
+ "version": "0.9.0-0",
4
4
  "author": "Jason Williscroft",
5
5
  "description": "Filesystem watcher that keeps a Qdrant vector store in sync with document changes",
6
6
  "license": "BSD-3-Clause",