@karmaniverous/jeeves-watcher 0.2.1 → 0.2.3

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/dist/cjs/index.js CHANGED
@@ -40,6 +40,31 @@ function _interopNamespaceDefault(e) {
40
40
 
41
41
  var cheerio__namespace = /*#__PURE__*/_interopNamespaceDefault(cheerio);
42
42
 
43
+ /**
44
+ * @module util/normalizeError
45
+ *
46
+ * Normalizes unknown thrown values into proper Error objects for pino serialization.
47
+ */
48
+ /**
49
+ * Convert an unknown thrown value into a proper Error with message, stack, and cause.
50
+ * Pino's built-in `err` serializer requires an Error instance to extract message/stack.
51
+ *
52
+ * @param error - The caught value (may not be an Error).
53
+ * @returns A proper Error instance.
54
+ */
55
+ function normalizeError(error) {
56
+ if (error instanceof Error)
57
+ return error;
58
+ if (typeof error === 'string')
59
+ return new Error(error);
60
+ const message = typeof error === 'object' && error !== null && 'message' in error
61
+ ? String(error.message)
62
+ : String(error);
63
+ const normalized = new Error(message);
64
+ normalized.cause = error;
65
+ return normalized;
66
+ }
67
+
43
68
  /**
44
69
  * Best-effort base directory inference for a glob pattern.
45
70
  *
@@ -162,13 +187,13 @@ function createConfigReindexHandler(deps) {
162
187
  }
163
188
  }
164
189
  catch (error) {
165
- deps.logger.error({ error, scope }, 'Config reindex failed');
190
+ deps.logger.error({ err: normalizeError(error), scope }, 'Config reindex failed');
166
191
  }
167
192
  })();
168
193
  return await reply.status(200).send({ status: 'started', scope });
169
194
  }
170
195
  catch (error) {
171
- deps.logger.error({ error }, 'Config reindex request failed');
196
+ deps.logger.error({ err: normalizeError(error) }, 'Config reindex request failed');
172
197
  return await reply.status(500).send({ error: 'Internal server error' });
173
198
  }
174
199
  };
@@ -191,7 +216,7 @@ function createMetadataHandler(deps) {
191
216
  return { ok: true };
192
217
  }
193
218
  catch (error) {
194
- deps.logger.error({ error }, 'Metadata update failed');
219
+ deps.logger.error({ err: normalizeError(error) }, 'Metadata update failed');
195
220
  return reply.status(500).send({ error: 'Internal server error' });
196
221
  }
197
222
  };
@@ -314,7 +339,7 @@ function createRebuildMetadataHandler(deps) {
314
339
  return await reply.status(200).send({ ok: true });
315
340
  }
316
341
  catch (error) {
317
- deps.logger.error({ error }, 'Rebuild metadata failed');
342
+ deps.logger.error({ err: normalizeError(error) }, 'Rebuild metadata failed');
318
343
  return await reply.status(500).send({ error: 'Internal server error' });
319
344
  }
320
345
  };
@@ -336,7 +361,7 @@ function createReindexHandler(deps) {
336
361
  return await reply.status(200).send({ ok: true, filesIndexed: count });
337
362
  }
338
363
  catch (error) {
339
- deps.logger.error({ error }, 'Reindex failed');
364
+ deps.logger.error({ err: normalizeError(error) }, 'Reindex failed');
340
365
  return await reply.status(500).send({ error: 'Internal server error' });
341
366
  }
342
367
  };
@@ -360,7 +385,7 @@ function createSearchHandler(deps) {
360
385
  return results;
361
386
  }
362
387
  catch (error) {
363
- deps.logger.error({ error }, 'Search failed');
388
+ deps.logger.error({ err: normalizeError(error) }, 'Search failed');
364
389
  return reply.status(500).send({ error: 'Internal server error' });
365
390
  }
366
391
  };
@@ -642,6 +667,49 @@ const jeevesWatcherConfigSchema = zod.z.object({
642
667
  .describe('Timeout in milliseconds for graceful shutdown.'),
643
668
  });
644
669
 
670
+ /**
671
+ * @module config/substituteEnvVars
672
+ *
673
+ * Deep-walks config objects and replaces `${VAR_NAME}` patterns with environment variable values.
674
+ */
675
+ const ENV_PATTERN = /\$\{([^}]+)\}/g;
676
+ /**
677
+ * Replace `${VAR_NAME}` patterns in a string with `process.env.VAR_NAME`.
678
+ *
679
+ * @param value - The string to process.
680
+ * @returns The string with resolved env vars; unresolvable expressions left untouched.
681
+ */
682
+ function substituteString(value) {
683
+ return value.replace(ENV_PATTERN, (match, varName) => {
684
+ const envValue = process.env[varName];
685
+ if (envValue === undefined)
686
+ return match;
687
+ return envValue;
688
+ });
689
+ }
690
+ /**
691
+ * Deep-walk a value and substitute `${VAR_NAME}` patterns in all string values.
692
+ *
693
+ * @param value - The value to walk (object, array, or primitive).
694
+ * @returns A new value with all env var references resolved.
695
+ */
696
+ function substituteEnvVars(value) {
697
+ if (typeof value === 'string') {
698
+ return substituteString(value);
699
+ }
700
+ if (Array.isArray(value)) {
701
+ return value.map((item) => substituteEnvVars(item));
702
+ }
703
+ if (value !== null && typeof value === 'object') {
704
+ const result = {};
705
+ for (const [key, val] of Object.entries(value)) {
706
+ result[key] = substituteEnvVars(val);
707
+ }
708
+ return result;
709
+ }
710
+ return value;
711
+ }
712
+
645
713
  const MODULE_NAME = 'jeeves-watcher';
646
714
  /**
647
715
  * Merge sensible defaults into a loaded configuration.
@@ -677,7 +745,8 @@ async function loadConfig(configPath) {
677
745
  }
678
746
  try {
679
747
  const validated = jeevesWatcherConfigSchema.parse(result.config);
680
- return applyDefaults(validated);
748
+ const withDefaults = applyDefaults(validated);
749
+ return substituteEnvVars(withDefaults);
681
750
  }
682
751
  catch (error) {
683
752
  if (error instanceof zod.ZodError) {
@@ -844,7 +913,7 @@ function createGeminiProvider(config, logger) {
844
913
  delayMs,
845
914
  provider: 'gemini',
846
915
  model: config.model,
847
- error,
916
+ err: normalizeError(error),
848
917
  }, 'Embedding call failed; will retry');
849
918
  },
850
919
  });
@@ -953,6 +1022,9 @@ function pointId(filePath, chunkIndex) {
953
1022
  */
954
1023
  function extractMarkdownFrontmatter(markdown) {
955
1024
  const trimmed = markdown.replace(/^\uFEFF/, '');
1025
+ // Only attempt frontmatter parsing if the file starts with ---
1026
+ if (!/^\s*---/.test(trimmed))
1027
+ return { body: markdown };
956
1028
  const match = /^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/m.exec(trimmed);
957
1029
  if (!match)
958
1030
  return { body: markdown };
@@ -1410,7 +1482,7 @@ class DocumentProcessor {
1410
1482
  this.logger.info({ filePath, chunks: chunks.length }, 'File processed successfully');
1411
1483
  }
1412
1484
  catch (error) {
1413
- this.logger.error({ filePath, error }, 'Failed to process file');
1485
+ this.logger.error({ filePath, err: normalizeError(error) }, 'Failed to process file');
1414
1486
  }
1415
1487
  }
1416
1488
  /**
@@ -1430,7 +1502,7 @@ class DocumentProcessor {
1430
1502
  this.logger.info({ filePath }, 'File deleted from index');
1431
1503
  }
1432
1504
  catch (error) {
1433
- this.logger.error({ filePath, error }, 'Failed to delete file');
1505
+ this.logger.error({ filePath, err: normalizeError(error) }, 'Failed to delete file');
1434
1506
  }
1435
1507
  }
1436
1508
  /**
@@ -1458,7 +1530,7 @@ class DocumentProcessor {
1458
1530
  return merged;
1459
1531
  }
1460
1532
  catch (error) {
1461
- this.logger.error({ filePath, error }, 'Failed to update metadata');
1533
+ this.logger.error({ filePath, err: normalizeError(error) }, 'Failed to update metadata');
1462
1534
  return null;
1463
1535
  }
1464
1536
  }
@@ -1488,7 +1560,7 @@ class DocumentProcessor {
1488
1560
  return metadata;
1489
1561
  }
1490
1562
  catch (error) {
1491
- this.logger.error({ filePath, error }, 'Failed to re-apply rules');
1563
+ this.logger.error({ filePath, err: normalizeError(error) }, 'Failed to re-apply rules');
1492
1564
  return null;
1493
1565
  }
1494
1566
  }
@@ -1716,7 +1788,12 @@ class VectorStoreClient {
1716
1788
  maxDelayMs: 10_000,
1717
1789
  jitter: 0.2,
1718
1790
  onRetry: ({ attempt, delayMs, error }) => {
1719
- this.log.warn({ attempt, delayMs, operation: 'qdrant.upsert', error }, 'Qdrant upsert failed; will retry');
1791
+ this.log.warn({
1792
+ attempt,
1793
+ delayMs,
1794
+ operation: 'qdrant.upsert',
1795
+ err: normalizeError(error),
1796
+ }, 'Qdrant upsert failed; will retry');
1720
1797
  },
1721
1798
  });
1722
1799
  }
@@ -1742,7 +1819,12 @@ class VectorStoreClient {
1742
1819
  maxDelayMs: 10_000,
1743
1820
  jitter: 0.2,
1744
1821
  onRetry: ({ attempt, delayMs, error }) => {
1745
- this.log.warn({ attempt, delayMs, operation: 'qdrant.delete', error }, 'Qdrant delete failed; will retry');
1822
+ this.log.warn({
1823
+ attempt,
1824
+ delayMs,
1825
+ operation: 'qdrant.delete',
1826
+ err: normalizeError(error),
1827
+ }, 'Qdrant delete failed; will retry');
1746
1828
  },
1747
1829
  });
1748
1830
  }
@@ -1896,7 +1978,7 @@ class FileSystemWatcher {
1896
1978
  this.queue.enqueue({ type: 'delete', path, priority: 'normal' }, () => this.processor.deleteFile(path));
1897
1979
  });
1898
1980
  this.watcher.on('error', (error) => {
1899
- this.logger.error({ error }, 'Watcher error');
1981
+ this.logger.error({ err: normalizeError(error) }, 'Watcher error');
1900
1982
  });
1901
1983
  this.queue.process();
1902
1984
  this.logger.info({ paths: this.config.paths }, 'Filesystem watcher started');
@@ -1941,7 +2023,7 @@ class ConfigWatcher {
1941
2023
  }, this.options.debounceMs);
1942
2024
  });
1943
2025
  this.watcher.on('error', (error) => {
1944
- this.options.logger.error({ error }, 'Config watcher error');
2026
+ this.options.logger.error({ err: normalizeError(error) }, 'Config watcher error');
1945
2027
  });
1946
2028
  this.options.logger.info({
1947
2029
  configPath: this.options.configPath,
@@ -2025,7 +2107,7 @@ class JeevesWatcher {
2025
2107
  embeddingProvider = this.factories.createEmbeddingProvider(this.config.embedding, logger);
2026
2108
  }
2027
2109
  catch (error) {
2028
- logger.fatal({ error }, 'Failed to create embedding provider');
2110
+ logger.fatal({ err: normalizeError(error) }, 'Failed to create embedding provider');
2029
2111
  throw error;
2030
2112
  }
2031
2113
  const vectorStore = this.factories.createVectorStoreClient(this.config.vectorStore, embeddingProvider.dimensions, logger);
@@ -2132,7 +2214,7 @@ class JeevesWatcher {
2132
2214
  logger.info({ configPath: this.configPath, rules: compiledRules.length }, 'Config reloaded');
2133
2215
  }
2134
2216
  catch (error) {
2135
- logger.error({ error }, 'Failed to reload config');
2217
+ logger.error({ err: normalizeError(error) }, 'Failed to reload config');
2136
2218
  }
2137
2219
  }
2138
2220
  }
@@ -22,6 +22,31 @@ import { MarkdownTextSplitter, RecursiveCharacterTextSplitter } from '@langchain
22
22
  import { QdrantClient } from '@qdrant/js-client-rest';
23
23
  import chokidar from 'chokidar';
24
24
 
25
+ /**
26
+ * @module util/normalizeError
27
+ *
28
+ * Normalizes unknown thrown values into proper Error objects for pino serialization.
29
+ */
30
+ /**
31
+ * Convert an unknown thrown value into a proper Error with message, stack, and cause.
32
+ * Pino's built-in `err` serializer requires an Error instance to extract message/stack.
33
+ *
34
+ * @param error - The caught value (may not be an Error).
35
+ * @returns A proper Error instance.
36
+ */
37
+ function normalizeError(error) {
38
+ if (error instanceof Error)
39
+ return error;
40
+ if (typeof error === 'string')
41
+ return new Error(error);
42
+ const message = typeof error === 'object' && error !== null && 'message' in error
43
+ ? String(error.message)
44
+ : String(error);
45
+ const normalized = new Error(message);
46
+ normalized.cause = error;
47
+ return normalized;
48
+ }
49
+
25
50
  /**
26
51
  * Best-effort base directory inference for a glob pattern.
27
52
  *
@@ -144,13 +169,13 @@ function createConfigReindexHandler(deps) {
144
169
  }
145
170
  }
146
171
  catch (error) {
147
- deps.logger.error({ error, scope }, 'Config reindex failed');
172
+ deps.logger.error({ err: normalizeError(error), scope }, 'Config reindex failed');
148
173
  }
149
174
  })();
150
175
  return await reply.status(200).send({ status: 'started', scope });
151
176
  }
152
177
  catch (error) {
153
- deps.logger.error({ error }, 'Config reindex request failed');
178
+ deps.logger.error({ err: normalizeError(error) }, 'Config reindex request failed');
154
179
  return await reply.status(500).send({ error: 'Internal server error' });
155
180
  }
156
181
  };
@@ -173,7 +198,7 @@ function createMetadataHandler(deps) {
173
198
  return { ok: true };
174
199
  }
175
200
  catch (error) {
176
- deps.logger.error({ error }, 'Metadata update failed');
201
+ deps.logger.error({ err: normalizeError(error) }, 'Metadata update failed');
177
202
  return reply.status(500).send({ error: 'Internal server error' });
178
203
  }
179
204
  };
@@ -296,7 +321,7 @@ function createRebuildMetadataHandler(deps) {
296
321
  return await reply.status(200).send({ ok: true });
297
322
  }
298
323
  catch (error) {
299
- deps.logger.error({ error }, 'Rebuild metadata failed');
324
+ deps.logger.error({ err: normalizeError(error) }, 'Rebuild metadata failed');
300
325
  return await reply.status(500).send({ error: 'Internal server error' });
301
326
  }
302
327
  };
@@ -318,7 +343,7 @@ function createReindexHandler(deps) {
318
343
  return await reply.status(200).send({ ok: true, filesIndexed: count });
319
344
  }
320
345
  catch (error) {
321
- deps.logger.error({ error }, 'Reindex failed');
346
+ deps.logger.error({ err: normalizeError(error) }, 'Reindex failed');
322
347
  return await reply.status(500).send({ error: 'Internal server error' });
323
348
  }
324
349
  };
@@ -342,7 +367,7 @@ function createSearchHandler(deps) {
342
367
  return results;
343
368
  }
344
369
  catch (error) {
345
- deps.logger.error({ error }, 'Search failed');
370
+ deps.logger.error({ err: normalizeError(error) }, 'Search failed');
346
371
  return reply.status(500).send({ error: 'Internal server error' });
347
372
  }
348
373
  };
@@ -645,6 +670,49 @@ const jeevesWatcherConfigSchema = z.object({
645
670
  .describe('Timeout in milliseconds for graceful shutdown.'),
646
671
  });
647
672
 
673
+ /**
674
+ * @module config/substituteEnvVars
675
+ *
676
+ * Deep-walks config objects and replaces `${VAR_NAME}` patterns with environment variable values.
677
+ */
678
+ const ENV_PATTERN = /\$\{([^}]+)\}/g;
679
+ /**
680
+ * Replace `${VAR_NAME}` patterns in a string with `process.env.VAR_NAME`.
681
+ *
682
+ * @param value - The string to process.
683
+ * @returns The string with resolved env vars; unresolvable expressions left untouched.
684
+ */
685
+ function substituteString(value) {
686
+ return value.replace(ENV_PATTERN, (match, varName) => {
687
+ const envValue = process.env[varName];
688
+ if (envValue === undefined)
689
+ return match;
690
+ return envValue;
691
+ });
692
+ }
693
+ /**
694
+ * Deep-walk a value and substitute `${VAR_NAME}` patterns in all string values.
695
+ *
696
+ * @param value - The value to walk (object, array, or primitive).
697
+ * @returns A new value with all env var references resolved.
698
+ */
699
+ function substituteEnvVars(value) {
700
+ if (typeof value === 'string') {
701
+ return substituteString(value);
702
+ }
703
+ if (Array.isArray(value)) {
704
+ return value.map((item) => substituteEnvVars(item));
705
+ }
706
+ if (value !== null && typeof value === 'object') {
707
+ const result = {};
708
+ for (const [key, val] of Object.entries(value)) {
709
+ result[key] = substituteEnvVars(val);
710
+ }
711
+ return result;
712
+ }
713
+ return value;
714
+ }
715
+
648
716
  const MODULE_NAME = 'jeeves-watcher';
649
717
  /**
650
718
  * Merge sensible defaults into a loaded configuration.
@@ -680,7 +748,8 @@ async function loadConfig(configPath) {
680
748
  }
681
749
  try {
682
750
  const validated = jeevesWatcherConfigSchema.parse(result.config);
683
- return applyDefaults(validated);
751
+ const withDefaults = applyDefaults(validated);
752
+ return substituteEnvVars(withDefaults);
684
753
  }
685
754
  catch (error) {
686
755
  if (error instanceof ZodError) {
@@ -847,7 +916,7 @@ function createGeminiProvider(config, logger) {
847
916
  delayMs,
848
917
  provider: 'gemini',
849
918
  model: config.model,
850
- error,
919
+ err: normalizeError(error),
851
920
  }, 'Embedding call failed; will retry');
852
921
  },
853
922
  });
@@ -956,6 +1025,9 @@ function pointId(filePath, chunkIndex) {
956
1025
  */
957
1026
  function extractMarkdownFrontmatter(markdown) {
958
1027
  const trimmed = markdown.replace(/^\uFEFF/, '');
1028
+ // Only attempt frontmatter parsing if the file starts with ---
1029
+ if (!/^\s*---/.test(trimmed))
1030
+ return { body: markdown };
959
1031
  const match = /^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/m.exec(trimmed);
960
1032
  if (!match)
961
1033
  return { body: markdown };
@@ -1413,7 +1485,7 @@ class DocumentProcessor {
1413
1485
  this.logger.info({ filePath, chunks: chunks.length }, 'File processed successfully');
1414
1486
  }
1415
1487
  catch (error) {
1416
- this.logger.error({ filePath, error }, 'Failed to process file');
1488
+ this.logger.error({ filePath, err: normalizeError(error) }, 'Failed to process file');
1417
1489
  }
1418
1490
  }
1419
1491
  /**
@@ -1433,7 +1505,7 @@ class DocumentProcessor {
1433
1505
  this.logger.info({ filePath }, 'File deleted from index');
1434
1506
  }
1435
1507
  catch (error) {
1436
- this.logger.error({ filePath, error }, 'Failed to delete file');
1508
+ this.logger.error({ filePath, err: normalizeError(error) }, 'Failed to delete file');
1437
1509
  }
1438
1510
  }
1439
1511
  /**
@@ -1461,7 +1533,7 @@ class DocumentProcessor {
1461
1533
  return merged;
1462
1534
  }
1463
1535
  catch (error) {
1464
- this.logger.error({ filePath, error }, 'Failed to update metadata');
1536
+ this.logger.error({ filePath, err: normalizeError(error) }, 'Failed to update metadata');
1465
1537
  return null;
1466
1538
  }
1467
1539
  }
@@ -1491,7 +1563,7 @@ class DocumentProcessor {
1491
1563
  return metadata;
1492
1564
  }
1493
1565
  catch (error) {
1494
- this.logger.error({ filePath, error }, 'Failed to re-apply rules');
1566
+ this.logger.error({ filePath, err: normalizeError(error) }, 'Failed to re-apply rules');
1495
1567
  return null;
1496
1568
  }
1497
1569
  }
@@ -1719,7 +1791,12 @@ class VectorStoreClient {
1719
1791
  maxDelayMs: 10_000,
1720
1792
  jitter: 0.2,
1721
1793
  onRetry: ({ attempt, delayMs, error }) => {
1722
- this.log.warn({ attempt, delayMs, operation: 'qdrant.upsert', error }, 'Qdrant upsert failed; will retry');
1794
+ this.log.warn({
1795
+ attempt,
1796
+ delayMs,
1797
+ operation: 'qdrant.upsert',
1798
+ err: normalizeError(error),
1799
+ }, 'Qdrant upsert failed; will retry');
1723
1800
  },
1724
1801
  });
1725
1802
  }
@@ -1745,7 +1822,12 @@ class VectorStoreClient {
1745
1822
  maxDelayMs: 10_000,
1746
1823
  jitter: 0.2,
1747
1824
  onRetry: ({ attempt, delayMs, error }) => {
1748
- this.log.warn({ attempt, delayMs, operation: 'qdrant.delete', error }, 'Qdrant delete failed; will retry');
1825
+ this.log.warn({
1826
+ attempt,
1827
+ delayMs,
1828
+ operation: 'qdrant.delete',
1829
+ err: normalizeError(error),
1830
+ }, 'Qdrant delete failed; will retry');
1749
1831
  },
1750
1832
  });
1751
1833
  }
@@ -1899,7 +1981,7 @@ class FileSystemWatcher {
1899
1981
  this.queue.enqueue({ type: 'delete', path, priority: 'normal' }, () => this.processor.deleteFile(path));
1900
1982
  });
1901
1983
  this.watcher.on('error', (error) => {
1902
- this.logger.error({ error }, 'Watcher error');
1984
+ this.logger.error({ err: normalizeError(error) }, 'Watcher error');
1903
1985
  });
1904
1986
  this.queue.process();
1905
1987
  this.logger.info({ paths: this.config.paths }, 'Filesystem watcher started');
@@ -1944,7 +2026,7 @@ class ConfigWatcher {
1944
2026
  }, this.options.debounceMs);
1945
2027
  });
1946
2028
  this.watcher.on('error', (error) => {
1947
- this.options.logger.error({ error }, 'Config watcher error');
2029
+ this.options.logger.error({ err: normalizeError(error) }, 'Config watcher error');
1948
2030
  });
1949
2031
  this.options.logger.info({
1950
2032
  configPath: this.options.configPath,
@@ -2028,7 +2110,7 @@ class JeevesWatcher {
2028
2110
  embeddingProvider = this.factories.createEmbeddingProvider(this.config.embedding, logger);
2029
2111
  }
2030
2112
  catch (error) {
2031
- logger.fatal({ error }, 'Failed to create embedding provider');
2113
+ logger.fatal({ err: normalizeError(error) }, 'Failed to create embedding provider');
2032
2114
  throw error;
2033
2115
  }
2034
2116
  const vectorStore = this.factories.createVectorStoreClient(this.config.vectorStore, embeddingProvider.dimensions, logger);
@@ -2135,7 +2217,7 @@ class JeevesWatcher {
2135
2217
  logger.info({ configPath: this.configPath, rules: compiledRules.length }, 'Config reloaded');
2136
2218
  }
2137
2219
  catch (error) {
2138
- logger.error({ error }, 'Failed to reload config');
2220
+ logger.error({ err: normalizeError(error) }, 'Failed to reload config');
2139
2221
  }
2140
2222
  }
2141
2223
  }
@@ -20,6 +20,31 @@
20
20
 
21
21
  var cheerio__namespace = /*#__PURE__*/_interopNamespaceDefault(cheerio);
22
22
 
23
+ /**
24
+ * @module util/normalizeError
25
+ *
26
+ * Normalizes unknown thrown values into proper Error objects for pino serialization.
27
+ */
28
+ /**
29
+ * Convert an unknown thrown value into a proper Error with message, stack, and cause.
30
+ * Pino's built-in `err` serializer requires an Error instance to extract message/stack.
31
+ *
32
+ * @param error - The caught value (may not be an Error).
33
+ * @returns A proper Error instance.
34
+ */
35
+ function normalizeError(error) {
36
+ if (error instanceof Error)
37
+ return error;
38
+ if (typeof error === 'string')
39
+ return new Error(error);
40
+ const message = typeof error === 'object' && error !== null && 'message' in error
41
+ ? String(error.message)
42
+ : String(error);
43
+ const normalized = new Error(message);
44
+ normalized.cause = error;
45
+ return normalized;
46
+ }
47
+
23
48
  /**
24
49
  * Best-effort base directory inference for a glob pattern.
25
50
  *
@@ -142,13 +167,13 @@
142
167
  }
143
168
  }
144
169
  catch (error) {
145
- deps.logger.error({ error, scope }, 'Config reindex failed');
170
+ deps.logger.error({ err: normalizeError(error), scope }, 'Config reindex failed');
146
171
  }
147
172
  })();
148
173
  return await reply.status(200).send({ status: 'started', scope });
149
174
  }
150
175
  catch (error) {
151
- deps.logger.error({ error }, 'Config reindex request failed');
176
+ deps.logger.error({ err: normalizeError(error) }, 'Config reindex request failed');
152
177
  return await reply.status(500).send({ error: 'Internal server error' });
153
178
  }
154
179
  };
@@ -171,7 +196,7 @@
171
196
  return { ok: true };
172
197
  }
173
198
  catch (error) {
174
- deps.logger.error({ error }, 'Metadata update failed');
199
+ deps.logger.error({ err: normalizeError(error) }, 'Metadata update failed');
175
200
  return reply.status(500).send({ error: 'Internal server error' });
176
201
  }
177
202
  };
@@ -294,7 +319,7 @@
294
319
  return await reply.status(200).send({ ok: true });
295
320
  }
296
321
  catch (error) {
297
- deps.logger.error({ error }, 'Rebuild metadata failed');
322
+ deps.logger.error({ err: normalizeError(error) }, 'Rebuild metadata failed');
298
323
  return await reply.status(500).send({ error: 'Internal server error' });
299
324
  }
300
325
  };
@@ -316,7 +341,7 @@
316
341
  return await reply.status(200).send({ ok: true, filesIndexed: count });
317
342
  }
318
343
  catch (error) {
319
- deps.logger.error({ error }, 'Reindex failed');
344
+ deps.logger.error({ err: normalizeError(error) }, 'Reindex failed');
320
345
  return await reply.status(500).send({ error: 'Internal server error' });
321
346
  }
322
347
  };
@@ -340,7 +365,7 @@
340
365
  return results;
341
366
  }
342
367
  catch (error) {
343
- deps.logger.error({ error }, 'Search failed');
368
+ deps.logger.error({ err: normalizeError(error) }, 'Search failed');
344
369
  return reply.status(500).send({ error: 'Internal server error' });
345
370
  }
346
371
  };
@@ -622,6 +647,49 @@
622
647
  .describe('Timeout in milliseconds for graceful shutdown.'),
623
648
  });
624
649
 
650
+ /**
651
+ * @module config/substituteEnvVars
652
+ *
653
+ * Deep-walks config objects and replaces `${VAR_NAME}` patterns with environment variable values.
654
+ */
655
+ const ENV_PATTERN = /\$\{([^}]+)\}/g;
656
+ /**
657
+ * Replace `${VAR_NAME}` patterns in a string with `process.env.VAR_NAME`.
658
+ *
659
+ * @param value - The string to process.
660
+ * @returns The string with resolved env vars; unresolvable expressions left untouched.
661
+ */
662
+ function substituteString(value) {
663
+ return value.replace(ENV_PATTERN, (match, varName) => {
664
+ const envValue = process.env[varName];
665
+ if (envValue === undefined)
666
+ return match;
667
+ return envValue;
668
+ });
669
+ }
670
+ /**
671
+ * Deep-walk a value and substitute `${VAR_NAME}` patterns in all string values.
672
+ *
673
+ * @param value - The value to walk (object, array, or primitive).
674
+ * @returns A new value with all env var references resolved.
675
+ */
676
+ function substituteEnvVars(value) {
677
+ if (typeof value === 'string') {
678
+ return substituteString(value);
679
+ }
680
+ if (Array.isArray(value)) {
681
+ return value.map((item) => substituteEnvVars(item));
682
+ }
683
+ if (value !== null && typeof value === 'object') {
684
+ const result = {};
685
+ for (const [key, val] of Object.entries(value)) {
686
+ result[key] = substituteEnvVars(val);
687
+ }
688
+ return result;
689
+ }
690
+ return value;
691
+ }
692
+
625
693
  const MODULE_NAME = 'jeeves-watcher';
626
694
  /**
627
695
  * Merge sensible defaults into a loaded configuration.
@@ -657,7 +725,8 @@
657
725
  }
658
726
  try {
659
727
  const validated = jeevesWatcherConfigSchema.parse(result.config);
660
- return applyDefaults(validated);
728
+ const withDefaults = applyDefaults(validated);
729
+ return substituteEnvVars(withDefaults);
661
730
  }
662
731
  catch (error) {
663
732
  if (error instanceof zod.ZodError) {
@@ -824,7 +893,7 @@
824
893
  delayMs,
825
894
  provider: 'gemini',
826
895
  model: config.model,
827
- error,
896
+ err: normalizeError(error),
828
897
  }, 'Embedding call failed; will retry');
829
898
  },
830
899
  });
@@ -933,6 +1002,9 @@
933
1002
  */
934
1003
  function extractMarkdownFrontmatter(markdown) {
935
1004
  const trimmed = markdown.replace(/^\uFEFF/, '');
1005
+ // Only attempt frontmatter parsing if the file starts with ---
1006
+ if (!/^\s*---/.test(trimmed))
1007
+ return { body: markdown };
936
1008
  const match = /^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/m.exec(trimmed);
937
1009
  if (!match)
938
1010
  return { body: markdown };
@@ -1390,7 +1462,7 @@
1390
1462
  this.logger.info({ filePath, chunks: chunks.length }, 'File processed successfully');
1391
1463
  }
1392
1464
  catch (error) {
1393
- this.logger.error({ filePath, error }, 'Failed to process file');
1465
+ this.logger.error({ filePath, err: normalizeError(error) }, 'Failed to process file');
1394
1466
  }
1395
1467
  }
1396
1468
  /**
@@ -1410,7 +1482,7 @@
1410
1482
  this.logger.info({ filePath }, 'File deleted from index');
1411
1483
  }
1412
1484
  catch (error) {
1413
- this.logger.error({ filePath, error }, 'Failed to delete file');
1485
+ this.logger.error({ filePath, err: normalizeError(error) }, 'Failed to delete file');
1414
1486
  }
1415
1487
  }
1416
1488
  /**
@@ -1438,7 +1510,7 @@
1438
1510
  return merged;
1439
1511
  }
1440
1512
  catch (error) {
1441
- this.logger.error({ filePath, error }, 'Failed to update metadata');
1513
+ this.logger.error({ filePath, err: normalizeError(error) }, 'Failed to update metadata');
1442
1514
  return null;
1443
1515
  }
1444
1516
  }
@@ -1468,7 +1540,7 @@
1468
1540
  return metadata;
1469
1541
  }
1470
1542
  catch (error) {
1471
- this.logger.error({ filePath, error }, 'Failed to re-apply rules');
1543
+ this.logger.error({ filePath, err: normalizeError(error) }, 'Failed to re-apply rules');
1472
1544
  return null;
1473
1545
  }
1474
1546
  }
@@ -1696,7 +1768,12 @@
1696
1768
  maxDelayMs: 10_000,
1697
1769
  jitter: 0.2,
1698
1770
  onRetry: ({ attempt, delayMs, error }) => {
1699
- this.log.warn({ attempt, delayMs, operation: 'qdrant.upsert', error }, 'Qdrant upsert failed; will retry');
1771
+ this.log.warn({
1772
+ attempt,
1773
+ delayMs,
1774
+ operation: 'qdrant.upsert',
1775
+ err: normalizeError(error),
1776
+ }, 'Qdrant upsert failed; will retry');
1700
1777
  },
1701
1778
  });
1702
1779
  }
@@ -1722,7 +1799,12 @@
1722
1799
  maxDelayMs: 10_000,
1723
1800
  jitter: 0.2,
1724
1801
  onRetry: ({ attempt, delayMs, error }) => {
1725
- this.log.warn({ attempt, delayMs, operation: 'qdrant.delete', error }, 'Qdrant delete failed; will retry');
1802
+ this.log.warn({
1803
+ attempt,
1804
+ delayMs,
1805
+ operation: 'qdrant.delete',
1806
+ err: normalizeError(error),
1807
+ }, 'Qdrant delete failed; will retry');
1726
1808
  },
1727
1809
  });
1728
1810
  }
@@ -1876,7 +1958,7 @@
1876
1958
  this.queue.enqueue({ type: 'delete', path, priority: 'normal' }, () => this.processor.deleteFile(path));
1877
1959
  });
1878
1960
  this.watcher.on('error', (error) => {
1879
- this.logger.error({ error }, 'Watcher error');
1961
+ this.logger.error({ err: normalizeError(error) }, 'Watcher error');
1880
1962
  });
1881
1963
  this.queue.process();
1882
1964
  this.logger.info({ paths: this.config.paths }, 'Filesystem watcher started');
@@ -1921,7 +2003,7 @@
1921
2003
  }, this.options.debounceMs);
1922
2004
  });
1923
2005
  this.watcher.on('error', (error) => {
1924
- this.options.logger.error({ error }, 'Config watcher error');
2006
+ this.options.logger.error({ err: normalizeError(error) }, 'Config watcher error');
1925
2007
  });
1926
2008
  this.options.logger.info({
1927
2009
  configPath: this.options.configPath,
@@ -2005,7 +2087,7 @@
2005
2087
  embeddingProvider = this.factories.createEmbeddingProvider(this.config.embedding, logger);
2006
2088
  }
2007
2089
  catch (error) {
2008
- logger.fatal({ error }, 'Failed to create embedding provider');
2090
+ logger.fatal({ err: normalizeError(error) }, 'Failed to create embedding provider');
2009
2091
  throw error;
2010
2092
  }
2011
2093
  const vectorStore = this.factories.createVectorStoreClient(this.config.vectorStore, embeddingProvider.dimensions, logger);
@@ -2112,7 +2194,7 @@
2112
2194
  logger.info({ configPath: this.configPath, rules: compiledRules.length }, 'Config reloaded');
2113
2195
  }
2114
2196
  catch (error) {
2115
- logger.error({ error }, 'Failed to reload config');
2197
+ logger.error({ err: normalizeError(error) }, 'Failed to reload config');
2116
2198
  }
2117
2199
  }
2118
2200
  }
@@ -1 +1 @@
1
- !function(e,t,i,r,o,n,s,a,c,l,d,h,u,g,f,p,m,y,w,b,v){"use strict";function M(e){var t=Object.create(null);return e&&Object.keys(e).forEach((function(i){if("default"!==i){var r=Object.getOwnPropertyDescriptor(e,i);Object.defineProperty(t,i,r.get?r:{enumerable:!0,get:function(){return e[i]}})}})),t.default=e,Object.freeze(t)}var P=M(g);function S(e){const t=e.replace(/\\/g,"/"),i=t.search(/[*?\[]/);if(-1===i)return r.resolve(e);const o=t.slice(0,i),n=o.endsWith("/")?o.slice(0,-1):r.dirname(o);return r.resolve(n)}async function*k(e){let t;try{t=(await i.readdir(e,{withFileTypes:!0})).map((e=>({name:e.name,isDirectory:e.isDirectory()})))}catch{return}for(const o of t){const t=r.resolve(e,o.name);if(o.isDirectory)yield*k(t);else try{(await i.stat(t)).isFile()&&(yield t)}catch{}}}async function x(e,t,i,r){const n=await async function(e,t=[]){const i=e.map((e=>e.replace(/\\/g,"/"))),r=t.map((e=>e.replace(/\\/g,"/"))),n=o(i,{dot:!0}),s=r.length?o(r,{dot:!0}):()=>!1,a=Array.from(new Set(e.map(S))),c=new Set;for(const e of a)for await(const t of k(e)){const e=t.replace(/\\/g,"/");s(e)||n(e)&&c.add(t)}return Array.from(c)}(e,t);for(const e of n)await i[r](e);return n.length}function z(e,t=!1){let i=e.replace(/\\/g,"/").toLowerCase();return t&&(i=i.replace(/^([a-z]):/,((e,t)=>t))),i}function j(e,t){const i=z(e,!0),o=s.createHash("sha256").update(i,"utf8").digest("hex");return r.join(t,`${o}.meta.json`)}async function C(e,t){try{const r=await i.readFile(j(e,t),"utf8");return JSON.parse(r)}catch{return null}}async function F(e,t,o){const n=j(e,t);await i.mkdir(r.dirname(n),{recursive:!0}),await i.writeFile(n,JSON.stringify(o,null,2),"utf8")}async function R(e,t){try{await i.rm(j(e,t))}catch{}}const T=["file_path","chunk_index","total_chunks","content_hash","chunk_text"];function I(e){const{processor:i,vectorStore:r,embeddingProvider:o,logger:s,config:a}=e,c=t({logger:!1});var l;return c.get("/status",(()=>({status:"ok",uptime:process.uptime()}))),c.post("/metadata",(l={processor:i,logger:s},async(e,t)=>{try{const{path:t,metadata:i}=e.body;return await l.processor.processMetadataUpdate(t,i),{ok:!0}}catch(e){return l.logger.error({error:e},"Metadata update failed"),t.status(500).send({error:"Internal server error"})}})),c.post("/search",function(e){return async(t,i)=>{try{const{query:i,limit:r=10}=t.body,o=await e.embeddingProvider.embed([i]);return await e.vectorStore.search(o[0],r)}catch(t){return e.logger.error({error:t},"Search failed"),i.status(500).send({error:"Internal server error"})}}}({embeddingProvider:o,vectorStore:r,logger:s})),c.post("/reindex",function(e){return async(t,i)=>{try{const t=await x(e.config.watch.paths,e.config.watch.ignored,e.processor,"processFile");return await i.status(200).send({ok:!0,filesIndexed:t})}catch(t){return e.logger.error({error:t},"Reindex failed"),await i.status(500).send({error:"Internal server error"})}}}({config:a,processor:i,logger:s})),c.post("/rebuild-metadata",function(e){return async(t,i)=>{try{const t=e.config.metadataDir??".jeeves-metadata",r=[...T];for await(const i of e.vectorStore.scroll()){const e=i.payload,o=e.file_path;if("string"!=typeof o||0===o.length)continue;const s=n.omit(e,r);await F(o,t,s)}return await i.status(200).send({ok:!0})}catch(t){return e.logger.error({error:t},"Rebuild metadata failed"),await i.status(500).send({error:"Internal server error"})}}}({config:a,vectorStore:r,logger:s})),c.post("/config-reindex",function(e){return async(t,i)=>{try{const r=t.body.scope??"rules";return(async()=>{try{if("rules"===r){const t=await x(e.config.watch.paths,e.config.watch.ignored,e.processor,"processRulesUpdate");e.logger.info({scope:r,filesProcessed:t},"Config reindex (rules) completed")}else{const t=await x(e.config.watch.paths,e.config.watch.ignored,e.processor,"processFile");e.logger.info({scope:r,filesProcessed:t},"Config reindex (full) completed")}}catch(t){e.logger.error({error:t,scope:r},"Config reindex failed")}})(),await i.status(200).send({status:"started",scope:r})}catch(t){return e.logger.error({error:t},"Config reindex request failed"),await i.status(500).send({error:"Internal server error"})}}}({config:a,processor:i,logger:s})),c}const D={metadataDir:".jeeves-watcher",shutdownTimeoutMs:1e4},E={enabled:!0,debounceMs:1e3},W={host:"127.0.0.1",port:3456},N={level:"info"},q={debounceMs:300,stabilityThresholdMs:500,usePolling:!1,pollIntervalMs:1e3},A={chunkSize:1e3,chunkOverlap:200,dimensions:3072,rateLimitPerMinute:300,concurrency:5},_=c.z.object({paths:c.z.array(c.z.string()).min(1).describe('Glob patterns for files to watch (e.g., "**/*.md"). At least one required.'),ignored:c.z.array(c.z.string()).optional().describe('Glob patterns to exclude from watching (e.g., "**/node_modules/**").'),pollIntervalMs:c.z.number().optional().describe("Polling interval in milliseconds when usePolling is enabled."),usePolling:c.z.boolean().optional().describe("Use polling instead of native file system events (for network drives)."),debounceMs:c.z.number().optional().describe("Debounce delay in milliseconds for file change events."),stabilityThresholdMs:c.z.number().optional().describe("Time in milliseconds a file must remain unchanged before processing.")}),L=c.z.object({enabled:c.z.boolean().optional().describe("Enable automatic reloading when config file changes."),debounceMs:c.z.number().optional().describe("Debounce delay in milliseconds for config file change detection.")}),O=c.z.object({provider:c.z.string().default("gemini").describe('Embedding provider name (e.g., "gemini", "openai").'),model:c.z.string().default("gemini-embedding-001").describe('Embedding model identifier (e.g., "gemini-embedding-001", "text-embedding-3-small").'),chunkSize:c.z.number().optional().describe("Maximum chunk size in characters for text splitting."),chunkOverlap:c.z.number().optional().describe("Character overlap between consecutive chunks."),dimensions:c.z.number().optional().describe("Embedding vector dimensions (must match model output)."),apiKey:c.z.string().optional().describe("API key for embedding provider (supports ${ENV_VAR} substitution)."),rateLimitPerMinute:c.z.number().optional().describe("Maximum embedding API requests per minute (rate limiting)."),concurrency:c.z.number().optional().describe("Maximum concurrent embedding requests.")}),Q=c.z.object({url:c.z.string().describe('Qdrant server URL (e.g., "http://localhost:6333").'),collectionName:c.z.string().describe("Qdrant collection name for vector storage."),apiKey:c.z.string().optional().describe("Qdrant API key for authentication (supports ${ENV_VAR} substitution).")}),$=c.z.object({host:c.z.string().optional().describe('Host address for API server (e.g., "127.0.0.1", "0.0.0.0").'),port:c.z.number().optional().describe("Port for API server (e.g., 3456).")}),K=c.z.object({level:c.z.string().optional().describe("Logging level (trace, debug, info, warn, error, fatal)."),file:c.z.string().optional().describe("Path to log file (logs to stdout if omitted).")}),J=c.z.object({match:c.z.record(c.z.string(),c.z.unknown()).describe("JSON Schema object to match against file attributes."),set:c.z.record(c.z.string(),c.z.unknown()).describe("Metadata fields to set when match succeeds."),map:c.z.union([l.jsonMapMapSchema,c.z.string()]).optional().describe("JsonMap transformation (inline definition or named map reference).")}),G=c.z.object({watch:_.describe("File system watch configuration."),configWatch:L.optional().describe("Configuration file watch settings."),embedding:O.describe("Embedding model configuration."),vectorStore:Q.describe("Qdrant vector store configuration."),metadataDir:c.z.string().optional().describe("Directory for persisted metadata sidecar files."),api:$.optional().describe("API server configuration."),extractors:c.z.record(c.z.string(),c.z.unknown()).optional().describe("Extractor configurations keyed by name."),inferenceRules:c.z.array(J).optional().describe("Rules for inferring metadata from file attributes."),maps:c.z.record(c.z.string(),l.jsonMapMapSchema).optional().describe("Reusable named JsonMap transformations."),logging:K.optional().describe("Logging configuration."),shutdownTimeoutMs:c.z.number().optional().describe("Timeout in milliseconds for graceful shutdown.")}),U="jeeves-watcher";async function V(e){const t=a.cosmiconfig(U),i=e?await t.load(e):await t.search();if(!i||i.isEmpty)throw new Error("No jeeves-watcher configuration found. Create a .jeeves-watcherrc or jeeves-watcher.config.{js,ts,json,yaml} file.");try{const e=G.parse(i.config);return r=e,{...D,...r,watch:{...q,...r.watch},configWatch:{...E,...r.configWatch},embedding:{...A,...r.embedding},api:{...W,...r.api},logging:{...N,...r.logging}}}catch(e){if(e instanceof c.ZodError){const t=e.issues.map((e=>`${e.path.join(".")}: ${e.message}`)).join("; ");throw new Error(`Invalid jeeves-watcher configuration: ${t}`)}throw e}var r}function B(e){return e||{warn(e,t){t?console.warn(e,t):console.warn(e)}}}function H(e,t){return e<=0?Promise.resolve():new Promise(((i,r)=>{const o=setTimeout((()=>{s(),i()}),e),n=()=>{s(),r(new Error("Retry sleep aborted"))},s=()=>{clearTimeout(o),t&&t.removeEventListener("abort",n)};if(t){if(t.aborted)return void n();t.addEventListener("abort",n,{once:!0})}}))}function Y(e,t,i,r=0){const o=Math.max(0,e-1),n=Math.min(i,t*2**o),s=r>0?1+Math.random()*r:1;return Math.round(n*s)}async function Z(e,t){const i=Math.max(1,t.attempts);let r;for(let o=1;o<=i;o++)try{return await e(o)}catch(e){r=e;if(o>=i)break;const n=Y(o,t.baseDelayMs,t.maxDelayMs,t.jitter);t.onRetry?.({attempt:o,attempts:i,delayMs:n,error:e}),await H(n,t.signal)}throw r}const X=new Map([["mock",function(e){return function(e){return{dimensions:e,embed:t=>Promise.resolve(t.map((t=>{const i=s.createHash("sha256").update(t,"utf8").digest(),r=[];for(let t=0;t<e;t++){const e=i[t%i.length];r.push(e/127.5-1)}return r})))}}(e.dimensions??768)}],["gemini",function(e,t){if(!e.apiKey)throw new Error("Gemini embedding provider requires config.embedding.apiKey");const i=e.dimensions??3072,r=B(t),o=new d.GoogleGenerativeAIEmbeddings({apiKey:e.apiKey,model:e.model});return{dimensions:i,async embed(t){const n=await Z((async i=>(i>1&&r.warn({attempt:i,provider:"gemini",model:e.model},"Retrying embedding request"),o.embedDocuments(t))),{attempts:5,baseDelayMs:500,maxDelayMs:1e4,jitter:.2,onRetry:({attempt:t,delayMs:i,error:o})=>{r.warn({attempt:t,delayMs:i,provider:"gemini",model:e.model,error:o},"Embedding call failed; will retry")}});for(const e of n)if(e.length!==i)throw new Error(`Gemini embedding returned invalid dimensions: expected ${String(i)}, got ${String(e.length)}`);return n}}}]]);function ee(e,t){const i=X.get(e.provider);if(!i)throw new Error(`Unsupported embedding provider: ${e.provider}`);return i(e,t)}function te(e){const t=e?.level??"info";if(e?.file){const i=h.transport({target:"pino/file",options:{destination:e.file,mkdir:!0}});return h({level:t},i)}return h({level:t})}function ie(e){return s.createHash("sha256").update(e,"utf8").digest("hex")}const re="6a6f686e-6761-4c74-ad6a-656576657321";function oe(e,t){const i=void 0!==t?`${z(e)}#${String(t)}`:z(e);return u.v5(i,re)}const ne=["content","body","text","snippet","subject","description","summary","transcript"];function se(e){if(!e||"object"!=typeof e)return JSON.stringify(e);const t=e;for(const e of ne){const i=t[e];if("string"==typeof i&&i.trim())return i}return JSON.stringify(e)}async function ae(e){const t=await i.readFile(e,"utf8"),{frontmatter:r,body:o}=function(e){const t=e.replace(/^\uFEFF/,""),i=/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/m.exec(t);if(!i)return{body:e};const[,r,o]=i,n=f.load(r);return{frontmatter:n&&"object"==typeof n&&!Array.isArray(n)?n:void 0,body:o}}(t);return{text:o,frontmatter:r}}async function ce(e){return{text:await i.readFile(e,"utf8")}}async function le(e){const t=await i.readFile(e,"utf8"),r=P.load(t);r("script, style").remove();return{text:r("body").text().trim()||r.text().trim()}}const de=new Map([[".md",ae],[".markdown",ae],[".txt",ce],[".text",ce],[".json",async function(e){const t=await i.readFile(e,"utf8"),r=JSON.parse(t),o=r&&"object"==typeof r&&!Array.isArray(r)?r:void 0;return{text:se(r),json:o}}],[".pdf",async function(e){const t=await i.readFile(e),r=new Uint8Array(t),{extractText:o}=await import("unpdf"),{text:n}=await o(r);return{text:Array.isArray(n)?n.join("\n\n"):n}}],[".docx",async function(e){const t=await i.readFile(e);return{text:(await p.extractRawText({buffer:t})).value}}],[".html",le],[".htm",le]]);async function he(e,t){const i=de.get(t.toLowerCase());return i?i(e):ce(e)}function ue(e,t){return"string"!=typeof e?e:e.replace(/\$\{([^}]+)\}/g,((e,i)=>{const r=n.get(t,i);return null==r?"":"string"==typeof r?r:JSON.stringify(r)}))}function ge(e,t){const i={};for(const[r,o]of Object.entries(e))i[r]=ue(o,t);return i}async function fe(e,t,i,r){const o={split:(e,t)=>e.split(t),slice:(e,t,i)=>e.slice(t,i),join:(e,t)=>e.join(t),toLowerCase:e=>e.toLowerCase(),replace:(e,t,i)=>e.replace(t,i),get:(e,t)=>n.get(e,t)};let s={};const a=r??console;for(const{rule:r,validate:n}of e)if(n(t)){const e=ge(r.set,t);if(s={...s,...e},r.map){let e;if("string"==typeof r.map){if(e=i?.[r.map],!e){a.warn(`Map reference "${r.map}" not found in named maps. Skipping map transformation.`);continue}}else e=r.map;try{const i=new l.JsonMap(e,o),r=await i.transform(t);r&&"object"==typeof r&&!Array.isArray(r)?s={...s,...r}:a.warn("JsonMap transformation did not return an object; skipping merge.")}catch(e){a.warn(`JsonMap transformation failed: ${e instanceof Error?e.message:String(e)}`)}}}return s}function pe(e,t,i,o){const n=e.replace(/\\/g,"/"),s={file:{path:n,directory:r.dirname(n).replace(/\\/g,"/"),filename:r.basename(n),extension:r.extname(n),sizeBytes:t.size,modified:t.mtime.toISOString()}};return i&&(s.frontmatter=i),o&&(s.json=o),s}function me(e){const t=function(){const e=new m({allErrors:!0});return y(e),e.addKeyword({keyword:"glob",type:"string",schemaType:"string",validate:(e,t)=>o.isMatch(t,e)}),e}();return e.map(((e,i)=>({rule:e,validate:t.compile({$id:`rule-${String(i)}`,...e.match})})))}async function ye(e,t,o,n,s){const a=r.extname(e),c=await i.stat(e),l=await he(e,a),d=pe(e,c,l.frontmatter,l.json),h=await fe(t,d,n,s),u=await C(e,o);return{inferred:h,enrichment:u,metadata:{...h,...u??{}},attributes:d,extracted:l}}function we(e,t){const i=[];for(let r=0;r<t;r++)i.push(oe(e,r));return i}function be(e,t=1){if(!e)return t;const i=e.total_chunks;return"number"==typeof i?i:t}class ve{config;embeddingProvider;vectorStore;compiledRules;logger;constructor(e,t,i,r,o){this.config=e,this.embeddingProvider=t,this.vectorStore=i,this.compiledRules=r,this.logger=o}async processFile(e){try{const t=r.extname(e),{metadata:i,extracted:o}=await ye(e,this.compiledRules,this.config.metadataDir,this.config.maps,this.logger);if(!o.text.trim())return void this.logger.debug({filePath:e},"Skipping empty file");const n=ie(o.text),s=oe(e,0),a=await this.vectorStore.getPayload(s);if(a&&a.content_hash===n)return void this.logger.debug({filePath:e},"Content unchanged, skipping");const c=be(a),l=this.config.chunkSize??1e3,d=function(e,t,i){const r=e.toLowerCase();return".md"===r||".markdown"===r?new w.MarkdownTextSplitter({chunkSize:t,chunkOverlap:i}):new w.RecursiveCharacterTextSplitter({chunkSize:t,chunkOverlap:i})}(t,l,this.config.chunkOverlap??200),h=await d.splitText(o.text),u=await this.embeddingProvider.embed(h),g=h.map(((t,r)=>({id:oe(e,r),vector:u[r],payload:{...i,file_path:e.replace(/\\/g,"/"),chunk_index:r,total_chunks:h.length,content_hash:n,chunk_text:t}})));if(await this.vectorStore.upsert(g),c>h.length){const t=we(e,c).slice(h.length);await this.vectorStore.delete(t)}this.logger.info({filePath:e,chunks:h.length},"File processed successfully")}catch(t){this.logger.error({filePath:e,error:t},"Failed to process file")}}async deleteFile(e){try{const t=oe(e,0),i=await this.vectorStore.getPayload(t),r=we(e,be(i));await this.vectorStore.delete(r),await R(e,this.config.metadataDir),this.logger.info({filePath:e},"File deleted from index")}catch(t){this.logger.error({filePath:e,error:t},"Failed to delete file")}}async processMetadataUpdate(e,t){try{const i={...await C(e,this.config.metadataDir)??{},...t};await F(e,this.config.metadataDir,i);const r=oe(e,0),o=await this.vectorStore.getPayload(r);if(!o)return null;const n=be(o),s=we(e,n);return await this.vectorStore.setPayload(s,i),this.logger.info({filePath:e,chunks:n},"Metadata updated"),i}catch(t){return this.logger.error({filePath:e,error:t},"Failed to update metadata"),null}}async processRulesUpdate(e){try{const t=oe(e,0),i=await this.vectorStore.getPayload(t);if(!i)return this.logger.debug({filePath:e},"File not indexed, skipping"),null;const{metadata:r}=await ye(e,this.compiledRules,this.config.metadataDir,this.config.maps,this.logger),o=be(i),n=we(e,o);return await this.vectorStore.setPayload(n,r),this.logger.info({filePath:e,chunks:o},"Rules re-applied"),r}catch(t){return this.logger.error({filePath:e,error:t},"Failed to re-apply rules"),null}}updateRules(e){this.compiledRules=e,this.logger.info({rules:e.length},"Inference rules updated")}}class Me{debounceMs;concurrency;rateLimitPerMinute;started=!1;active=0;debounceTimers=new Map;latestByKey=new Map;normalQueue=[];lowQueue=[];tokens;lastRefillMs=Date.now();drainWaiters=[];constructor(e){this.debounceMs=e.debounceMs,this.concurrency=e.concurrency,this.rateLimitPerMinute=e.rateLimitPerMinute,this.tokens=this.rateLimitPerMinute??Number.POSITIVE_INFINITY}enqueue(e,t){const i=`${e.priority}:${e.path}`;this.latestByKey.set(i,{event:e,fn:t});const r=this.debounceTimers.get(i);r&&clearTimeout(r);const o=setTimeout((()=>{this.debounceTimers.delete(i);const e=this.latestByKey.get(i);e&&(this.latestByKey.delete(i),this.push(e),this.pump())}),this.debounceMs);this.debounceTimers.set(i,o)}process(){this.started=!0,this.pump()}async drain(){this.isIdle()||await new Promise((e=>{this.drainWaiters.push(e)}))}push(e){"low"===e.event.priority?this.lowQueue.push(e):this.normalQueue.push(e)}refillTokens(e){if(void 0===this.rateLimitPerMinute)return;const t=Math.max(0,e-this.lastRefillMs)*(this.rateLimitPerMinute/6e4);this.tokens=Math.min(this.rateLimitPerMinute,this.tokens+t),this.lastRefillMs=e}takeToken(){const e=Date.now();return this.refillTokens(e),!(this.tokens<1)&&(this.tokens-=1,!0)}nextItem(){return this.normalQueue.shift()??this.lowQueue.shift()}pump(){if(this.started){for(;this.active<this.concurrency;){const e=this.nextItem();if(!e)break;if(!this.takeToken()){"low"===e.event.priority?this.lowQueue.unshift(e):this.normalQueue.unshift(e),setTimeout((()=>{this.pump()}),250);break}this.active+=1,Promise.resolve().then((()=>e.fn(e.event))).finally((()=>{this.active-=1,this.pump(),this.maybeResolveDrain()}))}this.maybeResolveDrain()}}isIdle(){return 0===this.active&&0===this.normalQueue.length&&0===this.lowQueue.length&&0===this.debounceTimers.size&&0===this.latestByKey.size}maybeResolveDrain(){if(!this.isIdle())return;const e=this.drainWaiters;this.drainWaiters=[];for(const t of e)t()}}class Pe{client;collectionName;dims;log;constructor(e,t,i){this.client=new b.QdrantClient({url:e.url,apiKey:e.apiKey,checkCompatibility:!1}),this.collectionName=e.collectionName,this.dims=t,this.log=B(i)}async ensureCollection(){try{const e=await this.client.getCollections();e.collections.some((e=>e.name===this.collectionName))||await this.client.createCollection(this.collectionName,{vectors:{size:this.dims,distance:"Cosine"}})}catch(e){throw new Error(`Failed to ensure collection "${this.collectionName}": ${String(e)}`)}}async upsert(e){0!==e.length&&await Z((async t=>{t>1&&this.log.warn({attempt:t,operation:"qdrant.upsert",points:e.length},"Retrying Qdrant upsert"),await this.client.upsert(this.collectionName,{wait:!0,points:e.map((e=>({id:e.id,vector:e.vector,payload:e.payload})))})}),{attempts:5,baseDelayMs:500,maxDelayMs:1e4,jitter:.2,onRetry:({attempt:e,delayMs:t,error:i})=>{this.log.warn({attempt:e,delayMs:t,operation:"qdrant.upsert",error:i},"Qdrant upsert failed; will retry")}})}async delete(e){0!==e.length&&await Z((async t=>{t>1&&this.log.warn({attempt:t,operation:"qdrant.delete",ids:e.length},"Retrying Qdrant delete"),await this.client.delete(this.collectionName,{wait:!0,points:e})}),{attempts:5,baseDelayMs:500,maxDelayMs:1e4,jitter:.2,onRetry:({attempt:e,delayMs:t,error:i})=>{this.log.warn({attempt:e,delayMs:t,operation:"qdrant.delete",error:i},"Qdrant delete failed; will retry")}})}async setPayload(e,t){0!==e.length&&await this.client.setPayload(this.collectionName,{wait:!0,points:e,payload:t})}async getPayload(e){try{const t=await this.client.retrieve(this.collectionName,{ids:[e],with_payload:!0,with_vector:!1});return 0===t.length?null:t[0].payload}catch{return null}}async search(e,t,i){return(await this.client.search(this.collectionName,{vector:e,limit:t,with_payload:!0,...i?{filter:i}:{}})).map((e=>({id:String(e.id),score:e.score,payload:e.payload})))}async*scroll(e,t=100){let i;for(;;){const r=await this.client.scroll(this.collectionName,{limit:t,with_payload:!0,with_vector:!1,...e?{filter:e}:{},...void 0!==i?{offset:i}:{}});for(const e of r.points)yield{id:String(e.id),payload:e.payload};const o=r.next_page_offset;if(null==o)break;if("string"!=typeof o&&"number"!=typeof o)break;i=o}}}class Se{config;queue;processor;logger;watcher;constructor(e,t,i,r){this.config=e,this.queue=t,this.processor=i,this.logger=r}start(){this.watcher=v.watch(this.config.paths,{ignored:this.config.ignored,usePolling:this.config.usePolling,interval:this.config.pollIntervalMs,awaitWriteFinish:!!this.config.stabilityThresholdMs&&{stabilityThreshold:this.config.stabilityThresholdMs},ignoreInitial:!1}),this.watcher.on("add",(e=>{this.logger.debug({path:e},"File added"),this.queue.enqueue({type:"create",path:e,priority:"normal"},(()=>this.processor.processFile(e)))})),this.watcher.on("change",(e=>{this.logger.debug({path:e},"File changed"),this.queue.enqueue({type:"modify",path:e,priority:"normal"},(()=>this.processor.processFile(e)))})),this.watcher.on("unlink",(e=>{this.logger.debug({path:e},"File removed"),this.queue.enqueue({type:"delete",path:e,priority:"normal"},(()=>this.processor.deleteFile(e)))})),this.watcher.on("error",(e=>{this.logger.error({error:e},"Watcher error")})),this.queue.process(),this.logger.info({paths:this.config.paths},"Filesystem watcher started")}async stop(){this.watcher&&(await this.watcher.close(),this.watcher=void 0,this.logger.info("Filesystem watcher stopped"))}}class ke{options;watcher;debounce;constructor(e){this.options=e}start(){this.options.enabled&&(this.watcher=v.watch(this.options.configPath,{ignoreInitial:!0}),this.watcher.on("change",(()=>{this.debounce&&clearTimeout(this.debounce),this.debounce=setTimeout((()=>{this.options.onChange()}),this.options.debounceMs)})),this.watcher.on("error",(e=>{this.options.logger.error({error:e},"Config watcher error")})),this.options.logger.info({configPath:this.options.configPath,debounceMs:this.options.debounceMs},"Config watcher started"))}async stop(){this.debounce&&(clearTimeout(this.debounce),this.debounce=void 0),this.watcher&&(await this.watcher.close(),this.watcher=void 0)}}const xe={loadConfig:V,createLogger:te,createEmbeddingProvider:ee,createVectorStoreClient:(e,t,i)=>new Pe(e,t,i),compileRules:me,createDocumentProcessor:(e,t,i,r,o)=>new ve(e,t,i,r,o),createEventQueue:e=>new Me(e),createFileSystemWatcher:(e,t,i,r)=>new Se(e,t,i,r),createApiServer:I};class ze{config;configPath;factories;logger;watcher;queue;server;processor;configWatcher;constructor(e,t,i={}){this.config=e,this.configPath=t,this.factories={...xe,...i}}async start(){const e=this.factories.createLogger(this.config.logging);let t;this.logger=e;try{t=this.factories.createEmbeddingProvider(this.config.embedding,e)}catch(t){throw e.fatal({error:t},"Failed to create embedding provider"),t}const i=this.factories.createVectorStoreClient(this.config.vectorStore,t.dimensions,e);await i.ensureCollection();const r=this.factories.compileRules(this.config.inferenceRules??[]),o={metadataDir:this.config.metadataDir??".jeeves-metadata",chunkSize:this.config.embedding.chunkSize,chunkOverlap:this.config.embedding.chunkOverlap,maps:this.config.maps},n=this.factories.createDocumentProcessor(o,t,i,r,e);this.processor=n;const s=this.factories.createEventQueue({debounceMs:this.config.watch.debounceMs??2e3,concurrency:this.config.embedding.concurrency??5,rateLimitPerMinute:this.config.embedding.rateLimitPerMinute});this.queue=s;const a=this.factories.createFileSystemWatcher(this.config.watch,s,n,e);this.watcher=a;const c=this.factories.createApiServer({processor:n,vectorStore:i,embeddingProvider:t,queue:s,config:this.config,logger:e});this.server=c,await c.listen({host:this.config.api?.host??"127.0.0.1",port:this.config.api?.port??3456}),a.start(),this.startConfigWatch(),e.info("jeeves-watcher started")}async stop(){if(await this.stopConfigWatch(),this.watcher&&await this.watcher.stop(),this.queue){const e=this.config.shutdownTimeoutMs??1e4;await Promise.race([this.queue.drain().then((()=>!0)),new Promise((t=>{setTimeout((()=>{t(!1)}),e)}))])||this.logger?.warn({timeoutMs:e},"Queue drain timeout hit, forcing shutdown")}this.server&&await this.server.close(),this.logger?.info("jeeves-watcher stopped")}startConfigWatch(){const e=this.logger;if(!e)return;const t=this.config.configWatch?.enabled??!0;if(!t)return;if(!this.configPath)return void e.debug("Config watch enabled, but no config path was provided");const i=this.config.configWatch?.debounceMs??1e4;this.configWatcher=new ke({configPath:this.configPath,enabled:t,debounceMs:i,logger:e,onChange:async()=>this.reloadConfig()}),this.configWatcher.start()}async stopConfigWatch(){this.configWatcher&&(await this.configWatcher.stop(),this.configWatcher=void 0)}async reloadConfig(){const e=this.logger,t=this.processor;if(e&&t&&this.configPath){e.info({configPath:this.configPath},"Config change detected, reloading...");try{const i=await this.factories.loadConfig(this.configPath);this.config=i;const r=this.factories.compileRules(i.inferenceRules??[]);t.updateRules(r),e.info({configPath:this.configPath,rules:r.length},"Config reloaded")}catch(t){e.error({error:t},"Failed to reload config")}}}}e.DocumentProcessor=ve,e.EventQueue=Me,e.FileSystemWatcher=Se,e.JeevesWatcher=ze,e.VectorStoreClient=Pe,e.apiConfigSchema=$,e.applyRules=fe,e.buildAttributes=pe,e.compileRules=me,e.configWatchConfigSchema=L,e.contentHash=ie,e.createApiServer=I,e.createEmbeddingProvider=ee,e.createLogger=te,e.deleteMetadata=R,e.embeddingConfigSchema=O,e.extractText=he,e.inferenceRuleSchema=J,e.jeevesWatcherConfigSchema=G,e.loadConfig=V,e.loggingConfigSchema=K,e.metadataPath=j,e.pointId=oe,e.readMetadata=C,e.startFromConfig=async function(e){const t=await V(e),i=new ze(t,e);return function(e){const t=async()=>{await e(),process.exit(0)};process.on("SIGTERM",(()=>{t()})),process.on("SIGINT",(()=>{t()}))}((()=>i.stop())),await i.start(),i},e.vectorStoreConfigSchema=Q,e.watchConfigSchema=_,e.writeMetadata=F}(this["jeeves-watcher"]=this["jeeves-watcher"]||{},Fastify,promises,node_path,picomatch,radash,node_crypto,cosmiconfig,zod,jsonmap,googleGenai,pino,uuid,cheerio,yaml,mammoth,Ajv,addFormats,textsplitters,jsClientRest,chokidar);
1
+ !function(e,t,i,r,o,n,s,a,c,l,d,h,u,g,f,p,m,y,w,b,v){"use strict";function M(e){var t=Object.create(null);return e&&Object.keys(e).forEach((function(i){if("default"!==i){var r=Object.getOwnPropertyDescriptor(e,i);Object.defineProperty(t,i,r.get?r:{enumerable:!0,get:function(){return e[i]}})}})),t.default=e,Object.freeze(t)}var P=M(g);function S(e){if(e instanceof Error)return e;if("string"==typeof e)return new Error(e);const t=String("object"==typeof e&&null!==e&&"message"in e?e.message:e),i=new Error(t);return i.cause=e,i}function k(e){const t=e.replace(/\\/g,"/"),i=t.search(/[*?\[]/);if(-1===i)return r.resolve(e);const o=t.slice(0,i),n=o.endsWith("/")?o.slice(0,-1):r.dirname(o);return r.resolve(n)}async function*x(e){let t;try{t=(await i.readdir(e,{withFileTypes:!0})).map((e=>({name:e.name,isDirectory:e.isDirectory()})))}catch{return}for(const o of t){const t=r.resolve(e,o.name);if(o.isDirectory)yield*x(t);else try{(await i.stat(t)).isFile()&&(yield t)}catch{}}}async function z(e,t,i,r){const n=await async function(e,t=[]){const i=e.map((e=>e.replace(/\\/g,"/"))),r=t.map((e=>e.replace(/\\/g,"/"))),n=o(i,{dot:!0}),s=r.length?o(r,{dot:!0}):()=>!1,a=Array.from(new Set(e.map(k))),c=new Set;for(const e of a)for await(const t of x(e)){const e=t.replace(/\\/g,"/");s(e)||n(e)&&c.add(t)}return Array.from(c)}(e,t);for(const e of n)await i[r](e);return n.length}function j(e,t=!1){let i=e.replace(/\\/g,"/").toLowerCase();return t&&(i=i.replace(/^([a-z]):/,((e,t)=>t))),i}function C(e,t){const i=j(e,!0),o=s.createHash("sha256").update(i,"utf8").digest("hex");return r.join(t,`${o}.meta.json`)}async function F(e,t){try{const r=await i.readFile(C(e,t),"utf8");return JSON.parse(r)}catch{return null}}async function R(e,t,o){const n=C(e,t);await i.mkdir(r.dirname(n),{recursive:!0}),await i.writeFile(n,JSON.stringify(o,null,2),"utf8")}async function T(e,t){try{await i.rm(C(e,t))}catch{}}const E=["file_path","chunk_index","total_chunks","content_hash","chunk_text"];function I(e){const{processor:i,vectorStore:r,embeddingProvider:o,logger:s,config:a}=e,c=t({logger:!1});var l;return c.get("/status",(()=>({status:"ok",uptime:process.uptime()}))),c.post("/metadata",(l={processor:i,logger:s},async(e,t)=>{try{const{path:t,metadata:i}=e.body;return await l.processor.processMetadataUpdate(t,i),{ok:!0}}catch(e){return l.logger.error({err:S(e)},"Metadata update failed"),t.status(500).send({error:"Internal server error"})}})),c.post("/search",function(e){return async(t,i)=>{try{const{query:i,limit:r=10}=t.body,o=await e.embeddingProvider.embed([i]);return await e.vectorStore.search(o[0],r)}catch(t){return e.logger.error({err:S(t)},"Search failed"),i.status(500).send({error:"Internal server error"})}}}({embeddingProvider:o,vectorStore:r,logger:s})),c.post("/reindex",function(e){return async(t,i)=>{try{const t=await z(e.config.watch.paths,e.config.watch.ignored,e.processor,"processFile");return await i.status(200).send({ok:!0,filesIndexed:t})}catch(t){return e.logger.error({err:S(t)},"Reindex failed"),await i.status(500).send({error:"Internal server error"})}}}({config:a,processor:i,logger:s})),c.post("/rebuild-metadata",function(e){return async(t,i)=>{try{const t=e.config.metadataDir??".jeeves-metadata",r=[...E];for await(const i of e.vectorStore.scroll()){const e=i.payload,o=e.file_path;if("string"!=typeof o||0===o.length)continue;const s=n.omit(e,r);await R(o,t,s)}return await i.status(200).send({ok:!0})}catch(t){return e.logger.error({err:S(t)},"Rebuild metadata failed"),await i.status(500).send({error:"Internal server error"})}}}({config:a,vectorStore:r,logger:s})),c.post("/config-reindex",function(e){return async(t,i)=>{try{const r=t.body.scope??"rules";return(async()=>{try{if("rules"===r){const t=await z(e.config.watch.paths,e.config.watch.ignored,e.processor,"processRulesUpdate");e.logger.info({scope:r,filesProcessed:t},"Config reindex (rules) completed")}else{const t=await z(e.config.watch.paths,e.config.watch.ignored,e.processor,"processFile");e.logger.info({scope:r,filesProcessed:t},"Config reindex (full) completed")}}catch(t){e.logger.error({err:S(t),scope:r},"Config reindex failed")}})(),await i.status(200).send({status:"started",scope:r})}catch(t){return e.logger.error({err:S(t)},"Config reindex request failed"),await i.status(500).send({error:"Internal server error"})}}}({config:a,processor:i,logger:s})),c}const D={metadataDir:".jeeves-watcher",shutdownTimeoutMs:1e4},A={enabled:!0,debounceMs:1e3},W={host:"127.0.0.1",port:3456},N={level:"info"},q={debounceMs:300,stabilityThresholdMs:500,usePolling:!1,pollIntervalMs:1e3},_={chunkSize:1e3,chunkOverlap:200,dimensions:3072,rateLimitPerMinute:300,concurrency:5},L=c.z.object({paths:c.z.array(c.z.string()).min(1).describe('Glob patterns for files to watch (e.g., "**/*.md"). At least one required.'),ignored:c.z.array(c.z.string()).optional().describe('Glob patterns to exclude from watching (e.g., "**/node_modules/**").'),pollIntervalMs:c.z.number().optional().describe("Polling interval in milliseconds when usePolling is enabled."),usePolling:c.z.boolean().optional().describe("Use polling instead of native file system events (for network drives)."),debounceMs:c.z.number().optional().describe("Debounce delay in milliseconds for file change events."),stabilityThresholdMs:c.z.number().optional().describe("Time in milliseconds a file must remain unchanged before processing.")}),O=c.z.object({enabled:c.z.boolean().optional().describe("Enable automatic reloading when config file changes."),debounceMs:c.z.number().optional().describe("Debounce delay in milliseconds for config file change detection.")}),Q=c.z.object({provider:c.z.string().default("gemini").describe('Embedding provider name (e.g., "gemini", "openai").'),model:c.z.string().default("gemini-embedding-001").describe('Embedding model identifier (e.g., "gemini-embedding-001", "text-embedding-3-small").'),chunkSize:c.z.number().optional().describe("Maximum chunk size in characters for text splitting."),chunkOverlap:c.z.number().optional().describe("Character overlap between consecutive chunks."),dimensions:c.z.number().optional().describe("Embedding vector dimensions (must match model output)."),apiKey:c.z.string().optional().describe("API key for embedding provider (supports ${ENV_VAR} substitution)."),rateLimitPerMinute:c.z.number().optional().describe("Maximum embedding API requests per minute (rate limiting)."),concurrency:c.z.number().optional().describe("Maximum concurrent embedding requests.")}),$=c.z.object({url:c.z.string().describe('Qdrant server URL (e.g., "http://localhost:6333").'),collectionName:c.z.string().describe("Qdrant collection name for vector storage."),apiKey:c.z.string().optional().describe("Qdrant API key for authentication (supports ${ENV_VAR} substitution).")}),K=c.z.object({host:c.z.string().optional().describe('Host address for API server (e.g., "127.0.0.1", "0.0.0.0").'),port:c.z.number().optional().describe("Port for API server (e.g., 3456).")}),J=c.z.object({level:c.z.string().optional().describe("Logging level (trace, debug, info, warn, error, fatal)."),file:c.z.string().optional().describe("Path to log file (logs to stdout if omitted).")}),G=c.z.object({match:c.z.record(c.z.string(),c.z.unknown()).describe("JSON Schema object to match against file attributes."),set:c.z.record(c.z.string(),c.z.unknown()).describe("Metadata fields to set when match succeeds."),map:c.z.union([l.jsonMapMapSchema,c.z.string()]).optional().describe("JsonMap transformation (inline definition or named map reference).")}),U=c.z.object({watch:L.describe("File system watch configuration."),configWatch:O.optional().describe("Configuration file watch settings."),embedding:Q.describe("Embedding model configuration."),vectorStore:$.describe("Qdrant vector store configuration."),metadataDir:c.z.string().optional().describe("Directory for persisted metadata sidecar files."),api:K.optional().describe("API server configuration."),extractors:c.z.record(c.z.string(),c.z.unknown()).optional().describe("Extractor configurations keyed by name."),inferenceRules:c.z.array(G).optional().describe("Rules for inferring metadata from file attributes."),maps:c.z.record(c.z.string(),l.jsonMapMapSchema).optional().describe("Reusable named JsonMap transformations."),logging:J.optional().describe("Logging configuration."),shutdownTimeoutMs:c.z.number().optional().describe("Timeout in milliseconds for graceful shutdown.")}),V=/\$\{([^}]+)\}/g;function B(e){if("string"==typeof e)return function(e){return e.replace(V,((e,t)=>{const i=process.env[t];return void 0===i?e:i}))}(e);if(Array.isArray(e))return e.map((e=>B(e)));if(null!==e&&"object"==typeof e){const t={};for(const[i,r]of Object.entries(e))t[i]=B(r);return t}return e}const H="jeeves-watcher";async function Y(e){const t=a.cosmiconfig(H),i=e?await t.load(e):await t.search();if(!i||i.isEmpty)throw new Error("No jeeves-watcher configuration found. Create a .jeeves-watcherrc or jeeves-watcher.config.{js,ts,json,yaml} file.");try{const e=U.parse(i.config);return B((r=e,{...D,...r,watch:{...q,...r.watch},configWatch:{...A,...r.configWatch},embedding:{..._,...r.embedding},api:{...W,...r.api},logging:{...N,...r.logging}}))}catch(e){if(e instanceof c.ZodError){const t=e.issues.map((e=>`${e.path.join(".")}: ${e.message}`)).join("; ");throw new Error(`Invalid jeeves-watcher configuration: ${t}`)}throw e}var r}function Z(e){return e||{warn(e,t){t?console.warn(e,t):console.warn(e)}}}function X(e,t){return e<=0?Promise.resolve():new Promise(((i,r)=>{const o=setTimeout((()=>{s(),i()}),e),n=()=>{s(),r(new Error("Retry sleep aborted"))},s=()=>{clearTimeout(o),t&&t.removeEventListener("abort",n)};if(t){if(t.aborted)return void n();t.addEventListener("abort",n,{once:!0})}}))}function ee(e,t,i,r=0){const o=Math.max(0,e-1),n=Math.min(i,t*2**o),s=r>0?1+Math.random()*r:1;return Math.round(n*s)}async function te(e,t){const i=Math.max(1,t.attempts);let r;for(let o=1;o<=i;o++)try{return await e(o)}catch(e){r=e;if(o>=i)break;const n=ee(o,t.baseDelayMs,t.maxDelayMs,t.jitter);t.onRetry?.({attempt:o,attempts:i,delayMs:n,error:e}),await X(n,t.signal)}throw r}const ie=new Map([["mock",function(e){return function(e){return{dimensions:e,embed:t=>Promise.resolve(t.map((t=>{const i=s.createHash("sha256").update(t,"utf8").digest(),r=[];for(let t=0;t<e;t++){const e=i[t%i.length];r.push(e/127.5-1)}return r})))}}(e.dimensions??768)}],["gemini",function(e,t){if(!e.apiKey)throw new Error("Gemini embedding provider requires config.embedding.apiKey");const i=e.dimensions??3072,r=Z(t),o=new d.GoogleGenerativeAIEmbeddings({apiKey:e.apiKey,model:e.model});return{dimensions:i,async embed(t){const n=await te((async i=>(i>1&&r.warn({attempt:i,provider:"gemini",model:e.model},"Retrying embedding request"),o.embedDocuments(t))),{attempts:5,baseDelayMs:500,maxDelayMs:1e4,jitter:.2,onRetry:({attempt:t,delayMs:i,error:o})=>{r.warn({attempt:t,delayMs:i,provider:"gemini",model:e.model,err:S(o)},"Embedding call failed; will retry")}});for(const e of n)if(e.length!==i)throw new Error(`Gemini embedding returned invalid dimensions: expected ${String(i)}, got ${String(e.length)}`);return n}}}]]);function re(e,t){const i=ie.get(e.provider);if(!i)throw new Error(`Unsupported embedding provider: ${e.provider}`);return i(e,t)}function oe(e){const t=e?.level??"info";if(e?.file){const i=h.transport({target:"pino/file",options:{destination:e.file,mkdir:!0}});return h({level:t},i)}return h({level:t})}function ne(e){return s.createHash("sha256").update(e,"utf8").digest("hex")}const se="6a6f686e-6761-4c74-ad6a-656576657321";function ae(e,t){const i=void 0!==t?`${j(e)}#${String(t)}`:j(e);return u.v5(i,se)}const ce=["content","body","text","snippet","subject","description","summary","transcript"];function le(e){if(!e||"object"!=typeof e)return JSON.stringify(e);const t=e;for(const e of ce){const i=t[e];if("string"==typeof i&&i.trim())return i}return JSON.stringify(e)}async function de(e){const t=await i.readFile(e,"utf8"),{frontmatter:r,body:o}=function(e){const t=e.replace(/^\uFEFF/,"");if(!/^\s*---/.test(t))return{body:e};const i=/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/m.exec(t);if(!i)return{body:e};const[,r,o]=i,n=f.load(r);return{frontmatter:n&&"object"==typeof n&&!Array.isArray(n)?n:void 0,body:o}}(t);return{text:o,frontmatter:r}}async function he(e){return{text:await i.readFile(e,"utf8")}}async function ue(e){const t=await i.readFile(e,"utf8"),r=P.load(t);r("script, style").remove();return{text:r("body").text().trim()||r.text().trim()}}const ge=new Map([[".md",de],[".markdown",de],[".txt",he],[".text",he],[".json",async function(e){const t=await i.readFile(e,"utf8"),r=JSON.parse(t),o=r&&"object"==typeof r&&!Array.isArray(r)?r:void 0;return{text:le(r),json:o}}],[".pdf",async function(e){const t=await i.readFile(e),r=new Uint8Array(t),{extractText:o}=await import("unpdf"),{text:n}=await o(r);return{text:Array.isArray(n)?n.join("\n\n"):n}}],[".docx",async function(e){const t=await i.readFile(e);return{text:(await p.extractRawText({buffer:t})).value}}],[".html",ue],[".htm",ue]]);async function fe(e,t){const i=ge.get(t.toLowerCase());return i?i(e):he(e)}function pe(e,t){return"string"!=typeof e?e:e.replace(/\$\{([^}]+)\}/g,((e,i)=>{const r=n.get(t,i);return null==r?"":"string"==typeof r?r:JSON.stringify(r)}))}function me(e,t){const i={};for(const[r,o]of Object.entries(e))i[r]=pe(o,t);return i}async function ye(e,t,i,r){const o={split:(e,t)=>e.split(t),slice:(e,t,i)=>e.slice(t,i),join:(e,t)=>e.join(t),toLowerCase:e=>e.toLowerCase(),replace:(e,t,i)=>e.replace(t,i),get:(e,t)=>n.get(e,t)};let s={};const a=r??console;for(const{rule:r,validate:n}of e)if(n(t)){const e=me(r.set,t);if(s={...s,...e},r.map){let e;if("string"==typeof r.map){if(e=i?.[r.map],!e){a.warn(`Map reference "${r.map}" not found in named maps. Skipping map transformation.`);continue}}else e=r.map;try{const i=new l.JsonMap(e,o),r=await i.transform(t);r&&"object"==typeof r&&!Array.isArray(r)?s={...s,...r}:a.warn("JsonMap transformation did not return an object; skipping merge.")}catch(e){a.warn(`JsonMap transformation failed: ${e instanceof Error?e.message:String(e)}`)}}}return s}function we(e,t,i,o){const n=e.replace(/\\/g,"/"),s={file:{path:n,directory:r.dirname(n).replace(/\\/g,"/"),filename:r.basename(n),extension:r.extname(n),sizeBytes:t.size,modified:t.mtime.toISOString()}};return i&&(s.frontmatter=i),o&&(s.json=o),s}function be(e){const t=function(){const e=new m({allErrors:!0});return y(e),e.addKeyword({keyword:"glob",type:"string",schemaType:"string",validate:(e,t)=>o.isMatch(t,e)}),e}();return e.map(((e,i)=>({rule:e,validate:t.compile({$id:`rule-${String(i)}`,...e.match})})))}async function ve(e,t,o,n,s){const a=r.extname(e),c=await i.stat(e),l=await fe(e,a),d=we(e,c,l.frontmatter,l.json),h=await ye(t,d,n,s),u=await F(e,o);return{inferred:h,enrichment:u,metadata:{...h,...u??{}},attributes:d,extracted:l}}function Me(e,t){const i=[];for(let r=0;r<t;r++)i.push(ae(e,r));return i}function Pe(e,t=1){if(!e)return t;const i=e.total_chunks;return"number"==typeof i?i:t}class Se{config;embeddingProvider;vectorStore;compiledRules;logger;constructor(e,t,i,r,o){this.config=e,this.embeddingProvider=t,this.vectorStore=i,this.compiledRules=r,this.logger=o}async processFile(e){try{const t=r.extname(e),{metadata:i,extracted:o}=await ve(e,this.compiledRules,this.config.metadataDir,this.config.maps,this.logger);if(!o.text.trim())return void this.logger.debug({filePath:e},"Skipping empty file");const n=ne(o.text),s=ae(e,0),a=await this.vectorStore.getPayload(s);if(a&&a.content_hash===n)return void this.logger.debug({filePath:e},"Content unchanged, skipping");const c=Pe(a),l=this.config.chunkSize??1e3,d=function(e,t,i){const r=e.toLowerCase();return".md"===r||".markdown"===r?new w.MarkdownTextSplitter({chunkSize:t,chunkOverlap:i}):new w.RecursiveCharacterTextSplitter({chunkSize:t,chunkOverlap:i})}(t,l,this.config.chunkOverlap??200),h=await d.splitText(o.text),u=await this.embeddingProvider.embed(h),g=h.map(((t,r)=>({id:ae(e,r),vector:u[r],payload:{...i,file_path:e.replace(/\\/g,"/"),chunk_index:r,total_chunks:h.length,content_hash:n,chunk_text:t}})));if(await this.vectorStore.upsert(g),c>h.length){const t=Me(e,c).slice(h.length);await this.vectorStore.delete(t)}this.logger.info({filePath:e,chunks:h.length},"File processed successfully")}catch(t){this.logger.error({filePath:e,err:S(t)},"Failed to process file")}}async deleteFile(e){try{const t=ae(e,0),i=await this.vectorStore.getPayload(t),r=Me(e,Pe(i));await this.vectorStore.delete(r),await T(e,this.config.metadataDir),this.logger.info({filePath:e},"File deleted from index")}catch(t){this.logger.error({filePath:e,err:S(t)},"Failed to delete file")}}async processMetadataUpdate(e,t){try{const i={...await F(e,this.config.metadataDir)??{},...t};await R(e,this.config.metadataDir,i);const r=ae(e,0),o=await this.vectorStore.getPayload(r);if(!o)return null;const n=Pe(o),s=Me(e,n);return await this.vectorStore.setPayload(s,i),this.logger.info({filePath:e,chunks:n},"Metadata updated"),i}catch(t){return this.logger.error({filePath:e,err:S(t)},"Failed to update metadata"),null}}async processRulesUpdate(e){try{const t=ae(e,0),i=await this.vectorStore.getPayload(t);if(!i)return this.logger.debug({filePath:e},"File not indexed, skipping"),null;const{metadata:r}=await ve(e,this.compiledRules,this.config.metadataDir,this.config.maps,this.logger),o=Pe(i),n=Me(e,o);return await this.vectorStore.setPayload(n,r),this.logger.info({filePath:e,chunks:o},"Rules re-applied"),r}catch(t){return this.logger.error({filePath:e,err:S(t)},"Failed to re-apply rules"),null}}updateRules(e){this.compiledRules=e,this.logger.info({rules:e.length},"Inference rules updated")}}class ke{debounceMs;concurrency;rateLimitPerMinute;started=!1;active=0;debounceTimers=new Map;latestByKey=new Map;normalQueue=[];lowQueue=[];tokens;lastRefillMs=Date.now();drainWaiters=[];constructor(e){this.debounceMs=e.debounceMs,this.concurrency=e.concurrency,this.rateLimitPerMinute=e.rateLimitPerMinute,this.tokens=this.rateLimitPerMinute??Number.POSITIVE_INFINITY}enqueue(e,t){const i=`${e.priority}:${e.path}`;this.latestByKey.set(i,{event:e,fn:t});const r=this.debounceTimers.get(i);r&&clearTimeout(r);const o=setTimeout((()=>{this.debounceTimers.delete(i);const e=this.latestByKey.get(i);e&&(this.latestByKey.delete(i),this.push(e),this.pump())}),this.debounceMs);this.debounceTimers.set(i,o)}process(){this.started=!0,this.pump()}async drain(){this.isIdle()||await new Promise((e=>{this.drainWaiters.push(e)}))}push(e){"low"===e.event.priority?this.lowQueue.push(e):this.normalQueue.push(e)}refillTokens(e){if(void 0===this.rateLimitPerMinute)return;const t=Math.max(0,e-this.lastRefillMs)*(this.rateLimitPerMinute/6e4);this.tokens=Math.min(this.rateLimitPerMinute,this.tokens+t),this.lastRefillMs=e}takeToken(){const e=Date.now();return this.refillTokens(e),!(this.tokens<1)&&(this.tokens-=1,!0)}nextItem(){return this.normalQueue.shift()??this.lowQueue.shift()}pump(){if(this.started){for(;this.active<this.concurrency;){const e=this.nextItem();if(!e)break;if(!this.takeToken()){"low"===e.event.priority?this.lowQueue.unshift(e):this.normalQueue.unshift(e),setTimeout((()=>{this.pump()}),250);break}this.active+=1,Promise.resolve().then((()=>e.fn(e.event))).finally((()=>{this.active-=1,this.pump(),this.maybeResolveDrain()}))}this.maybeResolveDrain()}}isIdle(){return 0===this.active&&0===this.normalQueue.length&&0===this.lowQueue.length&&0===this.debounceTimers.size&&0===this.latestByKey.size}maybeResolveDrain(){if(!this.isIdle())return;const e=this.drainWaiters;this.drainWaiters=[];for(const t of e)t()}}class xe{client;collectionName;dims;log;constructor(e,t,i){this.client=new b.QdrantClient({url:e.url,apiKey:e.apiKey,checkCompatibility:!1}),this.collectionName=e.collectionName,this.dims=t,this.log=Z(i)}async ensureCollection(){try{const e=await this.client.getCollections();e.collections.some((e=>e.name===this.collectionName))||await this.client.createCollection(this.collectionName,{vectors:{size:this.dims,distance:"Cosine"}})}catch(e){throw new Error(`Failed to ensure collection "${this.collectionName}": ${String(e)}`)}}async upsert(e){0!==e.length&&await te((async t=>{t>1&&this.log.warn({attempt:t,operation:"qdrant.upsert",points:e.length},"Retrying Qdrant upsert"),await this.client.upsert(this.collectionName,{wait:!0,points:e.map((e=>({id:e.id,vector:e.vector,payload:e.payload})))})}),{attempts:5,baseDelayMs:500,maxDelayMs:1e4,jitter:.2,onRetry:({attempt:e,delayMs:t,error:i})=>{this.log.warn({attempt:e,delayMs:t,operation:"qdrant.upsert",err:S(i)},"Qdrant upsert failed; will retry")}})}async delete(e){0!==e.length&&await te((async t=>{t>1&&this.log.warn({attempt:t,operation:"qdrant.delete",ids:e.length},"Retrying Qdrant delete"),await this.client.delete(this.collectionName,{wait:!0,points:e})}),{attempts:5,baseDelayMs:500,maxDelayMs:1e4,jitter:.2,onRetry:({attempt:e,delayMs:t,error:i})=>{this.log.warn({attempt:e,delayMs:t,operation:"qdrant.delete",err:S(i)},"Qdrant delete failed; will retry")}})}async setPayload(e,t){0!==e.length&&await this.client.setPayload(this.collectionName,{wait:!0,points:e,payload:t})}async getPayload(e){try{const t=await this.client.retrieve(this.collectionName,{ids:[e],with_payload:!0,with_vector:!1});return 0===t.length?null:t[0].payload}catch{return null}}async search(e,t,i){return(await this.client.search(this.collectionName,{vector:e,limit:t,with_payload:!0,...i?{filter:i}:{}})).map((e=>({id:String(e.id),score:e.score,payload:e.payload})))}async*scroll(e,t=100){let i;for(;;){const r=await this.client.scroll(this.collectionName,{limit:t,with_payload:!0,with_vector:!1,...e?{filter:e}:{},...void 0!==i?{offset:i}:{}});for(const e of r.points)yield{id:String(e.id),payload:e.payload};const o=r.next_page_offset;if(null==o)break;if("string"!=typeof o&&"number"!=typeof o)break;i=o}}}class ze{config;queue;processor;logger;watcher;constructor(e,t,i,r){this.config=e,this.queue=t,this.processor=i,this.logger=r}start(){this.watcher=v.watch(this.config.paths,{ignored:this.config.ignored,usePolling:this.config.usePolling,interval:this.config.pollIntervalMs,awaitWriteFinish:!!this.config.stabilityThresholdMs&&{stabilityThreshold:this.config.stabilityThresholdMs},ignoreInitial:!1}),this.watcher.on("add",(e=>{this.logger.debug({path:e},"File added"),this.queue.enqueue({type:"create",path:e,priority:"normal"},(()=>this.processor.processFile(e)))})),this.watcher.on("change",(e=>{this.logger.debug({path:e},"File changed"),this.queue.enqueue({type:"modify",path:e,priority:"normal"},(()=>this.processor.processFile(e)))})),this.watcher.on("unlink",(e=>{this.logger.debug({path:e},"File removed"),this.queue.enqueue({type:"delete",path:e,priority:"normal"},(()=>this.processor.deleteFile(e)))})),this.watcher.on("error",(e=>{this.logger.error({err:S(e)},"Watcher error")})),this.queue.process(),this.logger.info({paths:this.config.paths},"Filesystem watcher started")}async stop(){this.watcher&&(await this.watcher.close(),this.watcher=void 0,this.logger.info("Filesystem watcher stopped"))}}class je{options;watcher;debounce;constructor(e){this.options=e}start(){this.options.enabled&&(this.watcher=v.watch(this.options.configPath,{ignoreInitial:!0}),this.watcher.on("change",(()=>{this.debounce&&clearTimeout(this.debounce),this.debounce=setTimeout((()=>{this.options.onChange()}),this.options.debounceMs)})),this.watcher.on("error",(e=>{this.options.logger.error({err:S(e)},"Config watcher error")})),this.options.logger.info({configPath:this.options.configPath,debounceMs:this.options.debounceMs},"Config watcher started"))}async stop(){this.debounce&&(clearTimeout(this.debounce),this.debounce=void 0),this.watcher&&(await this.watcher.close(),this.watcher=void 0)}}const Ce={loadConfig:Y,createLogger:oe,createEmbeddingProvider:re,createVectorStoreClient:(e,t,i)=>new xe(e,t,i),compileRules:be,createDocumentProcessor:(e,t,i,r,o)=>new Se(e,t,i,r,o),createEventQueue:e=>new ke(e),createFileSystemWatcher:(e,t,i,r)=>new ze(e,t,i,r),createApiServer:I};class Fe{config;configPath;factories;logger;watcher;queue;server;processor;configWatcher;constructor(e,t,i={}){this.config=e,this.configPath=t,this.factories={...Ce,...i}}async start(){const e=this.factories.createLogger(this.config.logging);let t;this.logger=e;try{t=this.factories.createEmbeddingProvider(this.config.embedding,e)}catch(t){throw e.fatal({err:S(t)},"Failed to create embedding provider"),t}const i=this.factories.createVectorStoreClient(this.config.vectorStore,t.dimensions,e);await i.ensureCollection();const r=this.factories.compileRules(this.config.inferenceRules??[]),o={metadataDir:this.config.metadataDir??".jeeves-metadata",chunkSize:this.config.embedding.chunkSize,chunkOverlap:this.config.embedding.chunkOverlap,maps:this.config.maps},n=this.factories.createDocumentProcessor(o,t,i,r,e);this.processor=n;const s=this.factories.createEventQueue({debounceMs:this.config.watch.debounceMs??2e3,concurrency:this.config.embedding.concurrency??5,rateLimitPerMinute:this.config.embedding.rateLimitPerMinute});this.queue=s;const a=this.factories.createFileSystemWatcher(this.config.watch,s,n,e);this.watcher=a;const c=this.factories.createApiServer({processor:n,vectorStore:i,embeddingProvider:t,queue:s,config:this.config,logger:e});this.server=c,await c.listen({host:this.config.api?.host??"127.0.0.1",port:this.config.api?.port??3456}),a.start(),this.startConfigWatch(),e.info("jeeves-watcher started")}async stop(){if(await this.stopConfigWatch(),this.watcher&&await this.watcher.stop(),this.queue){const e=this.config.shutdownTimeoutMs??1e4;await Promise.race([this.queue.drain().then((()=>!0)),new Promise((t=>{setTimeout((()=>{t(!1)}),e)}))])||this.logger?.warn({timeoutMs:e},"Queue drain timeout hit, forcing shutdown")}this.server&&await this.server.close(),this.logger?.info("jeeves-watcher stopped")}startConfigWatch(){const e=this.logger;if(!e)return;const t=this.config.configWatch?.enabled??!0;if(!t)return;if(!this.configPath)return void e.debug("Config watch enabled, but no config path was provided");const i=this.config.configWatch?.debounceMs??1e4;this.configWatcher=new je({configPath:this.configPath,enabled:t,debounceMs:i,logger:e,onChange:async()=>this.reloadConfig()}),this.configWatcher.start()}async stopConfigWatch(){this.configWatcher&&(await this.configWatcher.stop(),this.configWatcher=void 0)}async reloadConfig(){const e=this.logger,t=this.processor;if(e&&t&&this.configPath){e.info({configPath:this.configPath},"Config change detected, reloading...");try{const i=await this.factories.loadConfig(this.configPath);this.config=i;const r=this.factories.compileRules(i.inferenceRules??[]);t.updateRules(r),e.info({configPath:this.configPath,rules:r.length},"Config reloaded")}catch(t){e.error({err:S(t)},"Failed to reload config")}}}}e.DocumentProcessor=Se,e.EventQueue=ke,e.FileSystemWatcher=ze,e.JeevesWatcher=Fe,e.VectorStoreClient=xe,e.apiConfigSchema=K,e.applyRules=ye,e.buildAttributes=we,e.compileRules=be,e.configWatchConfigSchema=O,e.contentHash=ne,e.createApiServer=I,e.createEmbeddingProvider=re,e.createLogger=oe,e.deleteMetadata=T,e.embeddingConfigSchema=Q,e.extractText=fe,e.inferenceRuleSchema=G,e.jeevesWatcherConfigSchema=U,e.loadConfig=Y,e.loggingConfigSchema=J,e.metadataPath=C,e.pointId=ae,e.readMetadata=F,e.startFromConfig=async function(e){const t=await Y(e),i=new Fe(t,e);return function(e){const t=async()=>{await e(),process.exit(0)};process.on("SIGTERM",(()=>{t()})),process.on("SIGINT",(()=>{t()}))}((()=>i.stop())),await i.start(),i},e.vectorStoreConfigSchema=$,e.watchConfigSchema=L,e.writeMetadata=R}(this["jeeves-watcher"]=this["jeeves-watcher"]||{},Fastify,promises,node_path,picomatch,radash,node_crypto,cosmiconfig,zod,jsonmap,googleGenai,pino,uuid,cheerio,yaml,mammoth,Ajv,addFormats,textsplitters,jsClientRest,chokidar);
package/dist/mjs/index.js CHANGED
@@ -19,6 +19,31 @@ import { MarkdownTextSplitter, RecursiveCharacterTextSplitter } from '@langchain
19
19
  import { QdrantClient } from '@qdrant/js-client-rest';
20
20
  import chokidar from 'chokidar';
21
21
 
22
+ /**
23
+ * @module util/normalizeError
24
+ *
25
+ * Normalizes unknown thrown values into proper Error objects for pino serialization.
26
+ */
27
+ /**
28
+ * Convert an unknown thrown value into a proper Error with message, stack, and cause.
29
+ * Pino's built-in `err` serializer requires an Error instance to extract message/stack.
30
+ *
31
+ * @param error - The caught value (may not be an Error).
32
+ * @returns A proper Error instance.
33
+ */
34
+ function normalizeError(error) {
35
+ if (error instanceof Error)
36
+ return error;
37
+ if (typeof error === 'string')
38
+ return new Error(error);
39
+ const message = typeof error === 'object' && error !== null && 'message' in error
40
+ ? String(error.message)
41
+ : String(error);
42
+ const normalized = new Error(message);
43
+ normalized.cause = error;
44
+ return normalized;
45
+ }
46
+
22
47
  /**
23
48
  * Best-effort base directory inference for a glob pattern.
24
49
  *
@@ -141,13 +166,13 @@ function createConfigReindexHandler(deps) {
141
166
  }
142
167
  }
143
168
  catch (error) {
144
- deps.logger.error({ error, scope }, 'Config reindex failed');
169
+ deps.logger.error({ err: normalizeError(error), scope }, 'Config reindex failed');
145
170
  }
146
171
  })();
147
172
  return await reply.status(200).send({ status: 'started', scope });
148
173
  }
149
174
  catch (error) {
150
- deps.logger.error({ error }, 'Config reindex request failed');
175
+ deps.logger.error({ err: normalizeError(error) }, 'Config reindex request failed');
151
176
  return await reply.status(500).send({ error: 'Internal server error' });
152
177
  }
153
178
  };
@@ -170,7 +195,7 @@ function createMetadataHandler(deps) {
170
195
  return { ok: true };
171
196
  }
172
197
  catch (error) {
173
- deps.logger.error({ error }, 'Metadata update failed');
198
+ deps.logger.error({ err: normalizeError(error) }, 'Metadata update failed');
174
199
  return reply.status(500).send({ error: 'Internal server error' });
175
200
  }
176
201
  };
@@ -293,7 +318,7 @@ function createRebuildMetadataHandler(deps) {
293
318
  return await reply.status(200).send({ ok: true });
294
319
  }
295
320
  catch (error) {
296
- deps.logger.error({ error }, 'Rebuild metadata failed');
321
+ deps.logger.error({ err: normalizeError(error) }, 'Rebuild metadata failed');
297
322
  return await reply.status(500).send({ error: 'Internal server error' });
298
323
  }
299
324
  };
@@ -315,7 +340,7 @@ function createReindexHandler(deps) {
315
340
  return await reply.status(200).send({ ok: true, filesIndexed: count });
316
341
  }
317
342
  catch (error) {
318
- deps.logger.error({ error }, 'Reindex failed');
343
+ deps.logger.error({ err: normalizeError(error) }, 'Reindex failed');
319
344
  return await reply.status(500).send({ error: 'Internal server error' });
320
345
  }
321
346
  };
@@ -339,7 +364,7 @@ function createSearchHandler(deps) {
339
364
  return results;
340
365
  }
341
366
  catch (error) {
342
- deps.logger.error({ error }, 'Search failed');
367
+ deps.logger.error({ err: normalizeError(error) }, 'Search failed');
343
368
  return reply.status(500).send({ error: 'Internal server error' });
344
369
  }
345
370
  };
@@ -621,6 +646,49 @@ const jeevesWatcherConfigSchema = z.object({
621
646
  .describe('Timeout in milliseconds for graceful shutdown.'),
622
647
  });
623
648
 
649
+ /**
650
+ * @module config/substituteEnvVars
651
+ *
652
+ * Deep-walks config objects and replaces `${VAR_NAME}` patterns with environment variable values.
653
+ */
654
+ const ENV_PATTERN = /\$\{([^}]+)\}/g;
655
+ /**
656
+ * Replace `${VAR_NAME}` patterns in a string with `process.env.VAR_NAME`.
657
+ *
658
+ * @param value - The string to process.
659
+ * @returns The string with resolved env vars; unresolvable expressions left untouched.
660
+ */
661
+ function substituteString(value) {
662
+ return value.replace(ENV_PATTERN, (match, varName) => {
663
+ const envValue = process.env[varName];
664
+ if (envValue === undefined)
665
+ return match;
666
+ return envValue;
667
+ });
668
+ }
669
+ /**
670
+ * Deep-walk a value and substitute `${VAR_NAME}` patterns in all string values.
671
+ *
672
+ * @param value - The value to walk (object, array, or primitive).
673
+ * @returns A new value with all env var references resolved.
674
+ */
675
+ function substituteEnvVars(value) {
676
+ if (typeof value === 'string') {
677
+ return substituteString(value);
678
+ }
679
+ if (Array.isArray(value)) {
680
+ return value.map((item) => substituteEnvVars(item));
681
+ }
682
+ if (value !== null && typeof value === 'object') {
683
+ const result = {};
684
+ for (const [key, val] of Object.entries(value)) {
685
+ result[key] = substituteEnvVars(val);
686
+ }
687
+ return result;
688
+ }
689
+ return value;
690
+ }
691
+
624
692
  const MODULE_NAME = 'jeeves-watcher';
625
693
  /**
626
694
  * Merge sensible defaults into a loaded configuration.
@@ -656,7 +724,8 @@ async function loadConfig(configPath) {
656
724
  }
657
725
  try {
658
726
  const validated = jeevesWatcherConfigSchema.parse(result.config);
659
- return applyDefaults(validated);
727
+ const withDefaults = applyDefaults(validated);
728
+ return substituteEnvVars(withDefaults);
660
729
  }
661
730
  catch (error) {
662
731
  if (error instanceof ZodError) {
@@ -823,7 +892,7 @@ function createGeminiProvider(config, logger) {
823
892
  delayMs,
824
893
  provider: 'gemini',
825
894
  model: config.model,
826
- error,
895
+ err: normalizeError(error),
827
896
  }, 'Embedding call failed; will retry');
828
897
  },
829
898
  });
@@ -932,6 +1001,9 @@ function pointId(filePath, chunkIndex) {
932
1001
  */
933
1002
  function extractMarkdownFrontmatter(markdown) {
934
1003
  const trimmed = markdown.replace(/^\uFEFF/, '');
1004
+ // Only attempt frontmatter parsing if the file starts with ---
1005
+ if (!/^\s*---/.test(trimmed))
1006
+ return { body: markdown };
935
1007
  const match = /^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/m.exec(trimmed);
936
1008
  if (!match)
937
1009
  return { body: markdown };
@@ -1389,7 +1461,7 @@ class DocumentProcessor {
1389
1461
  this.logger.info({ filePath, chunks: chunks.length }, 'File processed successfully');
1390
1462
  }
1391
1463
  catch (error) {
1392
- this.logger.error({ filePath, error }, 'Failed to process file');
1464
+ this.logger.error({ filePath, err: normalizeError(error) }, 'Failed to process file');
1393
1465
  }
1394
1466
  }
1395
1467
  /**
@@ -1409,7 +1481,7 @@ class DocumentProcessor {
1409
1481
  this.logger.info({ filePath }, 'File deleted from index');
1410
1482
  }
1411
1483
  catch (error) {
1412
- this.logger.error({ filePath, error }, 'Failed to delete file');
1484
+ this.logger.error({ filePath, err: normalizeError(error) }, 'Failed to delete file');
1413
1485
  }
1414
1486
  }
1415
1487
  /**
@@ -1437,7 +1509,7 @@ class DocumentProcessor {
1437
1509
  return merged;
1438
1510
  }
1439
1511
  catch (error) {
1440
- this.logger.error({ filePath, error }, 'Failed to update metadata');
1512
+ this.logger.error({ filePath, err: normalizeError(error) }, 'Failed to update metadata');
1441
1513
  return null;
1442
1514
  }
1443
1515
  }
@@ -1467,7 +1539,7 @@ class DocumentProcessor {
1467
1539
  return metadata;
1468
1540
  }
1469
1541
  catch (error) {
1470
- this.logger.error({ filePath, error }, 'Failed to re-apply rules');
1542
+ this.logger.error({ filePath, err: normalizeError(error) }, 'Failed to re-apply rules');
1471
1543
  return null;
1472
1544
  }
1473
1545
  }
@@ -1695,7 +1767,12 @@ class VectorStoreClient {
1695
1767
  maxDelayMs: 10_000,
1696
1768
  jitter: 0.2,
1697
1769
  onRetry: ({ attempt, delayMs, error }) => {
1698
- this.log.warn({ attempt, delayMs, operation: 'qdrant.upsert', error }, 'Qdrant upsert failed; will retry');
1770
+ this.log.warn({
1771
+ attempt,
1772
+ delayMs,
1773
+ operation: 'qdrant.upsert',
1774
+ err: normalizeError(error),
1775
+ }, 'Qdrant upsert failed; will retry');
1699
1776
  },
1700
1777
  });
1701
1778
  }
@@ -1721,7 +1798,12 @@ class VectorStoreClient {
1721
1798
  maxDelayMs: 10_000,
1722
1799
  jitter: 0.2,
1723
1800
  onRetry: ({ attempt, delayMs, error }) => {
1724
- this.log.warn({ attempt, delayMs, operation: 'qdrant.delete', error }, 'Qdrant delete failed; will retry');
1801
+ this.log.warn({
1802
+ attempt,
1803
+ delayMs,
1804
+ operation: 'qdrant.delete',
1805
+ err: normalizeError(error),
1806
+ }, 'Qdrant delete failed; will retry');
1725
1807
  },
1726
1808
  });
1727
1809
  }
@@ -1875,7 +1957,7 @@ class FileSystemWatcher {
1875
1957
  this.queue.enqueue({ type: 'delete', path, priority: 'normal' }, () => this.processor.deleteFile(path));
1876
1958
  });
1877
1959
  this.watcher.on('error', (error) => {
1878
- this.logger.error({ error }, 'Watcher error');
1960
+ this.logger.error({ err: normalizeError(error) }, 'Watcher error');
1879
1961
  });
1880
1962
  this.queue.process();
1881
1963
  this.logger.info({ paths: this.config.paths }, 'Filesystem watcher started');
@@ -1920,7 +2002,7 @@ class ConfigWatcher {
1920
2002
  }, this.options.debounceMs);
1921
2003
  });
1922
2004
  this.watcher.on('error', (error) => {
1923
- this.options.logger.error({ error }, 'Config watcher error');
2005
+ this.options.logger.error({ err: normalizeError(error) }, 'Config watcher error');
1924
2006
  });
1925
2007
  this.options.logger.info({
1926
2008
  configPath: this.options.configPath,
@@ -2004,7 +2086,7 @@ class JeevesWatcher {
2004
2086
  embeddingProvider = this.factories.createEmbeddingProvider(this.config.embedding, logger);
2005
2087
  }
2006
2088
  catch (error) {
2007
- logger.fatal({ error }, 'Failed to create embedding provider');
2089
+ logger.fatal({ err: normalizeError(error) }, 'Failed to create embedding provider');
2008
2090
  throw error;
2009
2091
  }
2010
2092
  const vectorStore = this.factories.createVectorStoreClient(this.config.vectorStore, embeddingProvider.dimensions, logger);
@@ -2111,7 +2193,7 @@ class JeevesWatcher {
2111
2193
  logger.info({ configPath: this.configPath, rules: compiledRules.length }, 'Config reloaded');
2112
2194
  }
2113
2195
  catch (error) {
2114
- logger.error({ error }, 'Failed to reload config');
2196
+ logger.error({ err: normalizeError(error) }, 'Failed to reload config');
2115
2197
  }
2116
2198
  }
2117
2199
  }
package/package.json CHANGED
@@ -171,5 +171,5 @@
171
171
  },
172
172
  "type": "module",
173
173
  "types": "dist/index.d.ts",
174
- "version": "0.2.1"
174
+ "version": "0.2.3"
175
175
  }